From bd2768b67689d4b76e265153f3e2d1ab4bef7f1e Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 25 Jul 2026 15:15:24 -0400 Subject: [PATCH] Add data-driven fish selection and catch showcase --- collection/collection_log.gd | 17 ++ collection/collection_log.gd.uid | 1 + fish/bluegill.tres | 10 - fish/default_availability.tres | 10 + fish/fish_availability.gd | 63 ++++ fish/fish_availability.gd.uid | 1 + fish/fish_catch.gd | 20 ++ fish/fish_catch.gd.uid | 1 + fish/fish_data.gd | 63 +++- fish/fish_pool.gd | 6 + fish/fish_pool.gd.uid | 1 + fish/fish_selector.gd | 78 +++++ fish/fish_selector.gd.uid | 1 + fish/pools/test_water_pool.tres | 10 + fish/species/bass/bass.svg | 6 + fish/species/bass/bass.svg.import | 43 +++ fish/species/bass/bass.tres | 28 ++ fish/species/bluegill/bluegill.svg | 6 + fish/species/bluegill/bluegill.svg.import | 43 +++ fish/species/bluegill/bluegill.tres | 21 ++ fish/species/carp/carp.svg | 6 + fish/species/carp/carp.svg.import | 43 +++ fish/species/carp/carp.tres | 28 ++ fishing/bass_catch_profile.tres | 16 + fishing/carp_catch_profile.tres | 16 + fishing/fishing_context.gd | 8 + fishing/fishing_context.gd.uid | 1 + fishing/fishing_presentation.gd | 8 +- fishing/fishing_spot.gd | 236 ++++++++++++++- fishing/fishing_spot.tscn | 4 +- inventory/fish_inventory.gd | 31 +- main/main.gd | 6 +- player/player.gd | 348 +++++++++++++++++++++- player/player.tscn | 23 +- ui/game_ui.gd | 45 ++- ui/game_ui.tscn | 19 +- world/fishable_water_region.gd | 8 + world/fishable_water_region.gd.uid | 1 + world/test_world.tscn | 8 +- 39 files changed, 1226 insertions(+), 58 deletions(-) create mode 100644 collection/collection_log.gd create mode 100644 collection/collection_log.gd.uid delete mode 100644 fish/bluegill.tres create mode 100644 fish/default_availability.tres create mode 100644 fish/fish_availability.gd create mode 100644 fish/fish_availability.gd.uid create mode 100644 fish/fish_catch.gd create mode 100644 fish/fish_catch.gd.uid create mode 100644 fish/fish_pool.gd create mode 100644 fish/fish_pool.gd.uid create mode 100644 fish/fish_selector.gd create mode 100644 fish/fish_selector.gd.uid create mode 100644 fish/pools/test_water_pool.tres create mode 100644 fish/species/bass/bass.svg create mode 100644 fish/species/bass/bass.svg.import create mode 100644 fish/species/bass/bass.tres create mode 100644 fish/species/bluegill/bluegill.svg create mode 100644 fish/species/bluegill/bluegill.svg.import create mode 100644 fish/species/bluegill/bluegill.tres create mode 100644 fish/species/carp/carp.svg create mode 100644 fish/species/carp/carp.svg.import create mode 100644 fish/species/carp/carp.tres create mode 100644 fishing/bass_catch_profile.tres create mode 100644 fishing/carp_catch_profile.tres create mode 100644 fishing/fishing_context.gd create mode 100644 fishing/fishing_context.gd.uid create mode 100644 world/fishable_water_region.gd create mode 100644 world/fishable_water_region.gd.uid diff --git a/collection/collection_log.gd b/collection/collection_log.gd new file mode 100644 index 0000000..c068a6e --- /dev/null +++ b/collection/collection_log.gd @@ -0,0 +1,17 @@ +class_name CollectionLog +extends Node + +signal fish_discovered(fish_id: StringName) + +var _discovered: Dictionary[StringName, bool] = {} + + +func has_discovered(fish_id: StringName) -> bool: + return not fish_id.is_empty() and _discovered.has(fish_id) + + +func mark_discovered(fish_id: StringName) -> void: + if fish_id.is_empty() or has_discovered(fish_id): + return + _discovered[fish_id] = true + fish_discovered.emit(fish_id) diff --git a/collection/collection_log.gd.uid b/collection/collection_log.gd.uid new file mode 100644 index 0000000..a2b229f --- /dev/null +++ b/collection/collection_log.gd.uid @@ -0,0 +1 @@ +uid://dwaig3bnt3xy3 diff --git a/fish/bluegill.tres b/fish/bluegill.tres deleted file mode 100644 index 572d202..0000000 --- a/fish/bluegill.tres +++ /dev/null @@ -1,10 +0,0 @@ -[gd_resource type="Resource" load_steps=3 format=3] - -[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] -[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] - -[resource] -script = ExtResource("1_fish_data") -id = &"bluegill" -display_name = "Bluegill" -catch_profile = ExtResource("2_profile") diff --git a/fish/default_availability.tres b/fish/default_availability.tres new file mode 100644 index 0000000..3c0cfec --- /dev/null +++ b/fish/default_availability.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" load_steps=2 format=3] + +[ext_resource type="Script" path="res://fish/fish_availability.gd" id="1_availability"] + +[resource] +script = ExtResource("1_availability") +allow_day = true +allow_night = true +preferred_bait_tags = Array[StringName]([&"worm"]) +preferred_bait_weight_multiplier = 1.35 diff --git a/fish/fish_availability.gd b/fish/fish_availability.gd new file mode 100644 index 0000000..f593d75 --- /dev/null +++ b/fish/fish_availability.gd @@ -0,0 +1,63 @@ +class_name FishAvailability +extends Resource + +const FishingContextType = preload("res://fishing/fishing_context.gd") + +@export var allowed_location_tags: Array[StringName] = [] +@export var required_event_tags: Array[StringName] = [] +@export var forbidden_event_tags: Array[StringName] = [] +@export var allow_day: bool = true +@export var allow_night: bool = true +@export var require_rain: bool = false +@export var forbid_rain: bool = false +@export var required_bait_tags: Array[StringName] = [] +@export var preferred_bait_tags: Array[StringName] = [] +@export_range(0.01, 10.0, 0.01) var preferred_bait_weight_multiplier: float = 1.25 + + +func is_available(context: FishingContextType) -> bool: + if context == null or (require_rain and forbid_rain): + return false + if context.is_night and not allow_night: + return false + if not context.is_night and not allow_day: + return false + if require_rain and not context.is_raining: + return false + if forbid_rain and context.is_raining: + return false + if ( + not allowed_location_tags.is_empty() + and not _has_any_match(allowed_location_tags, context.location_tags) + ): + return false + for required_tag: StringName in required_event_tags: + if required_tag not in context.active_event_tags: + return false + for forbidden_tag: StringName in forbidden_event_tags: + if forbidden_tag in context.active_event_tags: + return false + if ( + not required_bait_tags.is_empty() + and not _has_any_match(required_bait_tags, context.active_bait_tags) + ): + return false + return true + + +func get_bait_weight_multiplier(context: FishingContextType) -> float: + if context == null: + return 1.0 + if _has_any_match(preferred_bait_tags, context.active_bait_tags): + return maxf(preferred_bait_weight_multiplier, 0.01) + return 1.0 + + +func _has_any_match( + required_tags: Array[StringName], + active_tags: Array[StringName], +) -> bool: + for tag: StringName in required_tags: + if tag in active_tags: + return true + return false diff --git a/fish/fish_availability.gd.uid b/fish/fish_availability.gd.uid new file mode 100644 index 0000000..0a50d9a --- /dev/null +++ b/fish/fish_availability.gd.uid @@ -0,0 +1 @@ +uid://bs7cqi88csolc diff --git a/fish/fish_catch.gd b/fish/fish_catch.gd new file mode 100644 index 0000000..f79b3df --- /dev/null +++ b/fish/fish_catch.gd @@ -0,0 +1,20 @@ +class_name FishCatch +extends Resource + +const FishDataType = preload("res://fish/fish_data.gd") + +@export var fish: FishDataType +@export var fish_id: StringName +@export var weight_lb: float = 0.0 +@export var display_scale: float = 1.0 + + +func is_valid() -> bool: + return ( + fish != null + and not fish_id.is_empty() + and fish.id == fish_id + and weight_lb >= fish.get_minimum_weight() + and weight_lb <= fish.get_maximum_weight() + and display_scale > 0.0 + ) diff --git a/fish/fish_catch.gd.uid b/fish/fish_catch.gd.uid new file mode 100644 index 0000000..228bd15 --- /dev/null +++ b/fish/fish_catch.gd.uid @@ -0,0 +1 @@ +uid://dnocndoiq3lg4 diff --git a/fish/fish_data.gd b/fish/fish_data.gd index 2ab3994..8979864 100644 --- a/fish/fish_data.gd +++ b/fish/fish_data.gd @@ -4,8 +4,69 @@ extends Resource const CatchDifficultyProfileType = preload( "res://fishing/catch_difficulty_profile.gd" ) +const FishAvailabilityType = preload("res://fish/fish_availability.gd") + +enum Rarity { + COMMON, + UNCOMMON, + RARE, + EPIC, + LEGENDARY, +} @export var id: StringName @export var display_name: String -@export var icon: Texture2D +@export var rarity: Rarity = Rarity.COMMON +@export_range(0.0, 1000.0, 0.01) var base_catch_weight: float = 1.0 @export var catch_profile: CatchDifficultyProfileType +@export var availability: FishAvailabilityType +@export_range(0.01, 1000.0, 0.01) var weight_min_lb: float = 0.5 +@export_range(0.01, 1000.0, 0.01) var weight_max_lb: float = 1.0 +@export_range(0.01, 20.0, 0.01) var display_scale_min: float = 0.8 +@export_range(0.01, 20.0, 0.01) var display_scale_max: float = 1.2 +@export_range(0.01, 10.0, 0.01) var display_scale_curve: float = 1.0 +@export var display_texture: Texture2D + + +func is_selectable() -> bool: + return ( + not id.is_empty() + and base_catch_weight > 0.0 + and catch_profile != null + and weight_min_lb > 0.0 + and weight_max_lb >= weight_min_lb + and display_scale_min > 0.0 + and display_scale_max >= display_scale_min + and display_scale_curve > 0.0 + ) + + +func get_minimum_weight() -> float: + return maxf(weight_min_lb, 0.01) + + +func get_maximum_weight() -> float: + return maxf(weight_max_lb, get_minimum_weight()) + + +func get_display_scale_for_weight(weight_lb: float) -> float: + var minimum_weight: float = get_minimum_weight() + var maximum_weight: float = get_maximum_weight() + var normalized_weight: float = 0.0 + if maximum_weight > minimum_weight: + normalized_weight = inverse_lerp( + minimum_weight, + maximum_weight, + weight_lb + ) + normalized_weight = pow( + clampf(normalized_weight, 0.0, 1.0), + maxf(display_scale_curve, 0.01) + ) + var minimum_scale: float = maxf(display_scale_min, 0.01) + var maximum_scale: float = maxf(display_scale_max, minimum_scale) + return lerpf(minimum_scale, maximum_scale, normalized_weight) + + +func get_rarity_name() -> String: + return Rarity.keys()[rarity].capitalize() diff --git a/fish/fish_pool.gd b/fish/fish_pool.gd new file mode 100644 index 0000000..4021986 --- /dev/null +++ b/fish/fish_pool.gd @@ -0,0 +1,6 @@ +class_name FishPool +extends Resource + +const FishDataType = preload("res://fish/fish_data.gd") + +@export var candidates: Array[FishDataType] = [] diff --git a/fish/fish_pool.gd.uid b/fish/fish_pool.gd.uid new file mode 100644 index 0000000..06e813d --- /dev/null +++ b/fish/fish_pool.gd.uid @@ -0,0 +1 @@ +uid://qg6pu6l216ei diff --git a/fish/fish_selector.gd b/fish/fish_selector.gd new file mode 100644 index 0000000..837c422 --- /dev/null +++ b/fish/fish_selector.gd @@ -0,0 +1,78 @@ +class_name FishSelector +extends RefCounted + +const CollectionLogType = preload("res://collection/collection_log.gd") +const FishCatchType = preload("res://fish/fish_catch.gd") +const FishDataType = preload("res://fish/fish_data.gd") +const FishPoolType = preload("res://fish/fish_pool.gd") +const FishingContextType = preload("res://fishing/fishing_context.gd") + +var undiscovered_weight_multiplier: float = 1.5 +var use_deterministic_test_seed: bool = false +var deterministic_test_seed: int = 24680 +var selection_seed: int = 0 + +var _rng := RandomNumberGenerator.new() + + +func begin_roll() -> void: + if use_deterministic_test_seed: + selection_seed = deterministic_test_seed + _rng.seed = deterministic_test_seed + else: + _rng.randomize() + selection_seed = _rng.seed + + +func select_fish( + pool: FishPoolType, + context: FishingContextType, + collection_log: CollectionLogType, +) -> FishDataType: + if pool == null or context == null or collection_log == null: + return null + + var eligible_fish: Array[FishDataType] = [] + var weights: Array[float] = [] + var total_weight: float = 0.0 + for fish: FishDataType in pool.candidates: + if fish == null or not fish.is_selectable(): + continue + if fish.availability != null and not fish.availability.is_available(context): + continue + var final_weight: float = fish.base_catch_weight + if fish.availability != null: + final_weight *= fish.availability.get_bait_weight_multiplier(context) + if not collection_log.has_discovered(fish.id): + final_weight *= maxf(undiscovered_weight_multiplier, 0.0) + if final_weight <= 0.0: + continue + eligible_fish.append(fish) + weights.append(final_weight) + total_weight += final_weight + + if eligible_fish.is_empty() or total_weight <= 0.0: + return null + var roll: float = _rng.randf() * total_weight + var accumulated_weight: float = 0.0 + for index: int in range(eligible_fish.size()): + accumulated_weight += weights[index] + if roll <= accumulated_weight: + return eligible_fish[index] + return eligible_fish.back() + + +func create_catch(fish: FishDataType) -> FishCatchType: + if fish == null or not fish.is_selectable(): + return null + var caught_fish := FishCatchType.new() + caught_fish.fish = fish + caught_fish.fish_id = fish.id + caught_fish.weight_lb = _rng.randf_range( + fish.get_minimum_weight(), + fish.get_maximum_weight() + ) + caught_fish.display_scale = fish.get_display_scale_for_weight( + caught_fish.weight_lb + ) + return caught_fish diff --git a/fish/fish_selector.gd.uid b/fish/fish_selector.gd.uid new file mode 100644 index 0000000..b0ed527 --- /dev/null +++ b/fish/fish_selector.gd.uid @@ -0,0 +1 @@ +uid://csooreckpm0hh diff --git a/fish/pools/test_water_pool.tres b/fish/pools/test_water_pool.tres new file mode 100644 index 0000000..96d61a5 --- /dev/null +++ b/fish/pools/test_water_pool.tres @@ -0,0 +1,10 @@ +[gd_resource type="Resource" load_steps=5 format=3] + +[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"] +[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"] +[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="3_bass"] +[ext_resource type="Resource" path="res://fish/species/carp/carp.tres" id="4_carp"] + +[resource] +script = ExtResource("1_pool") +candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp")] diff --git a/fish/species/bass/bass.svg b/fish/species/bass/bass.svg new file mode 100644 index 0000000..feeef65 --- /dev/null +++ b/fish/species/bass/bass.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/fish/species/bass/bass.svg.import b/fish/species/bass/bass.svg.import new file mode 100644 index 0000000..2455d7f --- /dev/null +++ b/fish/species/bass/bass.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2510mmpn6cos" +path="res://.godot/imported/bass.svg-61a5b49cdcc6e48a0677d179d664071b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/bass/bass.svg" +dest_files=["res://.godot/imported/bass.svg-61a5b49cdcc6e48a0677d179d664071b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/fish/species/bass/bass.tres b/fish/species/bass/bass.tres new file mode 100644 index 0000000..028c2cd --- /dev/null +++ b/fish/species/bass/bass.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" load_steps=5 format=3] + +[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] +[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"] +[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"] +[ext_resource type="Texture2D" path="res://fish/species/bass/bass.svg" id="4_texture"] + +[sub_resource type="Resource" id="BassAvailability"] +script = ExtResource("3_availability_script") +allow_day = true +allow_night = true +preferred_bait_tags = Array[StringName]([&"worm", &"minnow"]) +preferred_bait_weight_multiplier = 1.4 + +[resource] +script = ExtResource("1_fish_data") +id = &"bass" +display_name = "Bass" +rarity = 1 +base_catch_weight = 4.0 +catch_profile = ExtResource("2_profile") +availability = SubResource("BassAvailability") +weight_min_lb = 1.5 +weight_max_lb = 8.0 +display_scale_min = 0.9 +display_scale_max = 1.5 +display_scale_curve = 0.9 +display_texture = ExtResource("4_texture") diff --git a/fish/species/bluegill/bluegill.svg b/fish/species/bluegill/bluegill.svg new file mode 100644 index 0000000..1e8c8aa --- /dev/null +++ b/fish/species/bluegill/bluegill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/fish/species/bluegill/bluegill.svg.import b/fish/species/bluegill/bluegill.svg.import new file mode 100644 index 0000000..92d9e25 --- /dev/null +++ b/fish/species/bluegill/bluegill.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dye6e87xuabvm" +path="res://.godot/imported/bluegill.svg-e8296ec8dd9d840b3ba713e441cd7e1e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/bluegill/bluegill.svg" +dest_files=["res://.godot/imported/bluegill.svg-e8296ec8dd9d840b3ba713e441cd7e1e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres new file mode 100644 index 0000000..5681fb0 --- /dev/null +++ b/fish/species/bluegill/bluegill.tres @@ -0,0 +1,21 @@ +[gd_resource type="Resource" load_steps=5 format=3] + +[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] +[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] +[ext_resource type="Resource" path="res://fish/default_availability.tres" id="3_availability"] +[ext_resource type="Texture2D" path="res://fish/species/bluegill/bluegill.svg" id="4_texture"] + +[resource] +script = ExtResource("1_fish_data") +id = &"bluegill" +display_name = "Bluegill" +rarity = 0 +base_catch_weight = 10.0 +catch_profile = ExtResource("2_profile") +availability = ExtResource("3_availability") +weight_min_lb = 0.3 +weight_max_lb = 2.0 +display_scale_min = 0.7 +display_scale_max = 1.1 +display_scale_curve = 1.0 +display_texture = ExtResource("4_texture") diff --git a/fish/species/carp/carp.svg b/fish/species/carp/carp.svg new file mode 100644 index 0000000..412ceb9 --- /dev/null +++ b/fish/species/carp/carp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/fish/species/carp/carp.svg.import b/fish/species/carp/carp.svg.import new file mode 100644 index 0000000..afb7f94 --- /dev/null +++ b/fish/species/carp/carp.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cv0egn63xwn3c" +path="res://.godot/imported/carp.svg-0d5425bc65e17d4f31a20a81115b2a99.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/carp/carp.svg" +dest_files=["res://.godot/imported/carp.svg-0d5425bc65e17d4f31a20a81115b2a99.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/fish/species/carp/carp.tres b/fish/species/carp/carp.tres new file mode 100644 index 0000000..20c3194 --- /dev/null +++ b/fish/species/carp/carp.tres @@ -0,0 +1,28 @@ +[gd_resource type="Resource" load_steps=5 format=3] + +[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] +[ext_resource type="Resource" path="res://fishing/carp_catch_profile.tres" id="2_profile"] +[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"] +[ext_resource type="Texture2D" path="res://fish/species/carp/carp.svg" id="4_texture"] + +[sub_resource type="Resource" id="CarpAvailability"] +script = ExtResource("3_availability_script") +allow_day = true +allow_night = true +preferred_bait_tags = Array[StringName]([&"corn", &"bread"]) +preferred_bait_weight_multiplier = 1.5 + +[resource] +script = ExtResource("1_fish_data") +id = &"carp" +display_name = "Carp" +rarity = 2 +base_catch_weight = 1.5 +catch_profile = ExtResource("2_profile") +availability = SubResource("CarpAvailability") +weight_min_lb = 3.0 +weight_max_lb = 18.0 +display_scale_min = 1.0 +display_scale_max = 1.8 +display_scale_curve = 0.85 +display_texture = ExtResource("4_texture") diff --git a/fishing/bass_catch_profile.tres b/fishing/bass_catch_profile.tres new file mode 100644 index 0000000..077b229 --- /dev/null +++ b/fishing/bass_catch_profile.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" load_steps=2 format=3] + +[ext_resource type="Script" path="res://fishing/catch_difficulty_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") +barrier_count_min = 3 +barrier_count_max = 4 +barrier_health_min = 4 +barrier_health_max = 7 +first_barrier_margin = 0.14 +final_barrier_margin = 0.1 +minimum_barrier_spacing = 0.14 +chase_start_delay = 1.0 +chase_start_offset = 0.16 +chase_speed = 0.145 diff --git a/fishing/carp_catch_profile.tres b/fishing/carp_catch_profile.tres new file mode 100644 index 0000000..bc3f5dc --- /dev/null +++ b/fishing/carp_catch_profile.tres @@ -0,0 +1,16 @@ +[gd_resource type="Resource" load_steps=2 format=3] + +[ext_resource type="Script" path="res://fishing/catch_difficulty_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") +barrier_count_min = 3 +barrier_count_max = 4 +barrier_health_min = 5 +barrier_health_max = 8 +first_barrier_margin = 0.13 +final_barrier_margin = 0.09 +minimum_barrier_spacing = 0.14 +chase_start_delay = 1.1 +chase_start_offset = 0.18 +chase_speed = 0.15 diff --git a/fishing/fishing_context.gd b/fishing/fishing_context.gd new file mode 100644 index 0000000..55b119f --- /dev/null +++ b/fishing/fishing_context.gd @@ -0,0 +1,8 @@ +class_name FishingContext +extends RefCounted + +var location_tags: Array[StringName] = [] +var active_event_tags: Array[StringName] = [] +var active_bait_tags: Array[StringName] = [] +var is_night: bool = false +var is_raining: bool = false diff --git a/fishing/fishing_context.gd.uid b/fishing/fishing_context.gd.uid new file mode 100644 index 0000000..7d7f2b1 --- /dev/null +++ b/fishing/fishing_context.gd.uid @@ -0,0 +1 @@ +uid://chcedxnmhuqb3 diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd index 44c3a1a..699da74 100644 --- a/fishing/fishing_presentation.gd +++ b/fishing/fishing_presentation.gd @@ -2,6 +2,7 @@ class_name FishingPresentation extends Node3D signal cast_completed +signal outcome_completed(outcome: StringName) enum VisualMode { NONE, @@ -304,7 +305,7 @@ func play_outcome(outcome: StringName) -> void: _: _active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18) - _active_tween.finished.connect(cleanup) + _active_tween.finished.connect(_on_outcome_finished.bind(outcome)) func cleanup() -> void: @@ -323,6 +324,11 @@ func cleanup() -> void: set_line_mode(LineMode.HIDDEN) +func _on_outcome_finished(outcome: StringName) -> void: + cleanup() + outcome_completed.emit(outcome) + + func _set_cast_sample( progress: float, start: Vector3, diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 4a46fa8..b63cbd7 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -2,9 +2,17 @@ class_name FishingSpot extends Node3D const FishDataType = preload("res://fish/fish_data.gd") +const FishCatchType = preload("res://fish/fish_catch.gd") +const FishSelectorType = preload("res://fish/fish_selector.gd") +const CollectionLogType = preload("res://collection/collection_log.gd") const CatchControllerType = preload("res://fishing/catch_controller.gd") +const FishingContextType = preload("res://fishing/fishing_context.gd") const FishingPresentationType = preload("res://fishing/fishing_presentation.gd") +const FishInventoryType = preload("res://inventory/fish_inventory.gd") const PlayerType = preload("res://player/player.gd") +const FishableWaterRegionType = preload( + "res://world/fishable_water_region.gd" +) signal status_changed(status: String) signal catch_display_changed( @@ -16,6 +24,12 @@ signal catch_display_changed( active_barrier_index: int, visible: bool, ) +signal showcase_changed( + fish_name: String, + rarity_name: String, + weight_lb: float, + visible: bool, +) enum FishingState { READY, @@ -24,6 +38,7 @@ enum FishingState { WAITING_FOR_BITE, BITE_ACTIVE, FIGHTING, + SHOWING_CATCH, COOLDOWN, } @@ -49,14 +64,24 @@ enum FishingState { @export_range(0.1, 10.0, 0.1) var bite_window_duration: float = 1.5 @export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0 -@export_category("Catch") -@export var catchable_fish: FishDataType +@export_category("Selection") +@export_range(0.0, 10.0, 0.05) var undiscovered_weight_multiplier: float = 1.5 +@export var use_deterministic_selection_seed: bool = false +@export var deterministic_selection_seed: int = 24680 + +@export_category("Context Testing") +@export var context_is_night: bool = false +@export var context_is_raining: bool = false +@export var context_event_tags: Array[StringName] = [] +@export var context_bait_tags: Array[StringName] = [] @onready var _catch_controller: CatchControllerType = %CatchController @onready var _presentation: FishingPresentationType = %FishingPresentation var state: FishingState = FishingState.READY var _local_player: PlayerType +var _local_inventory: FishInventoryType +var _local_collection_log: CollectionLogType var _active_player: PlayerType var _state_time_remaining: float = 0.0 var _cast_charge: float = 0.0 @@ -72,6 +97,14 @@ var _bobber_water_position: Vector3 var _fight_start_position: Vector3 var _cooldown_status: String = "" var _fishable_query_shape: SphereShape3D = SphereShape3D.new() +var _fish_selector: FishSelectorType = FishSelectorType.new() +var _selected_water_region: FishableWaterRegionType +var _selection_context: FishingContextType +var _selected_fish: FishDataType +var _pending_catch: FishCatchType +var _showcase_ready: bool = false +var _put_away_press_armed: bool = false +var _showcase_restore_generation: int = 0 func _ready() -> void: @@ -79,15 +112,52 @@ func _ready() -> void: _catch_controller.caught.connect(_on_catch_completed) _catch_controller.escaped.connect(_on_catch_escaped) _presentation.cast_completed.connect(_on_cast_completed) + _presentation.outcome_completed.connect(_on_outcome_completed) -func setup(local_player: PlayerType) -> void: +func setup( + local_player: PlayerType, + local_inventory: FishInventoryType, + local_collection_log: CollectionLogType, +) -> void: _local_player = local_player + _local_inventory = local_inventory + _local_collection_log = local_collection_log + + +func _exit_tree() -> void: + _showcase_restore_generation += 1 + if ( + state == FishingState.SHOWING_CATCH + and _pending_catch != null + and _local_inventory != null + and is_instance_valid(_local_inventory) + and _local_collection_log != null + and is_instance_valid(_local_collection_log) + ): + _local_inventory.add_catch(_pending_catch) + _local_collection_log.mark_discovered(_pending_catch.fish_id) + _pending_catch = null + if _active_player != null and is_instance_valid(_active_player): + _active_player.end_catch_showcase(Callable(), true) + _active_player.set_movement_enabled(true) + _active_player = null + _selected_water_region = null + _selection_context = null + _selected_fish = null + _showcase_ready = false + _put_away_press_armed = false func _process(delta: float) -> void: if state == FishingState.READY and not Input.is_action_pressed("fish_primary"): _new_cast_press_armed = true + if ( + state == FishingState.SHOWING_CATCH + and _showcase_ready + and not Input.is_action_pressed("fish_primary") + ): + _put_away_press_armed = true match state: FishingState.AIMING_CAST: @@ -110,6 +180,13 @@ func _process(delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: + if ( + event.is_action_pressed("ui_cancel") + and state == FishingState.SHOWING_CATCH + ): + _put_away_catch() + get_viewport().set_input_as_handled() + return if ( event.is_action_pressed("ui_cancel") and _active_player != null @@ -148,6 +225,11 @@ func _unhandled_input(event: InputEvent) -> void: _presentation.set_line_mode( FishingPresentationType.LineMode.TAUT ) + FishingState.SHOWING_CATCH: + if _showcase_ready and _put_away_press_armed: + _put_away_catch() + else: + return _: return get_viewport().set_input_as_handled() @@ -217,10 +299,24 @@ func _on_cast_completed() -> void: if state != FishingState.CASTING: return - _cast_landing_is_fishable = is_target_fishable(_cast_target) + _selected_water_region = get_fishable_water_region(_cast_target) + _cast_landing_is_fishable = _selected_water_region != null if not _cast_landing_is_fishable: _cleanup_attempt("Can't fish there.", &"invalid") return + _selection_context = _build_fishing_context(_selected_water_region) + _fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier + _fish_selector.use_deterministic_test_seed = use_deterministic_selection_seed + _fish_selector.deterministic_test_seed = deterministic_selection_seed + _fish_selector.begin_roll() + _selected_fish = _fish_selector.select_fish( + _selected_water_region.fish_pool, + _selection_context, + _local_collection_log + ) + if _selected_fish == null: + _cleanup_attempt("Nothing is biting here.", &"invalid") + return state = FishingState.WAITING_FOR_BITE _state_time_remaining = wait_time @@ -350,8 +446,8 @@ func _confirm_hook() -> void: if ( state != FishingState.BITE_ACTIVE or _active_player == null - or catchable_fish == null - or catchable_fish.catch_profile == null + or _selected_fish == null + or _selected_fish.catch_profile == null ): _cancel_attempt() return @@ -363,7 +459,7 @@ func _confirm_hook() -> void: _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.begin_reeling() _catch_controller.start_encounter( - catchable_fish.catch_profile, + _selected_fish.catch_profile, _active_player.reel_speed, _active_player.click_power ) @@ -420,12 +516,24 @@ func _on_catch_encounter_updated( func _on_catch_completed() -> void: - if state != FishingState.FIGHTING or _active_player == null or catchable_fish == null: + if ( + state != FishingState.FIGHTING + or _active_player == null + or _selected_fish == null + ): _cancel_attempt() return - _active_player.inventory.add_fish(catchable_fish) - _cleanup_attempt("Caught %s!" % catchable_fish.display_name, &"catch") + _pending_catch = _fish_selector.create_catch(_selected_fish) + if _pending_catch == null or not _pending_catch.is_valid(): + _cancel_attempt() + return + state = FishingState.SHOWING_CATCH + _showcase_ready = false + _put_away_press_armed = false + _catch_controller.reset() + _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) + _presentation.play_outcome(&"catch") func _on_catch_escaped() -> void: @@ -433,11 +541,69 @@ func _on_catch_escaped() -> void: return var fish_name: String = "The fish" - if catchable_fish != null and not catchable_fish.display_name.is_empty(): - fish_name = "The %s" % catchable_fish.display_name + if _selected_fish != null and not _selected_fish.display_name.is_empty(): + fish_name = "The %s" % _selected_fish.display_name _cleanup_attempt("%s got away!" % fish_name, &"escape") +func _on_outcome_completed(outcome: StringName) -> void: + if ( + outcome != &"catch" + or state != FishingState.SHOWING_CATCH + or _active_player == null + or _pending_catch == null + ): + return + _showcase_ready = true + _active_player.begin_catch_showcase(_pending_catch) + showcase_changed.emit( + _pending_catch.fish.display_name, + _pending_catch.fish.get_rarity_name(), + _pending_catch.weight_lb, + true + ) + status_changed.emit("Left click or Escape to put away") + + +func _put_away_catch() -> void: + if ( + state != FishingState.SHOWING_CATCH + or _active_player == null + or _local_inventory == null + or _local_collection_log == null + or _pending_catch == null + ): + return + var caught_name: String = _pending_catch.fish.display_name + _local_inventory.add_catch(_pending_catch) + _local_collection_log.mark_discovered(_pending_catch.fish_id) + _pending_catch = null + _showcase_ready = false + _put_away_press_armed = false + showcase_changed.emit("", "", 0.0, false) + _showcase_restore_generation += 1 + var restore_generation: int = _showcase_restore_generation + _active_player.end_catch_showcase( + _finish_showcase_put_away.bind( + restore_generation, + "Caught %s!" % caught_name + ) + ) + + +func _finish_showcase_put_away( + restore_generation: int, + cooldown_message: String, +) -> void: + if ( + restore_generation != _showcase_restore_generation + or state != FishingState.SHOWING_CATCH + or _active_player == null + ): + return + _cleanup_attempt(cooldown_message) + + func _miss_bite() -> void: if state != FishingState.BITE_ACTIVE: return @@ -448,7 +614,9 @@ func _cleanup_attempt( cooldown_message: String = "", visual_outcome: StringName = &"", ) -> void: + _showcase_restore_generation += 1 if _active_player != null: + _active_player.end_catch_showcase() _active_player.set_movement_enabled(true) _active_player = null _state_time_remaining = 0.0 @@ -462,6 +630,13 @@ func _cleanup_attempt( _withdrawal_input_held = false _bobber_water_position = Vector3.ZERO _fight_start_position = Vector3.ZERO + _selected_water_region = null + _selection_context = null + _selected_fish = null + _pending_catch = null + _showcase_ready = false + _put_away_press_armed = false + showcase_changed.emit("", "", 0.0, false) _catch_controller.reset() if visual_outcome.is_empty(): _presentation.cleanup() @@ -531,6 +706,12 @@ func _calculate_cast_target(distance: float) -> Vector3: func is_target_fishable(target: Vector3) -> bool: + return get_fishable_water_region(target) != null + + +func get_fishable_water_region( + target: Vector3, +) -> FishableWaterRegionType: _fishable_query_shape.radius = fishable_query_radius var query := PhysicsShapeQueryParameters3D.new() query.shape = _fishable_query_shape @@ -543,9 +724,36 @@ func is_target_fishable(target: Vector3) -> bool: query.collide_with_bodies = false var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_shape( query, - 1 + 32 ) - return not results.is_empty() + var selected_region: FishableWaterRegionType + for result: Dictionary in results: + var collider: Object = result.get("collider") + var region: FishableWaterRegionType = collider as FishableWaterRegionType + if region == null or region.fish_pool == null: + continue + if ( + selected_region == null + or region.selection_priority > selected_region.selection_priority + or ( + region.selection_priority == selected_region.selection_priority + and region.get_instance_id() < selected_region.get_instance_id() + ) + ): + selected_region = region + return selected_region + + +func _build_fishing_context( + region: FishableWaterRegionType, +) -> FishingContextType: + var context := FishingContextType.new() + context.location_tags = region.location_tags.duplicate() + context.active_event_tags = context_event_tags.duplicate() + context.active_bait_tags = context_bait_tags.duplicate() + context.is_night = context_is_night + context.is_raining = context_is_raining + return context func find_last_fishable_position(from: Vector3, to: Vector3) -> Vector3: diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn index a2870cd..d9ba7ad 100644 --- a/fishing/fishing_spot.tscn +++ b/fishing/fishing_spot.tscn @@ -1,7 +1,6 @@ -[gd_scene load_steps=12 format=3] +[gd_scene load_steps=11 format=3] [ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"] -[ext_resource type="Resource" path="res://fish/bluegill.tres" id="2_bluegill"] [ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"] [ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"] @@ -41,7 +40,6 @@ size = Vector3(0.72, 0.06, 0.12) [node name="FishingSpot" type="Node3D"] script = ExtResource("1_spot") -catchable_fish = ExtResource("2_bluegill") [node name="CatchController" type="Node" parent="."] unique_name_in_owner = true diff --git a/inventory/fish_inventory.gd b/inventory/fish_inventory.gd index 6fe7456..0c1b964 100644 --- a/inventory/fish_inventory.gd +++ b/inventory/fish_inventory.gd @@ -1,21 +1,34 @@ class_name FishInventory extends Node -const FishDataType = preload("res://fish/fish_data.gd") +const FishCatchType = preload("res://fish/fish_catch.gd") signal contents_changed(fish_id: StringName, count: int) -var _fish_counts: Dictionary[StringName, int] = {} +var _catches: Array[FishCatchType] = [] -func add_fish(fish: FishDataType, amount: int = 1) -> void: - if fish == null or fish.id.is_empty() or amount <= 0: +func add_catch(fish_catch: FishCatchType) -> void: + if fish_catch == null or not fish_catch.is_valid(): return - - var new_count: int = get_count(fish.id) + amount - _fish_counts[fish.id] = new_count - contents_changed.emit(fish.id, new_count) + _catches.append(fish_catch) + contents_changed.emit( + fish_catch.fish_id, + get_count(fish_catch.fish_id) + ) func get_count(fish_id: StringName) -> int: - return _fish_counts.get(fish_id, 0) + return get_catches_by_fish_id(fish_id).size() + + +func get_all_catches() -> Array[FishCatchType]: + return _catches.duplicate() + + +func get_catches_by_fish_id(fish_id: StringName) -> Array[FishCatchType]: + var matching: Array[FishCatchType] = [] + for fish_catch: FishCatchType in _catches: + if fish_catch.fish_id == fish_id: + matching.append(fish_catch) + return matching diff --git a/main/main.gd b/main/main.gd index 9b5b11b..53b0755 100644 --- a/main/main.gd +++ b/main/main.gd @@ -10,5 +10,9 @@ const PlayerType = preload("res://player/player.gd") func _ready() -> void: - _fishing_spot.setup(_player) + _fishing_spot.setup( + _player, + _player.inventory, + _player.collection_log + ) _game_ui.setup(_player.inventory, _fishing_spot) diff --git a/player/player.gd b/player/player.gd index d7a837a..effd739 100644 --- a/player/player.gd +++ b/player/player.gd @@ -2,6 +2,20 @@ class_name Player extends CharacterBody3D const FishInventoryType = preload("res://inventory/fish_inventory.gd") +const CollectionLogType = preload("res://collection/collection_log.gd") +const FishCatchType = preload("res://fish/fish_catch.gd") + +class ShowcaseCameraSnapshot: + extends RefCounted + + var yaw_transform: Transform3D + var yaw_rotation: Vector3 + var pitch_rotation: Vector3 + var spring_length: float + var target_zoom: float + var camera_input_enabled: bool + var camera_dragging: bool + @export_category("Movement") @export var walk_speed: float = 5.0 @@ -18,6 +32,15 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd") @export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32 @export_range(1, 100, 1) var click_power: int = 1 +@export_category("Showcase") +@export_range(0.05, 1.0, 0.01) var showcase_turn_duration: float = 0.28 +@export_range(0.05, 1.0, 0.01) var showcase_restore_duration: float = 0.24 +@export_range(0.05, 1.5, 0.01) var showcase_camera_transition_duration: float = 0.42 +@export_range(-180.0, 180.0, 1.0) var showcase_camera_yaw_offset: float = 180.0 +@export_range(-80.0, 80.0, 1.0) var showcase_camera_pitch: float = -8.0 +@export_range(1.0, 10.0, 0.1) var showcase_camera_zoom_distance: float = 3.6 +@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45 + @export_category("Camera") @export var mouse_sensitivity: float = 0.005 @export var controller_camera_speed: float = 2.5 @@ -35,14 +58,27 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd") @onready var _spring_arm: SpringArm3D = %SpringArm3D @onready var _camera: Camera3D = %Camera3D @onready var inventory: FishInventoryType = %Inventory +@onready var collection_log: CollectionLogType = %CollectionLog @onready var _cast_origin: Marker3D = %CastOrigin @onready var _fishing_rod: Node3D = %FishingRod @onready var _fishing_rod_tip: Marker3D = %FishingRodTip +@onready var _catch_display: Node3D = %CatchDisplay +@onready var _catch_sprite: Sprite3D = %CatchSprite var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false +var _camera_input_enabled: bool = true var _movement_enabled: bool = true var _target_zoom: float = 5.0 +var _showcase_rod_visibility: bool = true +var _showcase_rod_state_stored: bool = false +var _showcase_visual_rotation: Vector3 +var _showcase_visual_rotation_stored: bool = false +var _showcase_turn_tween: Tween +var _showcase_restore_tween: Tween +var _showcase_camera_tween: Tween +var _showcase_camera_snapshot: ShowcaseCameraSnapshot +var _showcase_restore_generation: int = 0 func _ready() -> void: @@ -98,26 +134,40 @@ func _process(delta: float) -> void: if not local_control_enabled: return - if _camera_dragging and not Input.is_action_pressed("camera_drag"): + if ( + _camera_input_enabled + and _camera_dragging + and not Input.is_action_pressed("camera_drag") + ): _camera_dragging = false - var stick: Vector2 = Vector2( - Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), - Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y) - ) - if stick.length() > controller_camera_deadzone: - var adjusted_strength: float = ( - (stick.length() - controller_camera_deadzone) - / (1.0 - controller_camera_deadzone) + if _camera_input_enabled: + var stick: Vector2 = Vector2( + Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), + Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y) ) - _rotate_camera(stick.normalized() * adjusted_strength * controller_camera_speed * delta) + if stick.length() > controller_camera_deadzone: + var adjusted_strength: float = ( + (stick.length() - controller_camera_deadzone) + / (1.0 - controller_camera_deadzone) + ) + _rotate_camera( + stick.normalized() + * adjusted_strength + * controller_camera_speed + * delta + ) - var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta) - _spring_arm.spring_length = lerpf(_spring_arm.spring_length, _target_zoom, zoom_weight) + var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta) + _spring_arm.spring_length = lerpf( + _spring_arm.spring_length, + _target_zoom, + zoom_weight + ) func _unhandled_input(event: InputEvent) -> void: - if not local_control_enabled: + if not local_control_enabled or not _camera_input_enabled: return if event.is_action("camera_drag"): @@ -192,6 +242,278 @@ func get_cast_origin_position() -> Vector3: return _cast_origin.global_position +func begin_catch_showcase(fish_catch: FishCatchType) -> void: + if fish_catch == null or not fish_catch.is_valid(): + return + _kill_showcase_camera_tween() + _kill_showcase_restore_tween() + _capture_showcase_camera_snapshot() + if not _showcase_visual_rotation_stored: + _showcase_visual_rotation = _visuals.rotation + _showcase_visual_rotation_stored = true + var showcase_camera_position: Vector3 = _begin_showcase_camera_transition() + _turn_showcase_toward_position(showcase_camera_position) + if not _showcase_rod_state_stored: + _showcase_rod_visibility = _fishing_rod.visible + _showcase_rod_state_stored = true + _fishing_rod.visible = false + _catch_sprite.texture = fish_catch.fish.display_texture + _catch_display.scale = Vector3.ONE * fish_catch.display_scale + _catch_display.visible = _catch_sprite.texture != null + + +func end_catch_showcase( + restored_callback: Callable = Callable(), + immediate: bool = false, +) -> void: + _kill_showcase_turn_tween() + _kill_showcase_camera_tween() + _kill_showcase_restore_tween() + _catch_display.visible = false + _catch_display.scale = Vector3.ONE + _catch_sprite.texture = null + if _showcase_rod_state_stored: + _fishing_rod.visible = _showcase_rod_visibility + _showcase_rod_visibility = true + _showcase_rod_state_stored = false + if ( + not _showcase_visual_rotation_stored + and _showcase_camera_snapshot == null + ): + if restored_callback.is_valid(): + restored_callback.call() + return + if immediate: + _complete_showcase_restore(restored_callback) + return + + var current_rotation: Vector3 = _visuals.rotation + var restore_target: Vector3 = _showcase_visual_rotation + restore_target.y = ( + current_rotation.y + + wrapf( + _showcase_visual_rotation.y - current_rotation.y, + -PI, + PI + ) + ) + var restore_generation: int = _showcase_restore_generation + _showcase_restore_tween = create_tween() + _showcase_restore_tween.set_parallel(true) + _showcase_restore_tween.set_trans(Tween.TRANS_QUAD) + _showcase_restore_tween.set_ease(Tween.EASE_IN_OUT) + if _showcase_visual_rotation_stored: + _showcase_restore_tween.tween_property( + _visuals, + "rotation", + restore_target, + maxf(showcase_restore_duration, 0.05) + ) + if _showcase_camera_snapshot != null: + var camera_yaw_target: Vector3 = ( + _showcase_camera_snapshot.yaw_rotation + ) + camera_yaw_target.y = ( + _camera_yaw.rotation.y + + wrapf( + camera_yaw_target.y - _camera_yaw.rotation.y, + -PI, + PI + ) + ) + var camera_duration: float = maxf( + showcase_camera_transition_duration, + 0.05 + ) + _showcase_restore_tween.tween_property( + _camera_yaw, + "rotation", + camera_yaw_target, + camera_duration + ) + _showcase_restore_tween.tween_property( + _camera_yaw, + "position", + _showcase_camera_snapshot.yaw_transform.origin, + camera_duration + ) + _showcase_restore_tween.tween_property( + _camera_pitch, + "rotation", + _showcase_camera_snapshot.pitch_rotation, + camera_duration + ) + _showcase_restore_tween.tween_property( + _spring_arm, + "spring_length", + _showcase_camera_snapshot.spring_length, + camera_duration + ) + _showcase_restore_tween.finished.connect( + _on_showcase_restore_finished.bind( + restore_generation, + restored_callback + ) + ) + + +func _turn_showcase_toward_position(target_position: Vector3) -> void: + _kill_showcase_turn_tween() + var camera_direction: Vector3 = ( + target_position - _visuals.global_position + ) + camera_direction.y = 0.0 + if camera_direction.is_zero_approx(): + return + camera_direction = camera_direction.normalized() + var target_world_yaw: float = atan2( + -camera_direction.x, + -camera_direction.z + ) + var target_local_yaw: float = target_world_yaw - global_rotation.y + var current_yaw: float = _visuals.rotation.y + var shortest_target_yaw: float = ( + current_yaw + + wrapf(target_local_yaw - current_yaw, -PI, PI) + ) + _showcase_turn_tween = create_tween() + _showcase_turn_tween.set_trans(Tween.TRANS_QUAD) + _showcase_turn_tween.set_ease(Tween.EASE_IN_OUT) + _showcase_turn_tween.tween_property( + _visuals, + "rotation:y", + shortest_target_yaw, + maxf(showcase_turn_duration, 0.05) + ) + _showcase_turn_tween.finished.connect(_on_showcase_turn_finished) + + +func _kill_showcase_turn_tween() -> void: + if _showcase_turn_tween != null and _showcase_turn_tween.is_valid(): + _showcase_turn_tween.kill() + _showcase_turn_tween = null + + +func _kill_showcase_restore_tween() -> void: + if _showcase_restore_tween != null and _showcase_restore_tween.is_valid(): + _showcase_restore_tween.kill() + _showcase_restore_tween = null + _showcase_restore_generation += 1 + + +func _kill_showcase_camera_tween() -> void: + if _showcase_camera_tween != null and _showcase_camera_tween.is_valid(): + _showcase_camera_tween.kill() + _showcase_camera_tween = null + + +func _on_showcase_turn_finished() -> void: + _showcase_turn_tween = null + + +func _on_showcase_restore_finished( + restore_generation: int, + restored_callback: Callable, +) -> void: + if restore_generation != _showcase_restore_generation: + return + _showcase_restore_tween = null + _complete_showcase_restore(restored_callback) + + +func _complete_showcase_restore( + restored_callback: Callable, +) -> void: + if _showcase_visual_rotation_stored: + _visuals.rotation = _showcase_visual_rotation + _showcase_visual_rotation = Vector3.ZERO + _showcase_visual_rotation_stored = false + if _showcase_camera_snapshot != null: + _camera_yaw.transform = _showcase_camera_snapshot.yaw_transform + _camera_pitch.rotation = _showcase_camera_snapshot.pitch_rotation + _spring_arm.spring_length = _showcase_camera_snapshot.spring_length + _target_zoom = _showcase_camera_snapshot.target_zoom + _camera_input_enabled = ( + _showcase_camera_snapshot.camera_input_enabled + ) + _camera_dragging = _showcase_camera_snapshot.camera_dragging + _showcase_camera_snapshot = null + if restored_callback.is_valid(): + restored_callback.call() + + +func _capture_showcase_camera_snapshot() -> void: + if _showcase_camera_snapshot != null: + return + _showcase_camera_snapshot = ShowcaseCameraSnapshot.new() + _showcase_camera_snapshot.yaw_transform = _camera_yaw.transform + _showcase_camera_snapshot.yaw_rotation = _camera_yaw.rotation + _showcase_camera_snapshot.pitch_rotation = _camera_pitch.rotation + _showcase_camera_snapshot.spring_length = _spring_arm.spring_length + _showcase_camera_snapshot.target_zoom = _target_zoom + _showcase_camera_snapshot.camera_input_enabled = _camera_input_enabled + _showcase_camera_snapshot.camera_dragging = _camera_dragging + _camera_input_enabled = false + _camera_dragging = false + + +func _begin_showcase_camera_transition() -> Vector3: + var target_world_yaw: float = ( + _visuals.global_rotation.y + + deg_to_rad(showcase_camera_yaw_offset) + ) + var target_local_yaw: float = target_world_yaw - global_rotation.y + var shortest_target_yaw: float = ( + _camera_yaw.rotation.y + + wrapf( + target_local_yaw - _camera_yaw.rotation.y, + -PI, + PI + ) + ) + var target_pitch: float = deg_to_rad(showcase_camera_pitch) + var target_position: Vector3 = _camera_yaw.position + target_position.y = showcase_camera_target_height + var target_zoom: float = maxf(showcase_camera_zoom_distance, 0.1) + var camera_duration: float = maxf( + showcase_camera_transition_duration, + 0.05 + ) + _showcase_camera_tween = create_tween() + _showcase_camera_tween.set_parallel(true) + _showcase_camera_tween.set_trans(Tween.TRANS_QUAD) + _showcase_camera_tween.set_ease(Tween.EASE_IN_OUT) + _showcase_camera_tween.tween_property( + _camera_yaw, + "rotation:y", + shortest_target_yaw, + camera_duration + ) + _showcase_camera_tween.tween_property( + _camera_yaw, + "position:y", + target_position.y, + camera_duration + ) + _showcase_camera_tween.tween_property( + _camera_pitch, + "rotation:x", + target_pitch, + camera_duration + ) + _showcase_camera_tween.tween_property( + _spring_arm, + "spring_length", + target_zoom, + camera_duration + ) + + var horizontal_offset: Vector3 = ( + Basis(Vector3.UP, target_world_yaw).z * target_zoom + ) + return global_position + target_position + horizontal_offset + + func get_facing_direction() -> Vector3: var facing: Vector3 = -_visuals.global_basis.z facing.y = 0.0 diff --git a/player/player.tscn b/player/player.tscn index aa59b98..a311247 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=10 format=3] +[gd_scene load_steps=11 format=3] [ext_resource type="Script" path="res://player/player.gd" id="1_script"] [ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"] +[ext_resource type="Script" path="res://collection/collection_log.gd" id="3_collection"] [sub_resource type="CapsuleShape3D" id="PlayerShape"] radius = 0.45 @@ -73,6 +74,26 @@ position = Vector3(0, 0.9, 0) unique_name_in_owner = true script = ExtResource("2_inventory") +[node name="CollectionLog" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("3_collection") + +[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"] +unique_name_in_owner = true +position = Vector3(0, 1.45, -1.15) + +[node name="CatchDisplay" type="Node3D" parent="Visuals/CatchDisplayAnchor"] +unique_name_in_owner = true +visible = false + +[node name="CatchSprite" type="Sprite3D" parent="Visuals/CatchDisplayAnchor/CatchDisplay"] +unique_name_in_owner = true +pixel_size = 0.012 +billboard = 1 +shaded = false +double_sided = true +texture_filter = 0 + [node name="CameraYaw" type="Node3D" parent="."] unique_name_in_owner = true position = Vector3(0, 1.35, 0) diff --git a/ui/game_ui.gd b/ui/game_ui.gd index e430751..d658061 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -6,27 +6,42 @@ const FishingSpotType = preload("res://fishing/fishing_spot.gd") @onready var _status_label: Label = %StatusLabel @onready var _bluegill_count_label: Label = %BluegillCountLabel +@onready var _bass_count_label: Label = %BassCountLabel +@onready var _carp_count_label: Label = %CarpCountLabel @onready var _catch_track: Control = %CatchTrack @onready var _green_catch_progress: ProgressBar = %GreenCatchProgress @onready var _red_chase_progress: ProgressBar = %RedChaseProgress @onready var _barrier_markers: Control = %BarrierMarkers @onready var _barrier_summary: Label = %BarrierSummary @onready var _barrier_health: Label = %BarrierHealth +@onready var _showcase_details: Label = %ShowcaseDetails + +var _showcase_active: bool = false func setup(inventory: FishInventoryType, fishing_spot: FishingSpotType) -> void: inventory.contents_changed.connect(_on_inventory_contents_changed) fishing_spot.status_changed.connect(_on_fishing_status_changed) fishing_spot.catch_display_changed.connect(_on_catch_display_changed) + fishing_spot.showcase_changed.connect(_on_showcase_changed) _update_bluegill_count(inventory.get_count(&"bluegill")) + _bass_count_label.text = "Bass: %d" % inventory.get_count(&"bass") + _carp_count_label.text = "Carp: %d" % inventory.get_count(&"carp") func _on_inventory_contents_changed(fish_id: StringName, count: int) -> void: - if fish_id == &"bluegill": - _update_bluegill_count(count) + match fish_id: + &"bluegill": + _update_bluegill_count(count) + &"bass": + _bass_count_label.text = "Bass: %d" % count + &"carp": + _carp_count_label.text = "Carp: %d" % count func _on_fishing_status_changed(status: String) -> void: + if _showcase_active: + return _status_label.text = status _status_label.visible = not status.is_empty() @@ -132,5 +147,31 @@ func _clear_barrier_markers() -> void: marker.queue_free() +func _on_showcase_changed( + fish_name: String, + rarity_name: String, + weight_lb: float, + visible: bool, +) -> void: + _showcase_active = visible + if not visible: + _status_label.text = "" + _status_label.visible = false + _showcase_details.text = "" + _showcase_details.visible = false + return + _catch_track.visible = false + _barrier_summary.visible = false + _barrier_health.visible = false + _clear_barrier_markers() + _status_label.text = "You caught a %s!" % fish_name + _status_label.visible = true + _showcase_details.text = ( + "%.1f lb • %s\nLeft click or Escape to put away" + % [weight_lb, rarity_name] + ) + _showcase_details.visible = true + + func _update_bluegill_count(count: int) -> void: _bluegill_count_label.text = "Bluegill: %d" % count diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 6d4cc0d..9c6db47 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -21,7 +21,7 @@ script = ExtResource("1_ui") offset_left = 16.0 offset_top = 16.0 offset_right = 176.0 -offset_bottom = 62.0 +offset_bottom = 104.0 [node name="MarginContainer" type="MarginContainer" parent="InventoryPanel"] theme_override_constants/margin_left = 12 @@ -29,10 +29,20 @@ theme_override_constants/margin_top = 8 theme_override_constants/margin_right = 12 theme_override_constants/margin_bottom = 8 -[node name="BluegillCountLabel" type="Label" parent="InventoryPanel/MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="InventoryPanel/MarginContainer"] + +[node name="BluegillCountLabel" type="Label" parent="InventoryPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true text = "Bluegill: 0" +[node name="BassCountLabel" type="Label" parent="InventoryPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +text = "Bass: 0" + +[node name="CarpCountLabel" type="Label" parent="InventoryPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +text = "Carp: 0" + [node name="FishingPanel" type="PanelContainer" parent="."] anchors_preset = 7 anchor_left = 0.5 @@ -61,6 +71,11 @@ visible = false text = "" horizontal_alignment = 1 +[node name="ShowcaseDetails" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +horizontal_alignment = 1 + [node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false diff --git a/world/fishable_water_region.gd b/world/fishable_water_region.gd new file mode 100644 index 0000000..4fb75ac --- /dev/null +++ b/world/fishable_water_region.gd @@ -0,0 +1,8 @@ +class_name FishableWaterRegion +extends Area3D + +const FishPoolType = preload("res://fish/fish_pool.gd") + +@export var location_tags: Array[StringName] = [] +@export var fish_pool: FishPoolType +@export var selection_priority: int = 0 diff --git a/world/fishable_water_region.gd.uid b/world/fishable_water_region.gd.uid new file mode 100644 index 0000000..0fd4f45 --- /dev/null +++ b/world/fishable_water_region.gd.uid @@ -0,0 +1 @@ +uid://b6fp2obu0y2d0 diff --git a/world/test_world.tscn b/world/test_world.tscn index 57259fb..a8968a1 100644 --- a/world/test_world.tscn +++ b/world/test_world.tscn @@ -1,4 +1,7 @@ -[gd_scene load_steps=12 format=3] +[gd_scene load_steps=14 format=3] + +[ext_resource type="Script" path="res://world/fishable_water_region.gd" id="1_water_region"] +[ext_resource type="Resource" path="res://fish/pools/test_water_pool.tres" id="2_fish_pool"] [sub_resource type="BoxShape3D" id="GroundShape"] size = Vector3(30, 1, 30) @@ -77,6 +80,9 @@ position = Vector3(0, 0.2, -29.75) collision_layer = 4 collision_mask = 0 monitoring = false +script = ExtResource("1_water_region") +location_tags = Array[StringName]([&"test_lake"]) +fish_pool = ExtResource("2_fish_pool") [node name="CollisionShape3D" type="CollisionShape3D" parent="FishableWater"] shape = SubResource("FishableWaterShape")