diff --git a/art/source/ui/title/bubbles/bubble1.xcf b/art/source/ui/title/bubbles/bubble1.xcf new file mode 100644 index 0000000..aaf6b4d Binary files /dev/null and b/art/source/ui/title/bubbles/bubble1.xcf differ diff --git a/art/source/ui/title/bubbles/bubble2.xcf b/art/source/ui/title/bubbles/bubble2.xcf new file mode 100644 index 0000000..11018d1 Binary files /dev/null and b/art/source/ui/title/bubbles/bubble2.xcf differ diff --git a/art/source/ui/title/bubbles/bubble3.xcf b/art/source/ui/title/bubbles/bubble3.xcf new file mode 100644 index 0000000..bf77096 Binary files /dev/null and b/art/source/ui/title/bubbles/bubble3.xcf differ diff --git a/audio/music/title/as_in_four_wolves.ogg b/audio/music/title/as_in_four_wolves.ogg new file mode 100644 index 0000000..64b2463 Binary files /dev/null and b/audio/music/title/as_in_four_wolves.ogg differ diff --git a/audio/music/title/as_in_four_wolves.ogg.import b/audio/music/title/as_in_four_wolves.ogg.import new file mode 100644 index 0000000..109abb6 --- /dev/null +++ b/audio/music/title/as_in_four_wolves.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://bgkfluumuen4m" +path="res://.godot/imported/as_in_four_wolves.ogg-3d5f167a45d25a52b56a9dd816196d44.oggvorbisstr" + +[deps] + +source_file="res://audio/music/title/as_in_four_wolves.ogg" +dest_files=["res://.godot/imported/as_in_four_wolves.ogg-3d5f167a45d25a52b56a9dd816196d44.oggvorbisstr"] + +[params] + +loop=true +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/default_bus_layout.tres b/default_bus_layout.tres new file mode 100644 index 0000000..ca38684 --- /dev/null +++ b/default_bus_layout.tres @@ -0,0 +1,15 @@ +[gd_resource type="AudioBusLayout" format=3] + +[resource] +bus/0/name = &"Master" +bus/0/solo = false +bus/0/mute = false +bus/0/bypass_fx = false +bus/0/volume_db = 0.0 +bus/0/send = &"" +bus/1/name = &"Music" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" diff --git a/economy/buyers/main_fishing_shop.tres b/economy/buyers/main_fishing_shop.tres index ee5424a..089d40c 100644 --- a/economy/buyers/main_fishing_shop.tres +++ b/economy/buyers/main_fishing_shop.tres @@ -5,7 +5,7 @@ [resource] script = ExtResource("1_profile") id = &"main_fishing_shop" -display_name = "Main Fishing Shop" +display_name = "main fishing shop" animal_name_singular = "shopkeeper" animal_name_plural = "shopkeepers" payout_multiplier = 1.0 diff --git a/economy/buyers/pelicans.tres b/economy/buyers/pelicans.tres index a9867c3..7dce4ec 100644 --- a/economy/buyers/pelicans.tres +++ b/economy/buyers/pelicans.tres @@ -5,7 +5,7 @@ [resource] script = ExtResource("1_profile") id = &"pelicans" -display_name = "Pelicans" +display_name = "pelicans" animal_name_singular = "pelican" animal_name_plural = "pelicans" payout_multiplier = 0.25 diff --git a/economy/buyers/trading_post.tres b/economy/buyers/trading_post.tres index b1be6ce..1165abb 100644 --- a/economy/buyers/trading_post.tres +++ b/economy/buyers/trading_post.tres @@ -5,7 +5,7 @@ [resource] script = ExtResource("1_profile") id = &"trading_post" -display_name = "Trading Post" +display_name = "trading post" animal_name_singular = "otter" animal_name_plural = "otters" payout_multiplier = 0.85 diff --git a/economy/fish_buyer_profile.gd b/economy/fish_buyer_profile.gd index bb1906f..bca1cea 100644 --- a/economy/fish_buyer_profile.gd +++ b/economy/fish_buyer_profile.gd @@ -39,7 +39,7 @@ func get_sale_message( var buyer_name: String = animal_name_plural if buyer_name.is_empty(): buyer_name = display_name - return "You sold your %s to the %s for $%d." % [ + return "you sold your %s to the %s for $%d." % [ fish_name, buyer_name, payout, diff --git a/economy/fish_sale_result.gd b/economy/fish_sale_result.gd index 0ef4a5b..e133245 100644 --- a/economy/fish_sale_result.gd +++ b/economy/fish_sale_result.gd @@ -8,12 +8,15 @@ enum Status { INVALID_VALUE, INVALID_BUYER, INVALID_OFFER, + INVALID_SELECTION, TRANSACTION_FAILED, } var success: bool = false var status: Status = Status.TRANSACTION_FAILED var catch_id: StringName +var catch_ids: Array[StringName] = [] +var fish_count: int = 0 var fish_name: String = "" var buyer_id: StringName var buyer_display_name: String = "" @@ -32,14 +35,16 @@ func get_message() -> String: Status.SUCCESS: return sale_message Status.NOT_FOUND: - return "Fish no longer exists." + return "fish no longer exists." Status.FAVORITED: - return "Favorited fish cannot be sold." + return "favorited fish cannot be sold." Status.INVALID_VALUE: - return "Invalid sale value." + return "invalid sale value." Status.INVALID_BUYER: - return "Buyer is unavailable." + return "buyer is unavailable." Status.INVALID_OFFER: - return "Invalid buyer offer." + return "invalid buyer offer." + Status.INVALID_SELECTION: + return "the fish selection is invalid." _: - return "Transaction failed." + return "transaction failed." diff --git a/economy/fish_sale_service.gd b/economy/fish_sale_service.gd index c046cc7..b5b81b2 100644 --- a/economy/fish_sale_service.gd +++ b/economy/fish_sale_service.gd @@ -25,24 +25,53 @@ func can_sell( catch_id: StringName, buyer: FishBuyerProfileType, ) -> bool: - return _validate_sale(catch_id, buyer).is_success() + return preview_batch([catch_id], buyer).is_success() + + +func can_sell_batch( + catch_ids: Array[StringName], + buyer: FishBuyerProfileType, +) -> bool: + return preview_batch(catch_ids, buyer).is_success() + + +func preview_batch( + catch_ids: Array[StringName], + buyer: FishBuyerProfileType, +) -> FishSaleResultType: + return _validate_batch(catch_ids, buyer) func sell( catch_id: StringName, buyer: FishBuyerProfileType, ) -> FishSaleResultType: - var result: FishSaleResultType = _validate_sale(catch_id, buyer) + return sell_batch([catch_id], buyer) + + +func sell_batch( + catch_ids: Array[StringName], + buyer: FishBuyerProfileType, +) -> FishSaleResultType: + var result: FishSaleResultType = _validate_batch(catch_ids, buyer) if not result.is_success(): return result - var removed_catch: FishCatchType = _inventory.remove_catch_by_id(catch_id) - if removed_catch == null: + var inventory_snapshot: Array[FishCatchType] = _inventory.get_all_catches() + var next_sequence_snapshot: int = _inventory.get_next_catch_sequence() + var removed_catches: Array[FishCatchType] = ( + _inventory.remove_catches_by_ids(result.catch_ids) + ) + if removed_catches.size() != result.catch_ids.size(): result.success = false - result.status = FishSaleResultType.Status.NOT_FOUND + result.status = FishSaleResultType.Status.TRANSACTION_FAILED return result if not _wallet.credit(result.payout): - _inventory.add_catch(removed_catch) + if not _inventory.replace_all_catches( + inventory_snapshot, + next_sequence_snapshot + ): + push_error("Unable to roll back a failed fish batch sale.") result.success = false result.status = FishSaleResultType.Status.TRANSACTION_FAILED return result @@ -50,27 +79,22 @@ func sell( return result -func _validate_sale( - catch_id: StringName, +func _validate_batch( + catch_ids: Array[StringName], buyer: FishBuyerProfileType, ) -> FishSaleResultType: var result := FishSaleResultType.new() - result.catch_id = catch_id - if ( - catch_id.is_empty() - or _inventory == null - or _wallet == null - ): - result.status = FishSaleResultType.Status.NOT_FOUND + result.catch_ids = catch_ids.duplicate() + result.fish_count = catch_ids.size() + if _inventory == null or _wallet == null: + result.status = FishSaleResultType.Status.TRANSACTION_FAILED + return result + if catch_ids.is_empty(): + result.status = FishSaleResultType.Status.INVALID_SELECTION return result if buyer == null or not buyer.is_valid(): result.status = FishSaleResultType.Status.INVALID_BUYER return result - var fish_catch: FishCatchType = _inventory.get_catch_by_id(catch_id) - if fish_catch == null: - result.status = FishSaleResultType.Status.NOT_FOUND - return result - result.fish_name = fish_catch.fish.display_name result.buyer_id = buyer.id result.buyer_display_name = buyer.display_name result.buyer_animal_name = ( @@ -78,21 +102,55 @@ func _validate_sale( if not buyer.animal_name_plural.is_empty() else buyer.display_name ) - result.base_value = fish_catch.sale_value - result.payout = buyer.get_offer(result.base_value) - if fish_catch.is_favorited: + var seen_ids: Dictionary[StringName, bool] = {} + var contains_favorite: bool = false + const MAX_SAFE_TOTAL: int = 9223372036854775807 + for catch_id: StringName in catch_ids: + if catch_id.is_empty() or seen_ids.has(catch_id): + result.status = FishSaleResultType.Status.INVALID_SELECTION + return result + seen_ids[catch_id] = true + var fish_catch: FishCatchType = _inventory.get_catch_by_id(catch_id) + if fish_catch == null: + result.status = FishSaleResultType.Status.NOT_FOUND + return result + if fish_catch.is_favorited: + contains_favorite = true + if fish_catch.sale_value < 0: + result.status = FishSaleResultType.Status.INVALID_VALUE + return result + var offer: int = buyer.get_offer(fish_catch.sale_value) + if offer < 0: + result.status = FishSaleResultType.Status.INVALID_OFFER + return result + if ( + result.base_value > MAX_SAFE_TOTAL - fish_catch.sale_value + or result.payout > MAX_SAFE_TOTAL - offer + ): + result.status = FishSaleResultType.Status.TRANSACTION_FAILED + return result + result.base_value += fish_catch.sale_value + result.payout += offer + if result.fish_count == 1: + result.catch_id = fish_catch.catch_id + result.fish_name = fish_catch.fish.display_name + if contains_favorite: result.status = FishSaleResultType.Status.FAVORITED - elif fish_catch.sale_value < 0: - result.status = FishSaleResultType.Status.INVALID_VALUE - elif result.payout < 0: - result.status = FishSaleResultType.Status.INVALID_OFFER - elif not _wallet.can_credit(result.payout): + return result + if not _wallet.can_credit(result.payout): result.status = FishSaleResultType.Status.TRANSACTION_FAILED - else: - result.status = FishSaleResultType.Status.SUCCESS - result.success = true + return result + result.status = FishSaleResultType.Status.SUCCESS + result.success = true + if result.fish_count == 1: result.sale_message = buyer.get_sale_message( result.fish_name, result.payout ) + else: + result.sale_message = "you sold %d fish to the %s for $%d." % [ + result.fish_count, + result.buyer_animal_name, + result.payout, + ] return result diff --git a/fish/fish_data.gd b/fish/fish_data.gd index 0207e7b..aa73d2a 100644 --- a/fish/fish_data.gd +++ b/fish/fish_data.gd @@ -98,4 +98,4 @@ func get_sale_value_for_weight(weight_lb: float) -> int: func get_rarity_name() -> String: - return Rarity.keys()[rarity].capitalize() + return Rarity.keys()[rarity].to_lower() diff --git a/fish/pools/test_water_pool.tres b/fish/pools/test_water_pool.tres index 96d61a5..a2634ca 100644 --- a/fish/pools/test_water_pool.tres +++ b/fish/pools/test_water_pool.tres @@ -1,10 +1,11 @@ -[gd_resource type="Resource" load_steps=5 format=3] +[gd_resource type="Resource" load_steps=6 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"] +[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="5_sunfish"] [resource] script = ExtResource("1_pool") -candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp")] +candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish")] diff --git a/fish/species/bass/bass.tres b/fish/species/bass/bass.tres index d9ffd1c..bf7d261 100644 --- a/fish/species/bass/bass.tres +++ b/fish/species/bass/bass.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://fish/species/bass/fish_bass_striped.png" id="4_texture"] [sub_resource type="Resource" id="BassAvailability"] script = ExtResource("3_availability_script") @@ -15,7 +15,7 @@ preferred_bait_weight_multiplier = 1.4 [resource] script = ExtResource("1_fish_data") id = &"bass" -display_name = "Bass" +display_name = "striped bass" rarity = 1 base_catch_weight = 4.0 catch_profile = ExtResource("2_profile") diff --git a/fish/species/bass/fish_bass_striped.png b/fish/species/bass/fish_bass_striped.png new file mode 100644 index 0000000..89b5b5e Binary files /dev/null and b/fish/species/bass/fish_bass_striped.png differ diff --git a/fish/species/bass/fish_bass_striped.png.import b/fish/species/bass/fish_bass_striped.png.import new file mode 100644 index 0000000..4acdf63 --- /dev/null +++ b/fish/species/bass/fish_bass_striped.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dvh6s86asl4ix" +path="res://.godot/imported/fish_bass_striped.png-b8c0def688ff4dd2d32a6dd21fa2339c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/bass/fish_bass_striped.png" +dest_files=["res://.godot/imported/fish_bass_striped.png-b8c0def688ff4dd2d32a6dd21fa2339c.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 diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres index f3eb81d..e766b5f 100644 --- a/fish/species/bluegill/bluegill.tres +++ b/fish/species/bluegill/bluegill.tres @@ -3,12 +3,12 @@ [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"] +[ext_resource type="Texture2D" path="res://fish/species/bluegill/fish_bluegill.png" id="4_texture"] [resource] script = ExtResource("1_fish_data") id = &"bluegill" -display_name = "Bluegill" +display_name = "bluegill" rarity = 0 base_catch_weight = 10.0 catch_profile = ExtResource("2_profile") diff --git a/fish/species/bluegill/fish_bluegill.png b/fish/species/bluegill/fish_bluegill.png new file mode 100644 index 0000000..8569e3c Binary files /dev/null and b/fish/species/bluegill/fish_bluegill.png differ diff --git a/fish/species/bluegill/fish_bluegill.png.import b/fish/species/bluegill/fish_bluegill.png.import new file mode 100644 index 0000000..2e6cf9c --- /dev/null +++ b/fish/species/bluegill/fish_bluegill.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwsl8862rf2vs" +path="res://.godot/imported/fish_bluegill.png-2307d25b88382e057d2259e28f2f89aa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/bluegill/fish_bluegill.png" +dest_files=["res://.godot/imported/fish_bluegill.png-2307d25b88382e057d2259e28f2f89aa.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 diff --git a/fish/species/carp/carp.tres b/fish/species/carp/carp.tres index 571cf69..bb4642d 100644 --- a/fish/species/carp/carp.tres +++ b/fish/species/carp/carp.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://fish/species/carp/fish_carp_common.png" id="4_texture"] [sub_resource type="Resource" id="CarpAvailability"] script = ExtResource("3_availability_script") @@ -15,7 +15,7 @@ preferred_bait_weight_multiplier = 1.5 [resource] script = ExtResource("1_fish_data") id = &"carp" -display_name = "Carp" +display_name = "common carp" rarity = 2 base_catch_weight = 1.5 catch_profile = ExtResource("2_profile") diff --git a/fish/species/carp/fish_carp_common.png b/fish/species/carp/fish_carp_common.png new file mode 100644 index 0000000..50d0b31 Binary files /dev/null and b/fish/species/carp/fish_carp_common.png differ diff --git a/fish/species/carp/fish_carp_common.png.import b/fish/species/carp/fish_carp_common.png.import new file mode 100644 index 0000000..855c974 --- /dev/null +++ b/fish/species/carp/fish_carp_common.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6pa1gqwv1a5s" +path="res://.godot/imported/fish_carp_common.png-4111bce53aeff44874d8e561aa1600a2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/carp/fish_carp_common.png" +dest_files=["res://.godot/imported/fish_carp_common.png-4111bce53aeff44874d8e561aa1600a2.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 diff --git a/fish/species/sunfish/fish_sunfish.png b/fish/species/sunfish/fish_sunfish.png new file mode 100644 index 0000000..138699e Binary files /dev/null and b/fish/species/sunfish/fish_sunfish.png differ diff --git a/fish/species/sunfish/fish_sunfish.png.import b/fish/species/sunfish/fish_sunfish.png.import new file mode 100644 index 0000000..d64d9fe --- /dev/null +++ b/fish/species/sunfish/fish_sunfish.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://x0exmgq6g7yy" +path="res://.godot/imported/fish_sunfish.png-a59a80b2543272e8c6e8ab17d8d3670e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://fish/species/sunfish/fish_sunfish.png" +dest_files=["res://.godot/imported/fish_sunfish.png-a59a80b2543272e8c6e8ab17d8d3670e.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 diff --git a/fish/species/sunfish/sunfish.tres b/fish/species/sunfish/sunfish.tres new file mode 100644 index 0000000..2346b71 --- /dev/null +++ b/fish/species/sunfish/sunfish.tres @@ -0,0 +1,24 @@ +[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/sunfish/fish_sunfish.png" id="4_texture"] + +[resource] +script = ExtResource("1_fish_data") +id = &"sunfish" +display_name = "sunfish" +rarity = 0 +base_catch_weight = 7.0 +catch_profile = ExtResource("2_profile") +availability = ExtResource("3_availability") +weight_min_lb = 0.4 +weight_max_lb = 2.5 +display_scale_min = 0.7 +display_scale_max = 1.1 +display_scale_curve = 1.0 +sell_value_min = 3 +sell_value_max = 3 +sell_value_curve = 1.0 +display_texture = ExtResource("4_texture") diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 3198fb0..6d7cb8e 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -385,14 +385,14 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() return if not has_active_fishing_rod(): - status_changed.emit("Select a fishing rod to cast.") + status_changed.emit("select a fishing rod to cast.") get_viewport().set_input_as_handled() return if ( _is_cooler_full() ): status_changed.emit( - "Cooler full. Sell fish before casting again." + "cooler full. sell fish before casting again." ) get_viewport().set_input_as_handled() return @@ -438,7 +438,7 @@ func _begin_aiming(player: PlayerType) -> void: _cast_charge = 0.0 _cast_target = _calculate_cast_target(minimum_cast_distance) state = FishingState.AIMING_CAST - status_changed.emit("Hold left click to aim • Release to cast") + status_changed.emit("hold left click to aim • release to cast") var target_is_fishable: bool = is_target_fishable(_cast_target) var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target) _presentation.begin_aim( @@ -530,7 +530,7 @@ func _confirm_cast() -> void: return state = FishingState.CASTING - status_changed.emit("Casting...") + status_changed.emit("casting...") _presentation.begin_cast(_cast_target) _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) @@ -542,7 +542,7 @@ func _on_cast_completed() -> void: _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") + _cleanup_attempt("can't fish there.", &"invalid") return _selection_context = _build_fishing_context(_selected_water_region) _fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier @@ -562,7 +562,7 @@ func _on_cast_completed() -> void: _local_collection_log ) if _selected_fish == null: - _cleanup_attempt("Nothing is biting here.", &"invalid") + _cleanup_attempt("nothing is biting here.", &"invalid") return state = FishingState.WAITING_FOR_BITE @@ -580,7 +580,7 @@ func _on_cast_completed() -> void: _cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance ) _presentation.set_line_mode(FishingPresentationType.LineMode.SLACK) - status_changed.emit("Waiting for a bite...") + status_changed.emit("waiting for a bite...") func _update_waiting_for_bite(delta: float) -> void: @@ -698,7 +698,7 @@ func _activate_bite() -> void: _withdrawal_input_held = false _fight_start_position = _bobber_water_position bite_activated.emit() - status_changed.emit("Fish on!") + status_changed.emit("fish on!") _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.begin_reeling() _catch_controller.start_encounter( @@ -800,9 +800,9 @@ func _on_catch_escaped() -> void: if state != FishingState.FIGHTING: return - var fish_name: String = "The fish" + var fish_name: String = "the fish" if _selected_fish != null and not _selected_fish.display_name.is_empty(): - fish_name = "The %s" % _selected_fish.display_name + fish_name = "the %s" % _selected_fish.display_name _cleanup_attempt("%s got away!" % fish_name, &"escape") @@ -822,7 +822,7 @@ func _on_outcome_completed(outcome: StringName) -> void: _pending_catch.weight_lb, true ) - status_changed.emit("Left click or Escape to put away") + status_changed.emit("left click or escape to put away") func _put_away_catch() -> void: @@ -846,7 +846,7 @@ func _put_away_catch() -> void: _active_player.end_catch_showcase( _finish_showcase_put_away.bind( restore_generation, - "Caught %s!" % caught_name + "caught %s!" % caught_name ) ) @@ -926,7 +926,7 @@ func _return_to_ready() -> void: func _cancel_attempt() -> void: - _cleanup_attempt("Fishing cancelled.", &"cancel") + _cleanup_attempt("fishing cancelled.", &"cancel") func _cancel_from_withdrawal() -> void: diff --git a/inventory/fish_inventory.gd b/inventory/fish_inventory.gd index 7304246..89e192d 100644 --- a/inventory/fish_inventory.gd +++ b/inventory/fish_inventory.gd @@ -95,18 +95,40 @@ func contains_catch_id(catch_id: StringName) -> bool: func remove_catch_by_id(catch_id: StringName) -> FishCatchType: if catch_id.is_empty(): return null - for index: int in range(_catches.size()): - var fish_catch: FishCatchType = _catches[index] - if fish_catch == null or fish_catch.catch_id != catch_id: - continue - _catches.remove_at(index) - contents_changed.emit( - fish_catch.fish_id, - get_count(fish_catch.fish_id) - ) - catches_changed.emit() - return fish_catch - return null + var removed: Array[FishCatchType] = remove_catches_by_ids([catch_id]) + return removed.front() if removed.size() == 1 else null + + +func remove_catches_by_ids( + catch_ids: Array[StringName], +) -> Array[FishCatchType]: + var removed: Array[FishCatchType] = [] + if catch_ids.is_empty(): + return removed + var requested_ids: Dictionary[StringName, bool] = {} + for catch_id: StringName in catch_ids: + if catch_id.is_empty() or requested_ids.has(catch_id): + return removed + requested_ids[catch_id] = true + for catch_id: StringName in catch_ids: + var fish_catch: FishCatchType = get_catch_by_id(catch_id) + if fish_catch == null: + removed.clear() + return removed + removed.append(fish_catch) + + var remaining: Array[FishCatchType] = [] + var affected_species: Dictionary[StringName, bool] = {} + for fish_catch: FishCatchType in _catches: + if requested_ids.has(fish_catch.catch_id): + affected_species[fish_catch.fish_id] = true + else: + remaining.append(fish_catch) + _catches = remaining + for fish_id: StringName in affected_species: + contents_changed.emit(fish_id, get_count(fish_id)) + catches_changed.emit() + return removed func set_catch_favorited( diff --git a/items/catalog/basic_fishing_rod.tres b/items/catalog/basic_fishing_rod.tres index 339c419..4530abc 100644 --- a/items/catalog/basic_fishing_rod.tres +++ b/items/catalog/basic_fishing_rod.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1_script") item_id = &"basic_fishing_rod" -display_name = "Basic Fishing Rod" -description = "A dependable starter rod. Select it to cast." +display_name = "basic fishing rod" +description = "a dependable starter rod. select it to cast." category = 0 icon = ExtResource("2_icon") stackable = false diff --git a/items/catalog/coffee.tres b/items/catalog/coffee.tres index 4e7d343..755b671 100644 --- a/items/catalog/coffee.tres +++ b/items/catalog/coffee.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1") item_id = &"coffee" -display_name = "Coffee" -description = "Temporarily increases ground movement speed." +display_name = "coffee" +description = "temporarily increases ground movement speed." category = 3 icon = ExtResource("2") stackable = true diff --git a/items/catalog/cooler_expansion.tres b/items/catalog/cooler_expansion.tres index 28114f6..2e9ed29 100644 --- a/items/catalog/cooler_expansion.tres +++ b/items/catalog/cooler_expansion.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1") item_id = &"cooler_expansion" -display_name = "Cooler Expansion" -description = "Permanently increases the number of fish the Cooler can hold." +display_name = "cooler expansion" +description = "permanently increases the number of fish the cooler can hold." category = 4 icon = ExtResource("2") stackable = false diff --git a/items/catalog/energy_drink.tres b/items/catalog/energy_drink.tres index 6594dbe..813230e 100644 --- a/items/catalog/energy_drink.tres +++ b/items/catalog/energy_drink.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1") item_id = &"energy_drink" -display_name = "Energy Drink" -description = "Temporarily increases reeling progress." +display_name = "energy drink" +description = "temporarily increases reeling progress." category = 3 icon = ExtResource("2") stackable = true diff --git a/items/catalog/fish_finder.tres b/items/catalog/fish_finder.tres index 310b16a..a37fb4a 100644 --- a/items/catalog/fish_finder.tres +++ b/items/catalog/fish_finder.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1") item_id = &"fish_finder" -display_name = "Fish Finder" -description = "Temporarily shortens bite waits and improves rarity weights." +display_name = "fish finder" +description = "temporarily shortens bite waits and improves rarity weights." category = 3 icon = ExtResource("2") stackable = true diff --git a/items/catalog/snack.tres b/items/catalog/snack.tres index f81edca..9d7c89b 100644 --- a/items/catalog/snack.tres +++ b/items/catalog/snack.tres @@ -6,8 +6,8 @@ [resource] script = ExtResource("1") item_id = &"snack" -display_name = "Snack" -description = "Temporarily adds one barrier damage." +display_name = "snack" +description = "temporarily adds one barrier damage." category = 3 icon = ExtResource("2") stackable = true diff --git a/items/item_data.gd b/items/item_data.gd index e487ca5..ebcca91 100644 --- a/items/item_data.gd +++ b/items/item_data.gd @@ -33,4 +33,4 @@ func is_valid() -> bool: func get_category_name() -> String: - return Category.keys()[category].capitalize() + return Category.keys()[category].to_lower() diff --git a/main/main.gd b/main/main.gd index abf93dd..b2bf6c6 100644 --- a/main/main.gd +++ b/main/main.gd @@ -25,10 +25,16 @@ const FishingShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const TITLE_MUSIC_SILENCE_DB: float = -80.0 + @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType @export var main_shop_buyer_profile: FishBuyerProfileType @export var item_catalog: ItemCatalogType +@export_category("Title Music") +@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -14.0 +@export_range(0.0, 5.0, 0.05) var title_music_fade_in_seconds: float = 1.0 +@export_range(0.0, 5.0, 0.05) var title_music_fade_out_seconds: float = 0.75 @onready var _test_world: TestWorldType = $TestWorld @onready var _player: PlayerType = %Player @@ -37,9 +43,14 @@ const FishingShopInteractionType = preload( @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager +@onready var _title_music: AudioStreamPlayer = %TitleMusic var _gameplay_started: bool = false var _shop_interaction: FishingShopInteractionType +var _title_music_tween: Tween +var _title_music_transition_generation: int = 0 +var _title_music_requested: bool = false +var _quit_in_progress: bool = false func _ready() -> void: @@ -144,6 +155,7 @@ func _ready() -> void: _player.hotbar.get_selected_slot(), _player.hotbar.get_selected_item_id() ) + _show_title_music() func _input(event: InputEvent) -> void: @@ -226,20 +238,26 @@ func _set_gameplay_active(active: bool) -> void: func _on_gameplay_requested() -> void: - if _gameplay_started: + if _gameplay_started or _quit_in_progress: return + _fade_out_title_music() _game_ui.get_title_screen().hide() _set_gameplay_active(true) func _on_return_to_title_requested() -> void: + if _quit_in_progress: + return var pause_menu: PauseMenuType = _game_ui.get_pause_menu() pause_menu.close_for_title_transition() _set_gameplay_active(false) _game_ui.get_title_screen().reopen() + _show_title_music() func _on_reset_progress_requested() -> void: + if _quit_in_progress: + return var pause_menu: PauseMenuType = _game_ui.get_pause_menu() if not _save_manager.delete_progression_save(): pause_menu.report_reset_failure() @@ -250,6 +268,7 @@ func _on_reset_progress_requested() -> void: pause_menu.close_for_title_transition() _set_gameplay_active(false) _game_ui.get_title_screen().reopen() + _show_title_music() func _on_water_recovery_starting() -> void: @@ -282,12 +301,116 @@ func _refresh_active_hotbar_item() -> void: func _on_quit_requested() -> void: + if _quit_in_progress: + return + _quit_in_progress = true _settings_manager.save_if_dirty() if _gameplay_started: _save_manager.save_if_dirty() + _fade_out_title_music(_finish_quit) + + +func _show_title_music() -> void: + _title_music_requested = true + var generation: int = _replace_title_music_transition() + if not _title_music.playing: + _title_music.volume_db = TITLE_MUSIC_SILENCE_DB + _title_music.play() + if is_equal_approx(_title_music.volume_db, title_music_volume_db): + return + if title_music_fade_in_seconds <= 0.0: + _title_music.volume_db = title_music_volume_db + return + _title_music_tween = create_tween() + _title_music_tween.set_trans(Tween.TRANS_CUBIC) + _title_music_tween.set_ease(Tween.EASE_OUT) + _title_music_tween.tween_property( + _title_music, + "volume_db", + title_music_volume_db, + title_music_fade_in_seconds + ) + _title_music_tween.finished.connect( + _on_title_music_faded_in.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _fade_out_title_music(on_complete: Callable = Callable()) -> void: + _title_music_requested = false + var generation: int = _replace_title_music_transition() + if not _title_music.playing: + _title_music.volume_db = title_music_volume_db + if on_complete.is_valid(): + on_complete.call() + return + if title_music_fade_out_seconds <= 0.0: + _complete_title_music_fade_out(generation, on_complete) + return + _title_music_tween = create_tween() + _title_music_tween.set_trans(Tween.TRANS_CUBIC) + _title_music_tween.set_ease(Tween.EASE_IN) + _title_music_tween.tween_property( + _title_music, + "volume_db", + TITLE_MUSIC_SILENCE_DB, + title_music_fade_out_seconds + ) + _title_music_tween.finished.connect( + _complete_title_music_fade_out.bind(generation, on_complete), + CONNECT_ONE_SHOT + ) + + +func _replace_title_music_transition() -> int: + _title_music_transition_generation += 1 + if _title_music_tween != null: + _title_music_tween.kill() + _title_music_tween = null + return _title_music_transition_generation + + +func _on_title_music_faded_in(generation: int) -> void: + if ( + generation != _title_music_transition_generation + or not _title_music_requested + ): + return + _title_music_tween = null + _title_music.volume_db = title_music_volume_db + + +func _complete_title_music_fade_out( + generation: int, + on_complete: Callable, +) -> void: + if ( + generation != _title_music_transition_generation + or _title_music_requested + ): + return + _title_music_tween = null + _title_music.stop() + _title_music.volume_db = title_music_volume_db + if on_complete.is_valid(): + on_complete.call() + + +func _finish_quit() -> void: + _title_music.queue_free() + await get_tree().process_frame get_tree().quit() +func _exit_tree() -> void: + if _title_music_tween != null: + _title_music_tween.kill() + _title_music_tween = null + if is_instance_valid(_title_music): + _title_music.stop() + _title_music.stream = null + + func _on_shop_range_changed(in_range: bool) -> void: if not in_range: _game_ui.get_fishing_shop().close_for_range_exit() diff --git a/main/main.tscn b/main/main.tscn index aacf883..d8565ec 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=13 format=3] +[gd_scene load_steps=14 format=3] [ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"] [ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] @@ -12,6 +12,7 @@ [ext_resource type="Script" path="res://settings/player_settings_manager.gd" id="10_settings"] [ext_resource type="Resource" path="res://items/catalog/item_catalog.tres" id="11_items"] [ext_resource type="Resource" path="res://economy/buyers/main_fishing_shop.tres" id="12_main_shop"] +[ext_resource type="AudioStream" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"] [node name="Main" type="Node3D"] script = ExtResource("3_main") @@ -44,5 +45,11 @@ script = ExtResource("9_save") unique_name_in_owner = true script = ExtResource("10_settings") +[node name="TitleMusic" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true +stream = ExtResource("13_title_music") +volume_db = -80.0 +bus = &"Music" + [node name="GameUI" parent="." instance=ExtResource("5_ui")] unique_name_in_owner = true diff --git a/player/player.gd b/player/player.gd index b06e5c5..705cb1c 100644 --- a/player/player.gd +++ b/player/player.gd @@ -56,6 +56,7 @@ class ShowcaseCameraSnapshot: @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_range(0.01, 1.0, 0.01) var catch_presentation_base_scale: float = 0.10 @export_category("Camera") @export var mouse_sensitivity: float = 0.005 @@ -369,7 +370,11 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void: _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.scale = ( + Vector3.ONE + * fish_catch.display_scale + * catch_presentation_base_scale + ) _catch_display.visible = _catch_sprite.texture != null diff --git a/progression/player_item_effects.gd b/progression/player_item_effects.gd index f0ee063..47cdbb5 100644 --- a/progression/player_item_effects.gd +++ b/progression/player_item_effects.gd @@ -117,13 +117,13 @@ func get_rarity_weight_multiplier(rarity: int) -> float: func get_feedback(item_id: StringName) -> String: match item_id: COFFEE_ID: - return "Coffee active: movement speed increased." + return "coffee active: movement speed increased." ENERGY_DRINK_ID: - return "Energy Drink active: reeling speed increased." + return "energy drink active: reeling speed increased." SNACK_ID: - return "Snack active: barrier damage increased." + return "snack active: barrier damage increased." FISH_FINDER_ID: - return "Fish Finder active: bites and uncommon catches improved." + return "fish finder active: bites and uncommon catches improved." _: return "" diff --git a/project.godot b/project.godot index dd2dfd5..1788218 100644 --- a/project.godot +++ b/project.godot @@ -21,6 +21,10 @@ config/icon="res://icon.svg" window/stretch/mode="canvas_items" window/stretch/aspect="expand" +[gui] + +theme/custom="res://ui/game_theme.tres" + [input] move_forward={ diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 41a5428..26c8ed6 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -227,46 +227,46 @@ func inspect_save() -> SaveInspectionType: result.has_primary_file = FileAccess.file_exists(SAVE_PATH) if not result.has_primary_file: result.status = SaveInspectionType.Status.MISSING - result.message = "No save found." + result.message = "no save found." return result var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ) if save_file == null: result.status = SaveInspectionType.Status.IO_ERROR - result.message = "The save could not be read." + result.message = "the save could not be read." return result var json := JSON.new() var parse_error: Error = json.parse(save_file.get_as_text()) save_file.close() if parse_error != OK or typeof(json.data) != TYPE_DICTIONARY: result.status = SaveInspectionType.Status.MALFORMED - result.message = "The save is corrupt and was preserved." + result.message = "the save is corrupt and was preserved." return result var save_data: Dictionary = json.data result.detected_version = _read_integer(save_data.get("save_version"), -1) if result.detected_version > SAVE_VERSION: result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION - result.message = "This save belongs to a newer game version." + result.message = "this save belongs to a newer game version." return result if result.detected_version < 1: result.status = SaveInspectionType.Status.MALFORMED - result.message = "The save version is unsupported." + result.message = "the save version is unsupported." return result if result.detected_version != SAVE_VERSION: save_data = _migrate_save(save_data, result.detected_version) if save_data.is_empty(): result.status = SaveInspectionType.Status.MALFORMED - result.message = "The save version is unsupported." + result.message = "the save version is unsupported." return result var snapshot: LoadSnapshot = _build_load_snapshot(save_data) if snapshot == null: result.status = SaveInspectionType.Status.MALFORMED - result.message = "The save is structurally invalid and was preserved." + result.message = "the save is structurally invalid and was preserved." return result result.status = SaveInspectionType.Status.VALID_SUPPORTED result.catch_count = snapshot.catches.size() result.wallet_balance = snapshot.wallet_balance result.discovered_species_count = snapshot.discovered_ids.size() - result.message = "Save ready." + result.message = "save ready." return result diff --git a/ui/assets/title/bubbles/bubble1.png b/ui/assets/title/bubbles/bubble1.png new file mode 100644 index 0000000..e3003e2 Binary files /dev/null and b/ui/assets/title/bubbles/bubble1.png differ diff --git a/ui/assets/title/bubbles/bubble1.png.import b/ui/assets/title/bubbles/bubble1.png.import new file mode 100644 index 0000000..276aa31 --- /dev/null +++ b/ui/assets/title/bubbles/bubble1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkvilbotou8bp" +path="res://.godot/imported/bubble1.png-c853cfc8f5bc5b89bb69a71c23de669d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble1.png" +dest_files=["res://.godot/imported/bubble1.png-c853cfc8f5bc5b89bb69a71c23de669d.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 diff --git a/ui/assets/title/bubbles/bubble2.png b/ui/assets/title/bubbles/bubble2.png new file mode 100644 index 0000000..a2a4d7d Binary files /dev/null and b/ui/assets/title/bubbles/bubble2.png differ diff --git a/ui/assets/title/bubbles/bubble2.png.import b/ui/assets/title/bubbles/bubble2.png.import new file mode 100644 index 0000000..e77ac0e --- /dev/null +++ b/ui/assets/title/bubbles/bubble2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dh11c1271egs7" +path="res://.godot/imported/bubble2.png-3fe11ab91543c1b1505cfc1100bfd0c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble2.png" +dest_files=["res://.godot/imported/bubble2.png-3fe11ab91543c1b1505cfc1100bfd0c7.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 diff --git a/ui/assets/title/bubbles/bubble3.png b/ui/assets/title/bubbles/bubble3.png new file mode 100644 index 0000000..fdacb6a Binary files /dev/null and b/ui/assets/title/bubbles/bubble3.png differ diff --git a/ui/assets/title/bubbles/bubble3.png.import b/ui/assets/title/bubbles/bubble3.png.import new file mode 100644 index 0000000..0986a8e --- /dev/null +++ b/ui/assets/title/bubbles/bubble3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5praw1x4vukl" +path="res://.godot/imported/bubble3.png-7e3e34f881bccf21cec803168829b93e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble3.png" +dest_files=["res://.godot/imported/bubble3.png-7e3e34f881bccf21cec803168829b93e.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 diff --git a/ui/assets/title/netfishing_logo.png b/ui/assets/title/netfishing_logo.png new file mode 100644 index 0000000..71c8ac8 Binary files /dev/null and b/ui/assets/title/netfishing_logo.png differ diff --git a/ui/assets/title/netfishing_logo.png.import b/ui/assets/title/netfishing_logo.png.import new file mode 100644 index 0000000..2aa351a --- /dev/null +++ b/ui/assets/title/netfishing_logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckqayis1ckb2v" +path="res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/netfishing_logo.png" +dest_files=["res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.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 diff --git a/ui/fish_batch_selection.gd b/ui/fish_batch_selection.gd new file mode 100644 index 0000000..ab147ea --- /dev/null +++ b/ui/fish_batch_selection.gd @@ -0,0 +1,106 @@ +class_name FishBatchSelection +extends RefCounted + +var _selected_ids: Dictionary[StringName, bool] = {} +var _visible_ids: Array[StringName] = [] +var _focused_id: StringName +var _anchor_id: StringName + + +func set_visible_order(visible_ids: Array[StringName]) -> void: + _visible_ids = visible_ids.duplicate() + var visible_set: Dictionary[StringName, bool] = {} + for catch_id: StringName in _visible_ids: + if not catch_id.is_empty(): + visible_set[catch_id] = true + for catch_id: StringName in _selected_ids.keys(): + if not visible_set.has(catch_id): + _selected_ids.erase(catch_id) + if not visible_set.has(_focused_id): + _focused_id = StringName() + if not visible_set.has(_anchor_id): + _anchor_id = StringName() + + +func apply_click( + catch_id: StringName, + ctrl_pressed: bool, + shift_pressed: bool, +) -> void: + if catch_id.is_empty() or not _visible_ids.has(catch_id): + return + if not shift_pressed or _anchor_id.is_empty(): + if ctrl_pressed: + if _selected_ids.has(catch_id): + _selected_ids.erase(catch_id) + else: + _selected_ids[catch_id] = true + else: + _selected_ids.clear() + _selected_ids[catch_id] = true + _focused_id = catch_id + _anchor_id = catch_id + return + + var anchor_index: int = _visible_ids.find(_anchor_id) + var target_index: int = _visible_ids.find(catch_id) + if anchor_index < 0 or target_index < 0: + select_only(catch_id) + return + if not ctrl_pressed: + _selected_ids.clear() + var first_index: int = mini(anchor_index, target_index) + var last_index: int = maxi(anchor_index, target_index) + for index: int in range(first_index, last_index + 1): + _selected_ids[_visible_ids[index]] = true + _focused_id = catch_id + + +func select_only(catch_id: StringName) -> void: + _selected_ids.clear() + if catch_id.is_empty() or not _visible_ids.has(catch_id): + _focused_id = StringName() + _anchor_id = StringName() + return + _selected_ids[catch_id] = true + _focused_id = catch_id + _anchor_id = catch_id + + +func clear() -> void: + _selected_ids.clear() + _focused_id = StringName() + _anchor_id = StringName() + + +func remove_ids(catch_ids: Array[StringName]) -> void: + for catch_id: StringName in catch_ids: + _selected_ids.erase(catch_id) + if _focused_id == catch_id: + _focused_id = StringName() + if _anchor_id == catch_id: + _anchor_id = StringName() + + +func get_selected_ids() -> Array[StringName]: + var selected_in_order: Array[StringName] = [] + for catch_id: StringName in _visible_ids: + if _selected_ids.has(catch_id): + selected_in_order.append(catch_id) + return selected_in_order + + +func is_selected(catch_id: StringName) -> bool: + return _selected_ids.has(catch_id) + + +func get_selected_count() -> int: + return _selected_ids.size() + + +func get_focused_id() -> StringName: + return _focused_id + + +func get_anchor_id() -> StringName: + return _anchor_id diff --git a/ui/fish_batch_selection.gd.uid b/ui/fish_batch_selection.gd.uid new file mode 100644 index 0000000..c3ac218 --- /dev/null +++ b/ui/fish_batch_selection.gd.uid @@ -0,0 +1 @@ +uid://ct2140qw4pdug diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 16774a3..1e7dfdd 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -24,6 +24,9 @@ const PlayerCoolerCapacityType = preload( const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const FishBatchSelectionType = preload( + "res://ui/fish_batch_selection.gd" +) signal menu_visibility_changed(is_open: bool) @@ -42,6 +45,7 @@ enum CloseReason { @onready var _fish_texture: TextureRect = %FishTexture @onready var _fish_name: Label = %FishName @onready var _fish_details: Label = %FishDetails +@onready var _selection_summary: Label = %SelectionSummary @onready var _sell_button: Button = %SellButton @onready var _feedback: Label = %Feedback @onready var _reel_level: Label = %ReelLevel @@ -73,14 +77,16 @@ var _interaction: ShopInteractionType var _bag: PlayerBagType var _item_catalog: ItemCatalogType var _cooler_capacity: PlayerCoolerCapacityType -var _selected_catch_id: StringName -var _confirmation_catch_id: StringName +var _fish_selection := FishBatchSelectionType.new() +var _confirmation_catch_ids: Array[StringName] = [] +var _confirmation_generation: int = -1 var _prior_movement_enabled: bool = true var _prior_camera_enabled: bool = true var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _snapshot_stored: bool = false var _mouse_snapshot_stored: bool = false var _generation: int = 0 +var _selection_input_generation: int = 0 var _transaction_in_progress: bool = false var _closing: bool = false @@ -88,6 +94,7 @@ var _closing: bool = false func _ready() -> void: %CloseButton.pressed.connect(close_shop) _fish_list.item_selected.connect(_on_fish_selected) + _fish_list.item_clicked.connect(_on_fish_clicked) _sell_button.pressed.connect(_open_sale_confirmation) _confirm_sale.pressed.connect(_on_confirm_sale) _cancel_sale.pressed.connect(_close_sale_confirmation) @@ -120,6 +127,7 @@ func setup( _bag = bag _item_catalog = item_catalog _cooler_capacity = cooler_capacity + _fish_selection.clear() if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) if not _wallet.balance_changed.is_connected(_on_wallet_changed): @@ -189,7 +197,7 @@ func close_shop( _generation += 1 _transaction_in_progress = false _close_sale_confirmation() - _selected_catch_id = StringName() + _fish_selection.clear() hide() get_viewport().gui_release_focus() if _fishing_spot != null and is_instance_valid(_fishing_spot): @@ -242,9 +250,9 @@ func _refresh_all() -> void: func _refresh_wallet() -> void: _wallet_label.text = ( - "Wallet: $%d" % _wallet.get_balance() + "wallet: $%d" % _wallet.get_balance() if _wallet != null - else "Wallet: $0" + else "wallet: $0" ) @@ -253,7 +261,7 @@ func _refresh_fish_list() -> void: var catches: Array[FishCatchType] = ( _inventory.get_all_catches() if _inventory != null else [] ) - _sales_title.text = "Cooler %d / %d • Full base value" % [ + _sales_title.text = "cooler %d / %d • full base value" % [ catches.size(), _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, ] @@ -261,11 +269,21 @@ func _refresh_fish_list() -> void: func(a: FishCatchType, b: FishCatchType) -> bool: return a.catch_sequence > b.catch_sequence ) - var selected_index: int = -1 + var visible_ids: Array[StringName] = [] + for fish_catch: FishCatchType in catches: + if fish_catch != null and fish_catch.is_valid(): + visible_ids.append(fish_catch.catch_id) + _fish_selection.set_visible_order(visible_ids) for fish_catch: FishCatchType in catches: if fish_catch == null or not fish_catch.is_valid(): continue - var marker: String = "★ " if fish_catch.is_favorited else "" + var marker: String = "" + if _fish_selection.is_selected(fish_catch.catch_id): + marker += "✓ " + if fish_catch.catch_id == _fish_selection.get_focused_id(): + marker += "◆ " + if fish_catch.is_favorited: + marker += "★ " var index: int = _fish_list.add_item( "%s%s — %.2f lb" % [marker, fish_catch.fish.display_name, fish_catch.weight_lb], @@ -277,16 +295,15 @@ func _refresh_fish_list() -> void: "%s • %s" % [ fish_catch.fish.get_rarity_name(), - "Favorited" if fish_catch.is_favorited else "Available", + "favorited" if fish_catch.is_favorited else "available", ] ) - if fish_catch.catch_id == _selected_catch_id: - selected_index = index _fish_empty.visible = _fish_list.item_count == 0 - if selected_index >= 0: - _fish_list.select(selected_index) - elif not _selected_catch_id.is_empty(): - _selected_catch_id = StringName() + _fish_list.deselect_all() + for index: int in range(_fish_list.item_count): + var catch_id := StringName(str(_fish_list.get_item_metadata(index))) + if _fish_selection.is_selected(catch_id): + _fish_list.select(index, false) func _refresh_selected_fish() -> void: @@ -295,27 +312,72 @@ func _refresh_selected_fish() -> void: fish_catch.fish.display_texture if fish_catch != null else null ) _fish_name.text = ( - fish_catch.fish.display_name if fish_catch != null else "Select a fish" + fish_catch.fish.display_name if fish_catch != null else "select a fish" ) if fish_catch == null: - _fish_details.text = "Choose one individual fish from your Cooler." - _sell_button.disabled = true + _fish_details.text = "choose one individual fish from your cooler." + _update_sale_summary() return - var offer: int = _buyer.get_offer(fish_catch.sale_value) + var individual_preview: FishSaleResultType = ( + _sale_service.preview_batch([fish_catch.catch_id], _buyer) + if _sale_service != null + else null + ) + var offer: int = ( + individual_preview.payout if individual_preview != null else -1 + ) _fish_details.text = ( - "%.2f lb • %s\nBase value: $%d\nMain-shop offer: $%d%s" + "%.2f lb • %s\nbase value: $%d\nmain-shop offer: $%d%s" % [ fish_catch.weight_lb, fish_catch.fish.get_rarity_name(), fish_catch.sale_value, offer, - "\nFavorited fish cannot be sold." + "\nfavorited fish cannot be sold." if fish_catch.is_favorited else "", ] ) + _update_sale_summary() + + +func _update_sale_summary() -> void: + var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() + var selected_count: int = selected_ids.size() + _sell_button.text = ( + "sell fish" + if selected_count <= 1 + else "sell %d fish" % selected_count + ) + if selected_count == 0: + _selection_summary.text = "no fish selected" + _sell_button.disabled = true + return + var preview: FishSaleResultType = ( + _sale_service.preview_batch(selected_ids, _buyer) + if _sale_service != null + else null + ) + _selection_summary.text = ( + "1 fish selected" + if selected_count == 1 + else "%d fish selected" % selected_count + ) + if ( + preview != null + and ( + preview.is_success() + or preview.status == FishSaleResultType.Status.FAVORITED + ) + ): + _selection_summary.text += "\ntotal offer: $%d" % preview.payout + if preview != null and preview.status == FishSaleResultType.Status.FAVORITED: + _selection_summary.text += ( + "\nfavorited fish must be removed from the selection." + ) _sell_button.disabled = ( - fish_catch.is_favorited + preview == null + or not preview.is_success() or _transaction_in_progress or _closing ) @@ -326,7 +388,7 @@ func _refresh_upgrades() -> void: return var reel_level: int = _upgrades.get_reel_speed_level() var reel_cost: int = _upgrades.get_next_reel_speed_cost() - _reel_level.text = "Level %d" % reel_level + _reel_level.text = "level %d" % reel_level _reel_purchase.disabled = ( reel_cost < 0 or _transaction_in_progress @@ -335,7 +397,7 @@ func _refresh_upgrades() -> void: ) if reel_cost < 0: _reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier() - _reel_cost.text = "MAX" + _reel_cost.text = "max" else: _reel_effect.text = ( "%.2f× → %.2f×" @@ -345,10 +407,10 @@ func _refresh_upgrades() -> void: ] ) _reel_cost.text = "$%d" % reel_cost - _reel_purchase.text = "MAX" if reel_cost < 0 else "Purchase" + _reel_purchase.text = "max" if reel_cost < 0 else "purchase" var barrier_level: int = _upgrades.get_barrier_power_level() var barrier_cost: int = _upgrades.get_next_barrier_power_cost() - _barrier_level.text = "Level %d" % barrier_level + _barrier_level.text = "level %d" % barrier_level _barrier_purchase.disabled = ( barrier_cost < 0 or _transaction_in_progress @@ -357,7 +419,7 @@ func _refresh_upgrades() -> void: ) if barrier_cost < 0: _barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage() - _barrier_cost.text = "MAX" + _barrier_cost.text = "max" else: _barrier_effect.text = ( "%d damage → %d damage" @@ -367,7 +429,7 @@ func _refresh_upgrades() -> void: ] ) _barrier_cost.text = "$%d" % barrier_cost - _barrier_purchase.text = "MAX" if barrier_cost < 0 else "Purchase" + _barrier_purchase.text = "max" if barrier_cost < 0 else "purchase" func _refresh_supplies() -> void: @@ -385,7 +447,7 @@ func _refresh_supplies() -> void: button.icon = item.icon button.expand_icon = true button.alignment = HORIZONTAL_ALIGNMENT_LEFT - button.text = "%s\n$%d • Owned %d" % [ + button.text = "%s\n$%d • owned %d" % [ item.display_name, FishingShopStockType.get_price(item_id), _bag.get_quantity(item_id), @@ -409,7 +471,7 @@ func _refresh_cooler_capacity() -> void: var level: int = _cooler_capacity.get_level() var cost: int = _cooler_capacity.get_next_cost() var next_capacity: int = _cooler_capacity.get_next_capacity() - _cooler_level.text = "Level %d" % level + _cooler_level.text = "level %d" % level _cooler_purchase.disabled = ( cost < 0 or _transaction_in_progress @@ -418,46 +480,99 @@ func _refresh_cooler_capacity() -> void: ) if cost < 0: _cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity() - _cooler_cost.text = "MAX" - _cooler_purchase.text = "MAX" + _cooler_cost.text = "max" + _cooler_purchase.text = "max" else: _cooler_effect.text = "%d → %d fish" % [ _cooler_capacity.get_capacity(), next_capacity, ] _cooler_cost.text = "$%d" % cost - _cooler_purchase.text = "Purchase" + _cooler_purchase.text = "purchase" func _on_fish_selected(index: int) -> void: if index < 0 or index >= _fish_list.item_count: return - _selected_catch_id = StringName(str(_fish_list.get_item_metadata(index))) + var input_generation: int = _selection_input_generation + call_deferred( + "_apply_keyboard_fish_selection", + index, + input_generation + ) + + +func _apply_keyboard_fish_selection( + index: int, + input_generation: int, +) -> void: + if ( + input_generation != _selection_input_generation + or index < 0 + or index >= _fish_list.item_count + ): + return + var catch_id := StringName(str(_fish_list.get_item_metadata(index))) + _fish_selection.select_only(catch_id) _feedback.text = "" + _refresh_fish_list() + _refresh_selected_fish() + + +func _on_fish_clicked( + index: int, + _position: Vector2, + mouse_button_index: int, +) -> void: + if ( + mouse_button_index != MOUSE_BUTTON_LEFT + or index < 0 + or index >= _fish_list.item_count + ): + return + _selection_input_generation += 1 + var catch_id := StringName(str(_fish_list.get_item_metadata(index))) + _fish_selection.apply_click( + catch_id, + Input.is_key_pressed(KEY_CTRL), + Input.is_key_pressed(KEY_SHIFT) + ) + _feedback.text = "" + _refresh_fish_list() _refresh_selected_fish() func _open_sale_confirmation() -> void: - var fish_catch: FishCatchType = _get_selected_catch() - if not _is_transaction_context_valid() or fish_catch == null: - _feedback.text = "This fish is no longer available." + var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() + if not _is_transaction_context_valid() or selected_ids.is_empty(): + _feedback.text = "the fish selection is no longer available." _refresh_all() return - if fish_catch.is_favorited: - _feedback.text = "Favorited fish cannot be sold." + var preview: FishSaleResultType = _sale_service.preview_batch( + selected_ids, + _buyer + ) + if not preview.is_success(): + _feedback.text = ( + "favorited fish cannot be sold. " + + "remove them from the selection first." + if preview.status == FishSaleResultType.Status.FAVORITED + else preview.get_message() + ) return - _confirmation_catch_id = fish_catch.catch_id - var offer: int = _buyer.get_offer(fish_catch.sale_value) + _confirmation_catch_ids = selected_ids.duplicate() + _confirmation_generation = _generation _confirmation_text.text = ( - "Sell this %.2f lb %s to the Fishing Shop for $%d?" - % [fish_catch.weight_lb, fish_catch.fish.display_name, offer] + "sell %d fish to the fishing shop for $%d?" + % [preview.fish_count, preview.payout] ) _confirmation.show() _cancel_sale.grab_focus() func _close_sale_confirmation() -> void: - _confirmation_catch_id = StringName() + _confirmation_catch_ids.clear() + _confirmation_generation = -1 _confirmation.hide() @@ -465,18 +580,23 @@ func _on_confirm_sale() -> void: if _transaction_in_progress: return var transaction_generation: int = _generation - var catch_id: StringName = _confirmation_catch_id + var catch_ids: Array[StringName] = _confirmation_catch_ids.duplicate() + var confirmation_generation: int = _confirmation_generation _close_sale_confirmation() if ( - catch_id.is_empty() + catch_ids.is_empty() or not _is_transaction_context_valid() or transaction_generation != _generation + or confirmation_generation != transaction_generation or _buyer.id != MAIN_SHOP_BUYER_ID ): - _feedback.text = "Unable to complete sale." + _feedback.text = "unable to complete sale." return _transaction_in_progress = true - var result: FishSaleResultType = _sale_service.sell(catch_id, _buyer) + var result: FishSaleResultType = _sale_service.sell_batch( + catch_ids, + _buyer + ) _transaction_in_progress = false if ( transaction_generation != _generation @@ -484,8 +604,15 @@ func _on_confirm_sale() -> void: ): return if result.is_success(): - _feedback.text = "Fish sold for $%d." % result.payout - _selected_catch_id = StringName() + _feedback.text = ( + "fish sold for $%d." + if result.fish_count == 1 + else "%d fish sold for $%d." % [ + result.fish_count, + result.payout, + ] + ) + _fish_selection.remove_ids(catch_ids) else: _feedback.text = result.get_message() _refresh_all() @@ -501,14 +628,14 @@ func _purchase_barrier_power() -> void: func _purchase_supply(item_id: StringName) -> void: if _transaction_in_progress or not _is_transaction_context_valid(): - _feedback.text = "Unable to complete purchase." + _feedback.text = "unable to complete purchase." return var price: int = FishingShopStockType.get_price(item_id) if not _bag.can_add_item(item_id, 1): - _feedback.text = "Bag cannot accept this item." + _feedback.text = "bag cannot accept this item." return if not _wallet.can_afford(price): - _feedback.text = "Not enough money." + _feedback.text = "not enough money." return var transaction_generation: int = _generation _transaction_in_progress = true @@ -522,21 +649,21 @@ func _purchase_supply(item_id: StringName) -> void: if transaction_generation != _generation or not visible: return _feedback.text = ( - "Item purchased." if purchased else "Unable to complete purchase." + "item purchased." if purchased else "unable to complete purchase." ) _refresh_all() func _purchase_cooler_capacity() -> void: if _transaction_in_progress or not _is_transaction_context_valid(): - _feedback.text = "Unable to complete purchase." + _feedback.text = "unable to complete purchase." return var cost: int = _cooler_capacity.get_next_cost() if cost < 0: - _feedback.text = "Maximum level reached." + _feedback.text = "maximum level reached." return if not _wallet.can_afford(cost): - _feedback.text = "Not enough money." + _feedback.text = "not enough money." return var transaction_generation: int = _generation _transaction_in_progress = true @@ -545,16 +672,16 @@ func _purchase_cooler_capacity() -> void: if transaction_generation != _generation or not visible: return _feedback.text = ( - "Upgrade purchased." + "upgrade purchased." if purchased - else "Unable to complete purchase." + else "unable to complete purchase." ) _refresh_all() func _purchase_upgrade(is_reel_speed: bool) -> void: if _transaction_in_progress or not _is_transaction_context_valid(): - _feedback.text = "Unable to complete purchase." + _feedback.text = "unable to complete purchase." return var cost: int = ( _upgrades.get_next_reel_speed_cost() @@ -562,10 +689,10 @@ func _purchase_upgrade(is_reel_speed: bool) -> void: else _upgrades.get_next_barrier_power_cost() ) if cost < 0: - _feedback.text = "Maximum level reached." + _feedback.text = "maximum level reached." return if not _wallet.can_afford(cost): - _feedback.text = "Not enough money." + _feedback.text = "not enough money." return var transaction_generation: int = _generation _transaction_in_progress = true @@ -578,17 +705,18 @@ func _purchase_upgrade(is_reel_speed: bool) -> void: if transaction_generation != _generation or not visible: return _feedback.text = ( - "Upgrade purchased." + "upgrade purchased." if purchased - else "Unable to complete purchase." + else "unable to complete purchase." ) _refresh_all() func _get_selected_catch() -> FishCatchType: - if _inventory == null or _selected_catch_id.is_empty(): + var focused_id: StringName = _fish_selection.get_focused_id() + if _inventory == null or focused_id.is_empty(): return null - return _inventory.get_catch_by_id(_selected_catch_id) + return _inventory.get_catch_by_id(focused_id) func _is_transaction_context_valid() -> bool: @@ -607,12 +735,22 @@ func _is_transaction_context_valid() -> bool: func _on_inventory_changed() -> void: _refresh_fish_list() - if ( - not _confirmation_catch_id.is_empty() - and _inventory.get_catch_by_id(_confirmation_catch_id) == null - ): - _close_sale_confirmation() - _feedback.text = "This fish is no longer available." + if not _confirmation_catch_ids.is_empty(): + var preview: FishSaleResultType = _sale_service.preview_batch( + _confirmation_catch_ids, + _buyer + ) + if ( + _confirmation_generation != _generation + or not preview.is_success() + ): + _close_sale_confirmation() + _feedback.text = ( + "favorited fish cannot be sold. " + + "remove them from the selection first." + if preview.status == FishSaleResultType.Status.FAVORITED + else preview.get_message() + ) _refresh_selected_fish() diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 4ecc8b5..11b062b 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -63,7 +63,7 @@ theme_override_constants/separation = 8 [node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Header"] layout_mode = 2 size_flags_horizontal = 3 -text = "Fishing Shop" +text = "fishing shop" theme_override_font_sizes/font_size = 24 theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) @@ -78,12 +78,12 @@ texture_filter = 1 [node name="WalletLabel" type="Label" parent="ShopPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 -text = "Wallet: $0" +text = "wallet: $0" [node name="CloseButton" type="Button" parent="ShopPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 -text = "Close" +text = "close" custom_minimum_size = Vector2(72, 30) [node name="Feedback" type="Label" parent="ShopPanel/Margin/Layout"] @@ -107,7 +107,7 @@ theme_override_constants/separation = 6 [node name="SalesTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales"] unique_name_in_owner = true layout_mode = 2 -text = "Sell individual Cooler fish • Full base value" +text = "sell individual cooler fish • full base value" theme_override_font_sizes/font_size = 17 [node name="SalesBody" type="HSplitContainer" parent="ShopPanel/Margin/Layout/Body/FishSales"] @@ -133,7 +133,7 @@ layout_mode = 2 [node name="FishEmpty" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"] unique_name_in_owner = true layout_mode = 2 -text = "Your Cooler is empty." +text = "your cooler is empty." horizontal_alignment = 1 [node name="FishList" type="ItemList" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"] @@ -142,6 +142,7 @@ layout_mode = 2 size_flags_vertical = 3 fixed_icon_size = Vector2i(48, 36) icon_mode = 1 +select_mode = 1 same_column_width = true [node name="FishDetail" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"] @@ -170,22 +171,30 @@ texture_filter = 1 [node name="FishName" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 -text = "Select a fish" +text = "select a fish" horizontal_alignment = 1 theme_override_font_sizes/font_size = 19 [node name="FishDetails" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 -text = "Choose one individual fish from your Cooler." +text = "choose one individual fish from your cooler." horizontal_alignment = 1 autowrap_mode = 2 +[node name="SelectionSummary" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] +unique_name_in_owner = true +layout_mode = 2 +text = "no fish selected" +horizontal_alignment = 1 +autowrap_mode = 2 +theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) + [node name="SellButton" type="Button" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 disabled = true -text = "Sell Selected" +text = "sell selected" [node name="Upgrades" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] custom_minimum_size = Vector2(270, 0) @@ -194,7 +203,7 @@ theme_override_constants/separation = 8 [node name="UpgradeTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades"] layout_mode = 2 -text = "Fishing Upgrades" +text = "fishing upgrades" theme_override_font_sizes/font_size = 17 [node name="Supplies" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] @@ -204,7 +213,7 @@ theme_override_constants/separation = 6 [node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"] layout_mode = 2 -text = "Supplies" +text = "supplies" theme_override_font_sizes/font_size = 17 [node name="Scroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Supplies"] @@ -246,13 +255,13 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] layout_mode = 2 -text = "Reel Speed" +text = "reel speed" theme_override_font_sizes/font_size = 17 [node name="ReelLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 -text = "Level 0" +text = "level 0" [node name="ReelEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] unique_name_in_owner = true @@ -267,7 +276,7 @@ text = "$50" [node name="ReelPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"] unique_name_in_owner = true layout_mode = 2 -text = "Purchase" +text = "purchase" [node name="BarrierCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"] layout_mode = 2 @@ -297,13 +306,13 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] layout_mode = 2 -text = "Barrier Power" +text = "barrier power" theme_override_font_sizes/font_size = 17 [node name="BarrierLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 -text = "Level 0" +text = "level 0" [node name="BarrierEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] unique_name_in_owner = true @@ -318,7 +327,7 @@ text = "$100" [node name="BarrierPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"] unique_name_in_owner = true layout_mode = 2 -text = "Purchase" +text = "purchase" [node name="CoolerCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"] layout_mode = 2 @@ -348,13 +357,13 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] layout_mode = 2 -text = "Cooler Capacity" +text = "cooler capacity" theme_override_font_sizes/font_size = 17 [node name="CoolerLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 -text = "Level 0" +text = "level 0" [node name="CoolerEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] unique_name_in_owner = true @@ -369,7 +378,7 @@ text = "$75" [node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] unique_name_in_owner = true layout_mode = 2 -text = "Purchase" +text = "purchase" [node name="SaleConfirmation" type="PanelContainer" parent="."] unique_name_in_owner = true @@ -413,10 +422,10 @@ theme_override_constants/separation = 8 [node name="ConfirmSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"] unique_name_in_owner = true layout_mode = 2 -text = "Sell Fish" +text = "sell fish" theme_type_variation = &"DangerButton" [node name="CancelSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"] unique_name_in_owner = true layout_mode = 2 -text = "Cancel" +text = "cancel" diff --git a/ui/fonts/seattle_avenue.otf b/ui/fonts/seattle_avenue.otf new file mode 100644 index 0000000..57f51c1 Binary files /dev/null and b/ui/fonts/seattle_avenue.otf differ diff --git a/ui/fonts/seattle_avenue.otf.import b/ui/fonts/seattle_avenue.otf.import new file mode 100644 index 0000000..1c99fd8 --- /dev/null +++ b/ui/fonts/seattle_avenue.otf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://ddbejmxpa3kw6" +path="res://.godot/imported/seattle_avenue.otf-6438e7427b67d392fd88ff9a0c47fc59.fontdata" + +[deps] + +source_file="res://ui/fonts/seattle_avenue.otf" +dest_files=["res://.godot/imported/seattle_avenue.otf-6438e7427b67d392fd88ff9a0c47fc59.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=3 +subpixel_positioning=4 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/ui/game_theme.tres b/ui/game_theme.tres index a8a88fa..1021bc8 100644 --- a/ui/game_theme.tres +++ b/ui/game_theme.tres @@ -1,4 +1,6 @@ -[gd_resource type="Theme" load_steps=12 format=3] +[gd_resource type="Theme" load_steps=13 format=3] + +[ext_resource type="FontFile" uid="uid://ddbejmxpa3kw6" path="res://ui/fonts/seattle_avenue.otf" id="1_font"] [sub_resource type="StyleBoxFlat" id="Panel"] bg_color = Color(0.075, 0.145, 0.17, 0.96) @@ -143,6 +145,7 @@ corner_radius_bottom_right = 6 corner_radius_bottom_left = 6 [resource] +default_font = ExtResource("1_font") default_font_size = 15 Label/colors/font_color = Color(0.956, 0.972, 0.976, 1) Label/colors/font_shadow_color = Color(0, 0, 0, 0) diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 883d746..0301989 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -129,10 +129,10 @@ func _process(_delta: float) -> void: if remaining <= 0.0: continue var label: String = { - PlayerItemEffectsType.COFFEE_ID: "Coffee", - PlayerItemEffectsType.ENERGY_DRINK_ID: "Energy", - PlayerItemEffectsType.SNACK_ID: "Snack", - PlayerItemEffectsType.FISH_FINDER_ID: "Finder", + PlayerItemEffectsType.COFFEE_ID: "coffee", + PlayerItemEffectsType.ENERGY_DRINK_ID: "energy", + PlayerItemEffectsType.SNACK_ID: "snack", + PlayerItemEffectsType.FISH_FINDER_ID: "finder", }.get(item_id, "") parts.append( "%s %d:%02d" @@ -296,7 +296,7 @@ func _on_catch_display_changed( "%d%% %s" % [roundi(barrier_positions[barrier_index] * 100.0), marker] ) - _barrier_summary.text = "Barriers: %s" % " ".join(barrier_labels) + _barrier_summary.text = "barriers: %s" % " ".join(barrier_labels) if ( active_barrier_index >= 0 @@ -304,14 +304,14 @@ func _on_catch_display_changed( and active_barrier_index < barrier_max_health.size() ): _barrier_health.text = ( - "Barrier: %d / %d" + "barrier: %d / %d" % [ barrier_health[active_barrier_index], barrier_max_health[active_barrier_index], ] ) else: - _barrier_health.text = "Hold left click to reel" + _barrier_health.text = "hold left click to reel" var chase_gap: float = progress - chase_progress if chase_gap <= 0.05: @@ -319,7 +319,7 @@ func _on_catch_display_changed( "%s%s" % [ _barrier_health.text, - "\nRed is closing in!", + "\nred is closing in!", ] ).strip_edges() _refresh_fishing_panel_visibility() @@ -377,11 +377,11 @@ func _on_showcase_changed( _barrier_health.visible = false _clear_barrier_markers() _showcase_details.text = ( - "%.1f lb • %s\nLeft click or Escape to put away" + "%.1f lb • %s\nleft click or escape to put away" % [weight_lb, rarity_name] ) _showcase_details.visible = true - _set_fishing_status("You caught a %s!" % fish_name) + _set_fishing_status("you caught a %s!" % fish_name) func _on_player_menu_visibility_changed(is_open: bool) -> void: diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 16171df..18b7864 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -161,7 +161,7 @@ theme_override_constants/margin_bottom = 7 [node name="Label" type="Label" parent="ShopPrompt/Margin"] layout_mode = 2 -text = "Press E to open Fishing Shop" +text = "press e to open fishing shop" horizontal_alignment = 1 [node name="FishingShop" parent="." instance=ExtResource("8_shop")] diff --git a/ui/hotbar_slot.gd b/ui/hotbar_slot.gd index 1bb4a23..f531312 100644 --- a/ui/hotbar_slot.gd +++ b/ui/hotbar_slot.gd @@ -75,7 +75,7 @@ func refresh() -> void: _quantity_label.text = quantity_text _quantity_label.visible = not quantity_text.is_empty() tooltip_text = ( - item.display_name if item != null else "Empty hotbar slot" + item.display_name if item != null else "empty hotbar slot" ) button_pressed = slot_index == _hotbar.get_selected_slot() diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index 90420c9..eb1a30a 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -161,9 +161,9 @@ func _save_now() -> void: _action_in_progress = true _save_button.disabled = true if _save_manager.save_now(): - _feedback.text = "Game saved." + _feedback.text = "game saved." else: - _feedback.text = "Save failed. Previous save was preserved." + _feedback.text = "save failed. previous save was preserved." _action_in_progress = false call_deferred("_reenable_save_button") @@ -181,7 +181,7 @@ func _open_settings() -> void: func _on_settings_applied() -> void: _root_panel.show() - _feedback.text = "Settings saved." + _feedback.text = "settings saved." %SettingsButton.grab_focus() @@ -193,9 +193,9 @@ func _on_settings_closed() -> void: func _confirm_return_to_title() -> void: _open_confirmation( ConfirmationAction.RETURN_TO_TITLE, - "Return to Title", - "Unsaved progress will be saved first.", - "Save and Return", + "return to title", + "unsaved progress will be saved first.", + "save and return", false ) @@ -203,13 +203,13 @@ func _confirm_return_to_title() -> void: func _confirm_reset_progress() -> void: _open_confirmation( ConfirmationAction.RESET_PROGRESS, - "Reset all progression?", + "reset all progression?", ( - "This permanently deletes your fish, discoveries, " + "this permanently deletes your fish, discoveries, " + "favorites, and wallet balance.\n\n" - + "Your settings will be preserved." + + "your settings will be preserved." ), - "DELETE ALL PROGRESS", + "delete all progress", true ) @@ -226,9 +226,9 @@ func _request_quit() -> void: return _open_confirmation( ConfirmationAction.QUIT_ANYWAY, - "Save failed", - "Some progress or settings could not be saved. Quit anyway?", - "Quit Anyway", + "save failed", + "some progress or settings could not be saved. quit anyway?", + "quit anyway", true ) @@ -276,7 +276,7 @@ func _accept_confirmation() -> void: if _save_manager.save_now(): return_to_title_requested.emit() else: - _feedback.text = "Save failed. Previous save was preserved." + _feedback.text = "save failed. previous save was preserved." _root_panel.show() ConfirmationAction.RESET_PROGRESS: reset_progress_requested.emit() @@ -291,7 +291,7 @@ func report_reset_failure() -> void: _action_in_progress = false _root_panel.show() _confirmation_panel.hide() - _feedback.text = "Reset failed. Your progression was preserved." + _feedback.text = "reset failed. your progression was preserved." %ResumeButton.grab_focus() diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index ad4f22c..e94252a 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -55,45 +55,45 @@ theme_override_constants/separation = 11 [node name="Title" type="Label" parent="RootCenter/RootPanel/Margin/Content"] layout_mode = 2 theme_override_font_sizes/font_size = 30 -text = "Game Menu" +text = "game menu" horizontal_alignment = 1 [node name="ResumeButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 -text = "Return to Game" +text = "return to game" [node name="SaveButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 -text = "Save Now" +text = "save now" [node name="SettingsButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 -text = "Settings" +text = "settings" [node name="ReturnToTitleButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 -text = "Return to Title" +text = "return to title" [node name="ResetProgressButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 theme_type_variation = &"DangerButton" -text = "Reset Progress" +text = "reset progress" [node name="QuitButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 43) layout_mode = 2 -text = "Quit" +text = "quit" [node name="FeedbackLabel" type="Label" parent="RootCenter/RootPanel/Margin/Content"] unique_name_in_owner = true @@ -147,13 +147,13 @@ unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.937, 0.357, 0.384, 1) theme_override_font_sizes/font_size = 24 -text = "Confirm" +text = "confirm" horizontal_alignment = 1 [node name="ConfirmationText" type="Label" parent="ConfirmationCenter/ConfirmationPanel/Margin/Content"] unique_name_in_owner = true layout_mode = 2 -text = "Are you sure?" +text = "are you sure?" horizontal_alignment = 1 autowrap_mode = 2 @@ -166,10 +166,10 @@ alignment = 1 unique_name_in_owner = true custom_minimum_size = Vector2(190, 43) layout_mode = 2 -text = "Confirm" +text = "confirm" [node name="CancelConfirmButton" type="Button" parent="ConfirmationCenter/ConfirmationPanel/Margin/Content/Buttons"] unique_name_in_owner = true custom_minimum_size = Vector2(130, 43) layout_mode = 2 -text = "Cancel" +text = "cancel" diff --git a/ui/player_menu.gd b/ui/player_menu.gd index ceaee4f..79b1fd0 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -22,6 +22,9 @@ const ItemDragSourceType = preload("res://ui/item_drag_source.gd") const PlayerCoolerCapacityType = preload( "res://progression/player_cooler_capacity.gd" ) +const FishBatchSelectionType = preload( + "res://ui/fish_batch_selection.gd" +) signal menu_visibility_changed(is_open: bool) @@ -68,6 +71,7 @@ enum CloseReason { @onready var _detail_texture: TextureRect = %DetailTexture @onready var _detail_name: Label = %DetailName @onready var _detail_data: Label = %DetailData +@onready var _selection_summary: Label = %SelectionSummary @onready var _favorite_button: Button = %FavoriteButton @onready var _sell_button: Button = %SellButton @onready var _sale_unavailable: Label = %SaleUnavailable @@ -94,7 +98,7 @@ var _cooler_capacity: PlayerCoolerCapacityType var _current_section: Section = Section.COOLER var _sort_mode: SortMode = SortMode.CATCH_ORDER var _sort_descending: bool = true -var _selected_catch_id: StringName +var _fish_selection := FishBatchSelectionType.new() var _selected_bag_item_id: StringName var _prior_movement_enabled: bool = true var _prior_camera_input_enabled: bool = true @@ -102,9 +106,10 @@ var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _control_snapshot_stored: bool = false var _mouse_snapshot_stored: bool = false var _menu_generation: int = 0 -var _confirmation_catch_id: StringName +var _confirmation_catch_ids: Array[StringName] = [] var _confirmation_buyer: FishBuyerProfileType var _confirmation_buyer_id: StringName +var _confirmation_generation: int = -1 var _sale_in_progress: bool = false @@ -123,9 +128,9 @@ func _ready() -> void: _sell_button.pressed.connect(_on_sell_pressed) _confirm_sale_button.pressed.connect(_on_confirm_sale_pressed) _cancel_sale_button.pressed.connect(_close_sale_confirmation) - _sort_option.add_item("Catch order", SortMode.CATCH_ORDER) - _sort_option.add_item("Name", SortMode.NAME) - _sort_option.add_item("Rarity", SortMode.RARITY) + _sort_option.add_item("catch order", SortMode.CATCH_ORDER) + _sort_option.add_item("name", SortMode.NAME) + _sort_option.add_item("rarity", SortMode.RARITY) _sort_option.select(SortMode.CATCH_ORDER) _update_sort_direction_text() _show_section(_current_section) @@ -157,6 +162,7 @@ func setup( _hotbar = hotbar _item_catalog = item_catalog _cooler_capacity = cooler_capacity + _fish_selection.clear() if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) if not _collection_log.fish_discovered.is_connected(_on_fish_discovered): @@ -234,6 +240,13 @@ func close_menu( return get_viewport().gui_cancel_drag() _close_sale_confirmation() + if reason in [ + CloseReason.BITE_STARTED, + CloseReason.WATER_RECOVERY, + CloseReason.SESSION_END, + CloseReason.TEARDOWN, + ]: + _fish_selection.clear() var closing_generation: int = _menu_generation visible = false get_viewport().gui_release_focus() @@ -249,6 +262,7 @@ func close_menu( func close_for_water_recovery() -> void: + _fish_selection.clear() close_menu(CloseReason.WATER_RECOVERY, false) @@ -257,13 +271,14 @@ func close_for_game_menu() -> void: func close_for_session_end() -> void: + _fish_selection.clear() close_menu(CloseReason.SESSION_END, false) func _exit_tree() -> void: if visible: close_menu(CloseReason.TEARDOWN, false) - _selected_catch_id = StringName() + _fish_selection.clear() _menu_generation += 1 @@ -340,11 +355,11 @@ func _on_sort_direction_pressed() -> void: func _update_sort_direction_text() -> void: match _sort_mode: SortMode.CATCH_ORDER: - _sort_direction.text = "Newest first" if _sort_descending else "Oldest first" + _sort_direction.text = "newest first" if _sort_descending else "oldest first" SortMode.NAME: - _sort_direction.text = "Z–A" if _sort_descending else "A–Z" + _sort_direction.text = "z–a" if _sort_descending else "a–z" SortMode.RARITY: - _sort_direction.text = "High to low" if _sort_descending else "Low to high" + _sort_direction.text = "high to low" if _sort_descending else "low to high" func _refresh_all() -> void: @@ -386,9 +401,9 @@ func _refresh_bag() -> void: item.get_category_name(), ] card.tooltip_text = ( - "Drag to a hotbar slot." + "drag to a hotbar slot." if item.hotbar_allowed - else "This item cannot be assigned to the hotbar." + else "this item cannot be assigned to the hotbar." ) card.setup(item.item_id, item.display_name, item.icon) card.pressed.connect(_select_bag_item.bind(item.item_id)) @@ -427,19 +442,19 @@ func _update_bag_detail() -> void: _bag_detail_texture.texture = item.icon if item != null else null _bag_detail_name.text = item.display_name if item != null else "" _bag_detail_data.text = ( - "%s\nQuantity: %d\n%s\n%s" + "%s\nquantity: %d\n%s\n%s" % [ item.get_category_name(), quantity, item.description, ( - "Can be assigned to the hotbar." + "can be assigned to the hotbar." if item.hotbar_allowed - else "Cannot be assigned to the hotbar." + else "cannot be assigned to the hotbar." ), ] if item != null - else "Select a Bag item for details." + else "select a bag item for details." ) @@ -470,8 +485,8 @@ func _refresh_economy_summary() -> void: if _inventory != null else 0 ) - _wallet_balance.text = "Wallet: $%d" % balance - _held_value.text = "Held fish base value: $%d" % held_total + _wallet_balance.text = "wallet: $%d" % balance + _held_value.text = "held fish base value: $%d" % held_total _cooler_count.text = "%d / %d" % [ _inventory.get_all_catches().size() if _inventory != null else 0, _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, @@ -493,16 +508,18 @@ func _refresh_inventory() -> void: catches.append(fish_catch) catches.sort_custom(_compare_catches) _inventory_empty.visible = catches.is_empty() + var visible_ids: Array[StringName] = [] + for fish_catch: FishCatchType in catches: + visible_ids.append(fish_catch.catch_id) + _fish_selection.set_visible_order(visible_ids) var selected: FishCatchType if _inventory != null: - selected = _inventory.get_catch(_selected_catch_id) - if selected == null and not catches.is_empty(): - selected = catches.front() - _selected_catch_id = selected.catch_id + selected = _inventory.get_catch(_fish_selection.get_focused_id()) for fish_catch: FishCatchType in catches: _inventory_grid.add_child(_create_inventory_card(fish_catch)) _update_inventory_detail(selected) + _update_sale_summary() _update_sort_direction_text() @@ -527,11 +544,17 @@ func _create_inventory_card(fish_catch: FishCatchType) -> Button: card.theme_type_variation = &"CardButton" card.custom_minimum_size = Vector2(132.0, 118.0) card.toggle_mode = true - card.button_pressed = fish_catch.catch_id == _selected_catch_id - card.pressed.connect(_select_catch.bind(fish_catch.catch_id)) + var is_selected: bool = _fish_selection.is_selected(fish_catch.catch_id) + var is_focused: bool = ( + fish_catch.catch_id == _fish_selection.get_focused_id() + ) + card.button_pressed = is_selected + card.pressed.connect(_on_catch_card_pressed.bind(fish_catch.catch_id)) _apply_inventory_card_styles( card, - UIPalette.get_rarity_color(fish_catch.fish.rarity) + UIPalette.get_rarity_color(fish_catch.fish.rarity), + is_selected, + is_focused ) var content := VBoxContainer.new() @@ -573,34 +596,83 @@ func _create_inventory_card(fish_catch: FishCatchType) -> Button: favorite_marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER favorite_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE card.add_child(favorite_marker) + if is_selected: + var selected_marker := Label.new() + selected_marker.text = "✓" + selected_marker.add_theme_color_override( + "font_color", + UIPalette.SUCCESS + ) + selected_marker.add_theme_font_size_override("font_size", 18) + selected_marker.set_anchors_preset(Control.PRESET_TOP_LEFT) + selected_marker.offset_left = 8.0 + selected_marker.offset_top = 5.0 + selected_marker.offset_right = 30.0 + selected_marker.offset_bottom = 29.0 + selected_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE + card.add_child(selected_marker) + if is_focused: + var focus_marker := Label.new() + focus_marker.text = "◆" + focus_marker.add_theme_color_override( + "font_color", + UIPalette.PRIMARY + ) + focus_marker.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) + focus_marker.offset_left = -26.0 + focus_marker.offset_top = -25.0 + focus_marker.offset_right = -8.0 + focus_marker.offset_bottom = -7.0 + focus_marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + focus_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE + card.add_child(focus_marker) return card func _apply_inventory_card_styles( card: Button, rarity_color: Color, + is_selected: bool, + is_focused: bool, ) -> void: card.add_theme_stylebox_override( "normal", - _create_inventory_card_style(rarity_color, 0) + _create_inventory_card_style( + rarity_color, + 0, + is_selected, + is_focused + ) ) card.add_theme_stylebox_override( "hover", - _create_inventory_card_style(rarity_color, 1) + _create_inventory_card_style( + rarity_color, + 1, + is_selected, + is_focused + ) ) card.add_theme_stylebox_override( "focus", - _create_inventory_card_style(rarity_color, 1) + _create_inventory_card_style( + rarity_color, + 1, + is_selected, + true + ) ) card.add_theme_stylebox_override( "pressed", - _create_inventory_card_style(rarity_color, 2) + _create_inventory_card_style(rarity_color, 2, true, is_focused) ) func _create_inventory_card_style( rarity_color: Color, state_index: int, + is_selected: bool, + is_focused: bool, ) -> StyleBoxFlat: var style := StyleBoxFlat.new() match state_index: @@ -609,9 +681,13 @@ func _create_inventory_card_style( 2: style.bg_color = UIPalette.PRIMARY.darkened(0.48) _: - style.bg_color = UIPalette.ELEVATED_PANEL + style.bg_color = ( + UIPalette.PRIMARY.darkened(0.55) + if is_selected + else UIPalette.ELEVATED_PANEL + ) style.border_color = rarity_color - var border_width: int = 4 if state_index == 2 else 2 + var border_width: int = 4 if state_index == 2 or is_focused else 2 style.set_border_width_all(border_width) style.set_corner_radius_all(8) style.content_margin_left = 7.0 @@ -621,13 +697,18 @@ func _create_inventory_card_style( return style -func _select_catch(catch_id: StringName) -> void: +func _on_catch_card_pressed(catch_id: StringName) -> void: if _inventory == null: return var selected: FishCatchType = _inventory.get_catch(catch_id) if selected == null: return - _selected_catch_id = catch_id + _fish_selection.apply_click( + catch_id, + Input.is_key_pressed(KEY_CTRL), + Input.is_key_pressed(KEY_SHIFT) + ) + _transaction_feedback.text = "" _refresh_inventory() @@ -638,26 +719,28 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: _detail_name.text = "" _detail_data.text = "" _favorite_button.disabled = true - _favorite_button.text = "Favorite" - _sell_button.disabled = true - _sale_unavailable.text = "" - _sale_unavailable.visible = false + _favorite_button.text = "favorite" return _detail_texture.texture = fish_catch.fish.display_texture _detail_texture.visible = fish_catch.fish.display_texture != null _detail_name.text = fish_catch.fish.display_name + var individual_preview: FishSaleResultType = ( + _sale_service.preview_batch([fish_catch.catch_id], _default_buyer) + if _sale_service != null + else null + ) var buyer_offer: int = ( - _default_buyer.get_offer(fish_catch.sale_value) - if _default_buyer != null + individual_preview.payout + if individual_preview != null else -1 ) - var buyer_offer_text: String = "Buyer unavailable" - if buyer_offer >= 0: + var buyer_offer_text: String = "buyer unavailable" + if buyer_offer >= 0 and _default_buyer != null: buyer_offer_text = "%s offer: $%d" % [ _default_buyer.display_name, buyer_offer, ] - _detail_data.text = "%.2f lb\n%s\nBase value: $%d\n%s" % [ + _detail_data.text = "%.2f lb\n%s\nbase value: $%d\n%s" % [ fish_catch.weight_lb, fish_catch.fish.get_rarity_name(), fish_catch.sale_value, @@ -665,35 +748,68 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: ] _favorite_button.disabled = false _favorite_button.text = ( - "Unfavorite" if fish_catch.is_favorited else "Favorite" + "unfavorite" if fish_catch.is_favorited else "favorite" ) - var valid_sale_value: bool = fish_catch.sale_value >= 0 - var valid_buyer: bool = ( - _default_buyer != null - and _default_buyer.is_valid() - and buyer_offer >= 0 + + +func _update_sale_summary() -> void: + var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() + var selected_count: int = selected_ids.size() + _sell_button.text = ( + "sell fish" + if selected_count == 1 + else "sell %d fish" % selected_count ) - _sell_button.disabled = ( - fish_catch.is_favorited - or not valid_sale_value - or not valid_buyer + if selected_count == 0: + _selection_summary.text = "no fish selected" + _sell_button.text = "sell fish" + _sell_button.disabled = true + _sale_unavailable.text = "" + _sale_unavailable.visible = false + return + var preview: FishSaleResultType = ( + _sale_service.preview_batch(selected_ids, _default_buyer) + if _sale_service != null + else null ) - if fish_catch.is_favorited: - _sale_unavailable.text = "Favorited fish cannot be sold." - elif not valid_sale_value: - _sale_unavailable.text = "Invalid sale value." - elif not valid_buyer: - _sale_unavailable.text = "Buyer is unavailable." + var count_text: String = ( + "1 fish selected" + if selected_count == 1 + else "%d fish selected" % selected_count + ) + _selection_summary.text = count_text + if ( + preview != null + and preview.payout >= 0 + and _default_buyer != null + and ( + preview.is_success() + or preview.status == FishSaleResultType.Status.FAVORITED + ) + ): + _selection_summary.text += "\n%s total offer: $%d" % [ + _default_buyer.display_name, + preview.payout, + ] + _sell_button.disabled = preview == null or not preview.is_success() + if preview != null and preview.status == FishSaleResultType.Status.FAVORITED: + _sale_unavailable.text = ( + "favorited fish cannot be sold. " + + "remove them from the selection first." + ) + elif preview != null and not preview.is_success(): + _sale_unavailable.text = preview.get_message() else: _sale_unavailable.text = "" _sale_unavailable.visible = not _sale_unavailable.text.is_empty() func _on_favorite_pressed() -> void: - if _inventory == null or _selected_catch_id.is_empty(): + var focused_id: StringName = _fish_selection.get_focused_id() + if _inventory == null or focused_id.is_empty(): return var fish_catch: FishCatchType = _inventory.get_catch_by_id( - _selected_catch_id + focused_id ) if fish_catch == null: _refresh_inventory() @@ -712,46 +828,40 @@ func _on_favorite_pressed() -> void: func _on_sell_pressed() -> void: + var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() if ( _inventory == null or _sale_service == null or _default_buyer == null - or _selected_catch_id.is_empty() + or selected_ids.is_empty() ): return - var fish_catch: FishCatchType = _inventory.get_catch_by_id( - _selected_catch_id + var preview: FishSaleResultType = _sale_service.preview_batch( + selected_ids, + _default_buyer ) - if fish_catch == null: - _transaction_feedback.text = "Fish no longer exists." + if not preview.is_success(): + _transaction_feedback.text = ( + "favorited fish cannot be sold. " + + "remove them from the selection first." + if preview.status == FishSaleResultType.Status.FAVORITED + else preview.get_message() + ) _refresh_inventory() return - if fish_catch.is_favorited: - _transaction_feedback.text = "Favorited fish cannot be sold." - return - if fish_catch.sale_value < 0: - _transaction_feedback.text = "Invalid sale value." - return - if not _default_buyer.is_valid(): - _transaction_feedback.text = "Buyer is unavailable." - return - var buyer_offer: int = _default_buyer.get_offer( - fish_catch.sale_value - ) - if buyer_offer < 0: - _transaction_feedback.text = "Invalid buyer offer." - return - _confirmation_catch_id = fish_catch.catch_id + _confirmation_catch_ids = selected_ids.duplicate() _confirmation_buyer = _default_buyer _confirmation_buyer_id = _default_buyer.id + _confirmation_generation = _menu_generation + var fish_label: String = "fish" _confirmation_message.text = ( - "Sell this %.2f lb %s to the %s for $%d?\nBase value: $%d" + "sell %d %s to the %s for $%d?\ncombined base value: $%d" % [ - fish_catch.weight_lb, - fish_catch.fish.display_name, + preview.fish_count, + fish_label, _get_buyer_display_group(_default_buyer), - buyer_offer, - fish_catch.sale_value, + preview.payout, + preview.base_value, ] ) _sale_confirmation.visible = true @@ -763,31 +873,39 @@ func _on_confirm_sale_pressed() -> void: if ( _sale_in_progress or _sale_service == null - or _confirmation_catch_id.is_empty() + or _confirmation_catch_ids.is_empty() or _confirmation_buyer == null or _confirmation_buyer.id != _confirmation_buyer_id + or _confirmation_generation != _menu_generation ): return _sale_in_progress = true - var requested_catch_id: StringName = _confirmation_catch_id + var requested_catch_ids: Array[StringName] = ( + _confirmation_catch_ids.duplicate() + ) + var transaction_generation: int = _menu_generation + var transaction_buyer: FishBuyerProfileType = _confirmation_buyer _confirm_sale_button.disabled = true - var result: FishSaleResultType = _sale_service.sell( - requested_catch_id, - _confirmation_buyer + var result: FishSaleResultType = _sale_service.sell_batch( + requested_catch_ids, + transaction_buyer ) _close_sale_confirmation() - _transaction_feedback.text = result.get_message() _sale_in_progress = false - if result.is_success() and _selected_catch_id == requested_catch_id: - _selected_catch_id = StringName() + if transaction_generation != _menu_generation or not visible: + return + _transaction_feedback.text = result.get_message() + if result.is_success(): + _fish_selection.remove_ids(requested_catch_ids) _refresh_all() _inventory_tab.grab_focus() func _close_sale_confirmation() -> void: - _confirmation_catch_id = StringName() + _confirmation_catch_ids.clear() _confirmation_buyer = null _confirmation_buyer_id = StringName() + _confirmation_generation = -1 _sale_confirmation.visible = false _confirmation_message.text = "" _confirm_sale_button.disabled = false @@ -796,25 +914,35 @@ func _close_sale_confirmation() -> void: func _revalidate_confirmation() -> void: if not _sale_confirmation.visible: return - var fish_catch: FishCatchType - if _inventory != null: - fish_catch = _inventory.get_catch_by_id(_confirmation_catch_id) - if fish_catch == null: + if ( + _confirmation_generation != _menu_generation + or _confirmation_catch_ids.is_empty() + or _sale_service == null + or _confirmation_buyer == null + or _confirmation_buyer.id != _confirmation_buyer_id + ): _close_sale_confirmation() - _transaction_feedback.text = "Fish no longer exists." - elif fish_catch.is_favorited: - _close_sale_confirmation() - _transaction_feedback.text = "Favorited fish cannot be sold." - elif fish_catch.sale_value < 0: - _close_sale_confirmation() - _transaction_feedback.text = "Invalid sale value." - elif ( + _transaction_feedback.text = "sale selection is no longer available." + return + var preview: FishSaleResultType = _sale_service.preview_batch( + _confirmation_catch_ids, + _confirmation_buyer + ) + if ( + not preview.is_success() + or ( _confirmation_buyer == null or _confirmation_buyer.id != _confirmation_buyer_id or not _confirmation_buyer.is_valid() + ) ): _close_sale_confirmation() - _transaction_feedback.text = "Buyer is unavailable." + _transaction_feedback.text = ( + "favorited fish cannot be sold. " + + "remove them from the selection first." + if preview.status == FishSaleResultType.Status.FAVORITED + else preview.get_message() + ) func _get_buyer_display_group( @@ -840,9 +968,9 @@ func _refresh_logbook() -> void: valid_species.append(fish) _logbook_empty.visible = valid_species.is_empty() _logbook_empty.text = ( - "No fish catalog configured." + "no fish catalog configured." if valid_species.is_empty() - else "No species discovered yet." + else "no species discovered yet." ) if not valid_species.is_empty() and _collection_log != null: var any_discovered: bool = false @@ -881,12 +1009,12 @@ func _create_logbook_card(fish: FishDataType) -> Control: var owned_count: int = ( _inventory.get_count(fish.id) if _inventory != null else 0 ) - details.text = "%s\nOwned: %d" % [ + details.text = "%s\nowned: %d" % [ fish.get_rarity_name(), owned_count, ] else: - details.text = "Undiscovered" + details.text = "undiscovered" details.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER content.add_child(details) return card @@ -904,7 +1032,7 @@ func _create_texture_frame( texture_frame.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST texture_frame.mouse_filter = Control.MOUSE_FILTER_IGNORE if texture == null: - texture_frame.tooltip_text = "Display texture unavailable" + texture_frame.tooltip_text = "display texture unavailable" return texture_frame diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index 1fe08dc..3dccbbe 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -56,39 +56,39 @@ layout_mode = 2 size_flags_horizontal = 3 theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) theme_override_font_sizes/font_size = 22 -text = "Player Menu" +text = "player menu" [node name="WalletBalance" type="Label" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 -text = "Wallet: $0" +text = "wallet: $0" theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) [node name="InventoryTab" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 toggle_mode = true -text = "Cooler" +text = "cooler" custom_minimum_size = Vector2(88, 30) [node name="BagTab" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 toggle_mode = true -text = "Bag" +text = "bag" custom_minimum_size = Vector2(76, 30) [node name="LogbookTab" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 toggle_mode = true -text = "Logbook" +text = "logbook" custom_minimum_size = Vector2(88, 30) [node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 -text = "Close" +text = "close" custom_minimum_size = Vector2(68, 30) [node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"] @@ -123,7 +123,7 @@ theme_override_constants/separation = 6 [node name="SortLabel" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] layout_mode = 2 -text = "Sort:" +text = "sort:" [node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] unique_name_in_owner = true @@ -134,7 +134,7 @@ custom_minimum_size = Vector2(130, 30) unique_name_in_owner = true layout_mode = 2 custom_minimum_size = Vector2(112, 30) -text = "Newest first" +text = "newest first" [node name="SortSpacer" type="Control" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] layout_mode = 2 @@ -149,7 +149,7 @@ theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) [node name="HeldValue" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] unique_name_in_owner = true layout_mode = 2 -text = "Held fish base value: $0" +text = "held fish base value: $0" theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) [node name="InventoryBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"] @@ -176,7 +176,7 @@ theme_override_constants/separation = 6 [node name="InventoryEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] unique_name_in_owner = true layout_mode = 2 -text = "Your Cooler is empty." +text = "your cooler is empty." horizontal_alignment = 1 [node name="InventoryScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] @@ -228,17 +228,25 @@ layout_mode = 2 autowrap_mode = 2 horizontal_alignment = 1 +[node name="SelectionSummary" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] +unique_name_in_owner = true +layout_mode = 2 +text = "no fish selected" +autowrap_mode = 2 +horizontal_alignment = 1 +theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) + [node name="FavoriteButton" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 disabled = true -text = "Favorite" +text = "favorite" [node name="SellButton" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 disabled = true -text = "Sell" +text = "sell" theme_type_variation = &"DangerButton" [node name="SaleUnavailable" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] @@ -262,7 +270,7 @@ theme_override_constants/separation = 7 [node name="BagHint" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection"] layout_mode = 2 -text = "Drag hotbar-compatible items onto a slot below. Right-click a slot to clear it." +text = "drag hotbar-compatible items onto a slot below. right-click a slot to clear it." horizontal_alignment = 1 [node name="BagBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/BagSection"] @@ -289,7 +297,7 @@ theme_override_constants/separation = 6 [node name="BagEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"] unique_name_in_owner = true layout_mode = 2 -text = "Your Bag is empty." +text = "your bag is empty." horizontal_alignment = 1 [node name="BagScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagList/Margin/Stack"] @@ -337,7 +345,7 @@ horizontal_alignment = 1 [node name="BagDetailData" type="Label" parent="MenuPanel/Margin/Layout/Content/BagSection/BagBody/BagDetail/Margin/Stack"] unique_name_in_owner = true layout_mode = 2 -text = "Select a Bag item for details." +text = "select a bag item for details." horizontal_alignment = 1 autowrap_mode = 2 @@ -355,7 +363,7 @@ theme_override_constants/separation = 7 [node name="LogbookEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/LogbookSection"] unique_name_in_owner = true layout_mode = 2 -text = "No species discovered yet." +text = "no species discovered yet." horizontal_alignment = 1 [node name="LogbookScroll" type="ScrollContainer" parent="MenuPanel/Margin/Layout/Content/LogbookSection"] @@ -412,12 +420,12 @@ alignment = 1 [node name="ConfirmSaleButton" type="Button" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 -text = "Confirm" +text = "confirm" theme_type_variation = &"DangerButton" custom_minimum_size = Vector2(120, 38) [node name="CancelSaleButton" type="Button" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 -text = "Cancel" +text = "cancel" custom_minimum_size = Vector2(120, 38) diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index a6b1553..d31cb19 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -47,7 +47,7 @@ func close_panel() -> void: func _apply_settings() -> void: if _settings_manager == null: - _feedback.text = "Settings are unavailable." + _feedback.text = "settings are unavailable." return var edited := PlayerSettings.new() edited.auto_click_enabled = _auto_click_toggle.button_pressed @@ -59,7 +59,7 @@ func _apply_settings() -> void: hide() applied.emit() else: - _feedback.text = "Failed to save settings." + _feedback.text = "failed to save settings." func _load_controls() -> void: diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index a2f75a0..cfdef8e 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -23,17 +23,17 @@ theme_override_constants/separation = 12 [node name="Heading" type="Label" parent="Margin/Content"] layout_mode = 2 theme_override_font_sizes/font_size = 26 -text = "Settings" +text = "settings" horizontal_alignment = 1 [node name="AutoClickToggle" type="CheckButton" parent="Margin/Content"] unique_name_in_owner = true layout_mode = 2 -text = "Accessibility auto-click" +text = "accessibility auto-click" [node name="AutoClickHelp" type="Label" parent="Margin/Content"] layout_mode = 2 -text = "Lower intervals click barriers faster while held." +text = "lower intervals click barriers faster while held." [node name="AutoClickRow" type="HBoxContainer" parent="Margin/Content"] layout_mode = 2 @@ -42,7 +42,7 @@ theme_override_constants/separation = 12 [node name="Label" type="Label" parent="Margin/Content/AutoClickRow"] custom_minimum_size = Vector2(190, 0) layout_mode = 2 -text = "Auto-click interval" +text = "auto-click interval" [node name="AutoClickInterval" type="HSlider" parent="Margin/Content/AutoClickRow"] unique_name_in_owner = true @@ -66,7 +66,7 @@ theme_override_constants/separation = 12 [node name="Label" type="Label" parent="Margin/Content/MouseRow"] custom_minimum_size = Vector2(190, 0) layout_mode = 2 -text = "Mouse camera sensitivity" +text = "mouse camera sensitivity" [node name="MouseSensitivity" type="HSlider" parent="Margin/Content/MouseRow"] unique_name_in_owner = true @@ -90,7 +90,7 @@ theme_override_constants/separation = 12 [node name="Label" type="Label" parent="Margin/Content/ControllerRow"] custom_minimum_size = Vector2(190, 0) layout_mode = 2 -text = "Controller camera sensitivity" +text = "controller camera sensitivity" [node name="ControllerSensitivity" type="HSlider" parent="Margin/Content/ControllerRow"] unique_name_in_owner = true @@ -110,7 +110,7 @@ text = "2.5" [node name="InvertYToggle" type="CheckButton" parent="Margin/Content"] unique_name_in_owner = true layout_mode = 2 -text = "Invert vertical camera" +text = "invert vertical camera" [node name="SettingsFeedback" type="Label" parent="Margin/Content"] unique_name_in_owner = true @@ -127,10 +127,10 @@ alignment = 2 unique_name_in_owner = true custom_minimum_size = Vector2(120, 42) layout_mode = 2 -text = "Apply" +text = "apply" [node name="CancelSettingsButton" type="Button" parent="Margin/Content/Buttons"] unique_name_in_owner = true custom_minimum_size = Vector2(120, 42) layout_mode = 2 -text = "Back" +text = "back" diff --git a/ui/title_logo_underwater.gdshader b/ui/title_logo_underwater.gdshader new file mode 100644 index 0000000..cdcfce8 --- /dev/null +++ b/ui/title_logo_underwater.gdshader @@ -0,0 +1,48 @@ +shader_type canvas_item; + +uniform float horizontal_displacement_pixels : hint_range(0.0, 2.0, 0.25) = 1.5; +uniform float wave_speed : hint_range(0.0, 2.0, 0.05) = 0.55; +uniform float wave_scale : hint_range(0.0, 0.20, 0.005) = 0.055; +uniform vec3 underwater_tint : source_color = vec3(0.78, 0.96, 1.08); +uniform float tint_strength : hint_range(0.0, 0.35, 0.01) = 0.18; +uniform float highlight_strength : hint_range(0.0, 0.15, 0.01) = 0.06; +uniform float bob_amount_pixels : hint_range(0.0, 4.0, 1.0) = 4.0; +uniform float bob_speed : hint_range(0.0, 2.0, 0.05) = 0.65; + +void vertex() { + VERTEX.y += round( + sin(TIME * bob_speed) * bob_amount_pixels + ); +} + +void fragment() { + float stepped_time = floor(TIME * 12.0) / 12.0; + float source_row = floor(UV.y / max(TEXTURE_PIXEL_SIZE.y, 0.000001)); + float wave = sin( + source_row * wave_scale + stepped_time * wave_speed + ); + float pixel_offset = clamp( + round(wave * horizontal_displacement_pixels), + -2.0, + 2.0 + ); + vec2 sample_uv = UV + vec2(pixel_offset * TEXTURE_PIXEL_SIZE.x, 0.0); + vec4 source = texture(TEXTURE, sample_uv); + + vec3 treated_color = mix( + source.rgb, + source.rgb * underwater_tint, + tint_strength + ); + float highlight_wave = sin( + UV.y * 26.0 - stepped_time * wave_speed * 0.8 + ) * 0.5 + 0.5; + float highlight_level = floor(highlight_wave * 3.0) / 3.0; + treated_color += ( + vec3(0.12, 0.30, 0.34) + * highlight_level + * highlight_strength + * source.a + ); + COLOR = vec4(treated_color, source.a); +} diff --git a/ui/title_logo_underwater.gdshader.uid b/ui/title_logo_underwater.gdshader.uid new file mode 100644 index 0000000..77c6f67 --- /dev/null +++ b/ui/title_logo_underwater.gdshader.uid @@ -0,0 +1 @@ +uid://bu14y18kegufh diff --git a/ui/title_screen.gd b/ui/title_screen.gd index be83160..2c70655 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -7,6 +7,52 @@ const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") +const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ + preload("res://fish/species/bass/fish_bass_striped.png"), + preload("res://fish/species/bluegill/fish_bluegill.png"), + preload("res://fish/species/carp/fish_carp_common.png"), + preload("res://fish/species/sunfish/fish_sunfish.png"), +] +const DECORATIVE_BUBBLE_TEXTURES: Array[Texture2D] = [ + preload("res://ui/assets/title/bubbles/bubble1.png"), + preload("res://ui/assets/title/bubbles/bubble2.png"), + preload("res://ui/assets/title/bubbles/bubble3.png"), +] + +const FIRST_FISH_DELAY_MIN: float = 2.0 +const FIRST_FISH_DELAY_MAX: float = 5.0 +const NEXT_FISH_DELAY_MIN: float = 5.0 +const NEXT_FISH_DELAY_MAX: float = 12.0 +const CROSSING_DURATION_MIN: float = 8.0 +const CROSSING_DURATION_MAX: float = 16.0 +const FISH_LONGEST_SIDE_MIN: float = 120.0 +const FISH_LONGEST_SIDE_MAX: float = 210.0 +const FISH_EDGE_MARGIN: float = 24.0 +const BUBBLE_EVENT_DELAY_MIN: float = 4.0 +const BUBBLE_EVENT_DELAY_MAX: float = 10.0 +const BUBBLE_CLUSTER_DELAY_MIN: float = 0.25 +const BUBBLE_CLUSTER_DELAY_MAX: float = 0.65 +const BUBBLE_TRAVEL_DURATION_MIN: float = 7.0 +const BUBBLE_TRAVEL_DURATION_MAX: float = 14.0 +const BUBBLE_SCALE_MIN: float = 0.65 +const BUBBLE_SCALE_MAX: float = 1.15 +const BUBBLE_OPACITY_MIN: float = 0.55 +const BUBBLE_OPACITY_MAX: float = 0.90 +const BUBBLE_DRIFT_MIN: float = 10.0 +const BUBBLE_DRIFT_MAX: float = 45.0 +const BUBBLE_WOBBLE_MIN: float = 3.0 +const BUBBLE_WOBBLE_MAX: float = 9.0 +const BUBBLE_EDGE_MARGIN: float = 16.0 +const MAX_DECORATIVE_BUBBLES: int = 6 +const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0 +const LOGO_WIDTH_FACTOR: float = 0.52 +const LOGO_MIN_WIDTH: float = 320.0 +const LOGO_MAX_WIDTH: float = 720.0 +const BUTTON_COLUMN_WIDTH: float = 360.0 +const TITLE_HORIZONTAL_MARGIN: float = 48.0 +const START_PROMPT_MIN_SCALE: float = 0.985 +const START_PROMPT_MAX_SCALE: float = 1.015 +const START_PROMPT_CYCLE_SECONDS: float = 3.0 signal gameplay_requested signal quit_requested @@ -21,17 +67,41 @@ enum ConfirmationAction { @onready var _new_game_button: Button = %NewGameButton @onready var _settings_button: Button = %SettingsButton @onready var _delete_button: Button = %DeleteSaveButton +@onready var _quit_button: Button = %QuitButton @onready var _feedback_label: Label = %FeedbackLabel @onready var _confirmation_panel: PanelContainer = %ConfirmationPanel @onready var _confirmation_text: Label = %ConfirmationText @onready var _confirm_button: Button = %ConfirmButton @onready var _settings_panel: SettingsPanelType = %SettingsPanel +@onready var _decorative_fish_layer: Control = %DecorativeFishLayer +@onready var _decorative_fish_timer: Timer = %DecorativeFishTimer +@onready var _decorative_bubble_layer: Control = %DecorativeBubbleLayer +@onready var _decorative_bubble_event_timer: Timer = %DecorativeBubbleEventTimer +@onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer +@onready var _title_logo: TextureRect = %TitleLogo +@onready var _button_center: CenterContainer = %ButtonCenter +@onready var _button_stack: VBoxContainer = %ButtonStack +@onready var _start_prompt_center: CenterContainer = %StartPromptCenter +@onready var _start_prompt_label: Label = %StartPromptLabel var _save_manager: SaveManagerType var _settings_manager: SettingsManagerType var _inspection: SaveInspectionType var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _action_in_progress: bool = false +var _decorative_rng := RandomNumberGenerator.new() +var _decorative_fish: TextureRect +var _decorative_fish_tween: Tween +var _decorative_presentation_ready: bool = false +var _decorative_presentation_active: bool = false +var _decorative_generation: int = 0 +var _decorative_bubbles: Array[TextureRect] = [] +var _decorative_bubble_tweens: Dictionary[int, Tween] = {} +var _pending_cluster_bubbles: int = 0 +var _awaiting_start_input: bool = false +var _start_prompt_elapsed: float = 0.0 +var _navigation_focus_active: bool = false +var _modal_restore_navigation_focus: bool = false func _ready() -> void: @@ -44,6 +114,20 @@ func _ready() -> void: %CancelConfirmButton.pressed.connect(_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) + _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) + _decorative_bubble_event_timer.timeout.connect( + _on_decorative_bubble_event_timer_timeout + ) + _decorative_bubble_cluster_timer.timeout.connect( + _on_decorative_bubble_cluster_timer_timeout + ) + visibility_changed.connect(_on_title_visibility_changed) + resized.connect(_update_title_layout) + _start_prompt_label.resized.connect(_update_start_prompt_pivot) + _decorative_rng.randomize() + set_process(false) + call_deferred("_update_title_layout") + call_deferred("_update_start_prompt_pivot") func setup( @@ -55,20 +139,104 @@ func setup( Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _refresh_save_inspection() show() - call_deferred("_focus_initial_button") + _decorative_presentation_ready = true + _start_decorative_presentation() + _begin_title_entry() func reopen() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _prepare_awaiting_start_input() _close_confirmation() _settings_panel.hide() _refresh_save_inspection() show() - call_deferred("_focus_initial_button") + _start_decorative_presentation() + _start_entry_prompt_animation() + + +func is_awaiting_start_input() -> bool: + return _awaiting_start_input + + +func is_decorative_presentation_active() -> bool: + return _decorative_presentation_active + + +func get_active_decorative_fish_count() -> int: + return 1 if is_instance_valid(_decorative_fish) else 0 + + +func get_active_decorative_bubble_count() -> int: + return _decorative_bubbles.size() + + +func is_decorative_bubble_scheduler_active() -> bool: + return ( + _decorative_presentation_active + and ( + not _decorative_bubble_event_timer.is_stopped() + or not _decorative_bubble_cluster_timer.is_stopped() + ) + ) + + +func _update_title_layout() -> void: + if not is_node_ready(): + return + var available_width: float = maxf(1.0, size.x - TITLE_HORIZONTAL_MARGIN) + var logo_width: float = minf( + clampf( + size.x * LOGO_WIDTH_FACTOR, + LOGO_MIN_WIDTH, + LOGO_MAX_WIDTH + ), + available_width + ) + _title_logo.custom_minimum_size = Vector2( + logo_width, + logo_width / LOGO_ASPECT_RATIO + ) + _button_stack.custom_minimum_size = Vector2( + minf(BUTTON_COLUMN_WIDTH, available_width), + 0.0 + ) + + +func _process(delta: float) -> void: + if ( + not _awaiting_start_input + or not visible + or not _start_prompt_center.visible + ): + return + _start_prompt_elapsed = fmod( + _start_prompt_elapsed + delta, + START_PROMPT_CYCLE_SECONDS + ) + var phase: float = _start_prompt_elapsed / START_PROMPT_CYCLE_SECONDS + var pulse_weight: float = (sin(phase * TAU - PI * 0.5) + 1.0) * 0.5 + var prompt_scale: float = lerpf( + START_PROMPT_MIN_SCALE, + START_PROMPT_MAX_SCALE, + pulse_weight + ) + _start_prompt_label.scale = Vector2.ONE * prompt_scale func _input(event: InputEvent) -> void: - if not visible or not event.is_action_pressed("ui_cancel"): + if not visible: + return + if _awaiting_start_input: + if not _is_start_prompt_reveal_event(event): + return + _reveal_primary_menu() + get_viewport().set_input_as_handled() + return + if _handle_primary_menu_focus_input(event): + get_viewport().set_input_as_handled() + return + if not event.is_action_pressed("ui_cancel"): return if _confirmation_panel.visible: _close_confirmation() @@ -79,6 +247,119 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func _is_start_prompt_reveal_event(event: InputEvent) -> bool: + if event is InputEventKey: + var key_event := event as InputEventKey + return key_event.pressed and not key_event.echo + if event is InputEventJoypadButton: + return (event as InputEventJoypadButton).pressed + if event is InputEventMouseButton: + return (event as InputEventMouseButton).pressed + return false + + +func _handle_primary_menu_focus_input(event: InputEvent) -> bool: + if ( + not _button_center.visible + or _confirmation_panel.visible + or _settings_panel.visible + ): + return false + if event is InputEventMouseMotion: + _navigation_focus_active = false + _release_primary_menu_focus() + return false + if event is InputEventKey and (event as InputEventKey).echo: + return false + var moves_forward: bool = ( + event.is_action_pressed("ui_down") + or event.is_action_pressed("ui_right") + ) + var moves_backward: bool = ( + event.is_action_pressed("ui_up") + or event.is_action_pressed("ui_left") + ) + if not moves_forward and not moves_backward: + return false + _navigation_focus_active = true + if _primary_menu_has_focus(): + return false + if moves_forward: + _get_first_available_menu_button().grab_focus() + else: + _quit_button.grab_focus() + return true + + +func _get_first_available_menu_button() -> Button: + return _continue_button if not _continue_button.disabled else _new_game_button + + +func _primary_menu_has_focus() -> bool: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + return focus_owner != null and _button_center.is_ancestor_of(focus_owner) + + +func _release_primary_menu_focus() -> void: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and _button_center.is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _release_title_focus() -> void: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _begin_title_entry() -> void: + _prepare_awaiting_start_input() + _start_entry_prompt_animation() + + +func _prepare_awaiting_start_input() -> void: + _awaiting_start_input = true + _navigation_focus_active = false + _modal_restore_navigation_focus = false + _button_center.hide() + _feedback_label.hide() + _start_prompt_center.show() + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _start_entry_prompt_animation() -> void: + _start_prompt_elapsed = 0.0 + _start_prompt_label.scale = Vector2.ONE * START_PROMPT_MIN_SCALE + _update_start_prompt_pivot() + set_process(true) + + +func _stop_entry_prompt_animation() -> void: + set_process(false) + _start_prompt_elapsed = 0.0 + _start_prompt_label.scale = Vector2.ONE + + +func _update_start_prompt_pivot() -> void: + if not is_node_ready(): + return + _start_prompt_label.pivot_offset = _start_prompt_label.size * 0.5 + + +func _reveal_primary_menu() -> void: + if not _awaiting_start_input: + return + _awaiting_start_input = false + _stop_entry_prompt_animation() + _start_prompt_center.hide() + _button_center.show() + _feedback_label.show() + _navigation_focus_active = false + _release_title_focus() + + func _on_continue_pressed() -> void: if ( _action_in_progress @@ -90,11 +371,11 @@ func _on_continue_pressed() -> void: return _action_in_progress = true if _save_manager.load_player_data(): - _feedback_label.text = "Save loaded." + _feedback_label.text = "save loaded." gameplay_requested.emit() else: _refresh_save_inspection() - _feedback_label.text = "Failed to load save. The original was preserved." + _feedback_label.text = "failed to load save. the original was preserved." _action_in_progress = false @@ -112,17 +393,17 @@ func _on_new_game_pressed() -> void: return if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION: _feedback_label.text = ( - "This save is from a newer game version. " - + "Use Delete Save before starting over." + "this save is from a newer game version. " + + "use delete save before starting over." ) return if _inspection.status == SaveInspectionType.Status.IO_ERROR: - _feedback_label.text = "The existing save cannot be accessed safely." + _feedback_label.text = "the existing save cannot be accessed safely." return _open_confirmation( ConfirmationAction.NEW_GAME, - "Start a new game? Existing progression will be deleted.", - "Start New Game" + "start a new game? existing progression will be deleted.", + "start new game" ) @@ -137,8 +418,8 @@ func _on_delete_pressed() -> void: return _open_confirmation( ConfirmationAction.DELETE_SAVE, - "Delete your saved progression? This cannot be undone.", - "Delete" + "delete your saved progression? this cannot be undone.", + "delete" ) @@ -149,7 +430,7 @@ func _on_confirmation_accepted() -> void: _close_confirmation() _action_in_progress = true if not _save_manager.delete_progression_save(): - _feedback_label.text = "Failed to delete saved progression." + _feedback_label.text = "failed to delete saved progression." _action_in_progress = false _refresh_save_inspection() return @@ -158,7 +439,7 @@ func _on_confirmation_accepted() -> void: if action == ConfirmationAction.NEW_GAME: gameplay_requested.emit() else: - _feedback_label.text = "Saved progression deleted." + _feedback_label.text = "saved progression deleted." _action_in_progress = false @@ -167,6 +448,7 @@ func _open_confirmation( message: String, confirm_text: String, ) -> void: + _modal_restore_navigation_focus = _navigation_focus_active _confirmation_action = action _confirmation_text.text = message _confirm_button.text = confirm_text @@ -175,14 +457,23 @@ func _open_confirmation( func _close_confirmation() -> void: + var restore_navigation_focus: bool = _modal_restore_navigation_focus + _modal_restore_navigation_focus = false _confirmation_action = ConfirmationAction.NONE _confirmation_panel.visible = false - _focus_initial_button() + if _awaiting_start_input: + return + if restore_navigation_focus: + _focus_initial_button() + else: + _navigation_focus_active = false + _release_title_focus() func _open_settings() -> void: if _confirmation_panel.visible or _action_in_progress: return + _modal_restore_navigation_focus = _navigation_focus_active _feedback_label.text = "" _settings_panel.open_panel(_settings_manager) @@ -192,12 +483,23 @@ func _close_settings() -> void: func _on_settings_applied() -> void: - _feedback_label.text = "Settings saved." - _settings_button.grab_focus() + _feedback_label.text = "settings saved." + _restore_settings_focus() func _on_settings_closed() -> void: - _settings_button.grab_focus() + _restore_settings_focus() + + +func _restore_settings_focus() -> void: + var restore_navigation_focus: bool = _modal_restore_navigation_focus + _modal_restore_navigation_focus = false + if restore_navigation_focus: + _navigation_focus_active = true + _settings_button.grab_focus() + else: + _navigation_focus_active = false + _release_title_focus() func _refresh_save_inspection() -> void: @@ -217,12 +519,18 @@ func _refresh_save_inspection() -> void: func _focus_initial_button() -> void: - if not is_node_ready() or not visible: + if ( + not is_node_ready() + or not visible + or _awaiting_start_input + or not _button_center.visible + ): return if not _continue_button.disabled: _continue_button.grab_focus() else: _new_game_button.grab_focus() + _navigation_focus_active = true func _on_quit_pressed() -> void: @@ -232,3 +540,430 @@ func _on_quit_pressed() -> void: and not _settings_panel.visible ): quit_requested.emit() + + +func _on_title_visibility_changed() -> void: + if not _decorative_presentation_ready: + return + if visible: + _start_decorative_presentation() + else: + _stop_entry_prompt_animation() + _navigation_focus_active = false + _modal_restore_navigation_focus = false + _stop_decorative_presentation() + + +func _start_decorative_presentation() -> void: + if ( + not _decorative_presentation_ready + or not visible + or _decorative_presentation_active + ): + return + _decorative_generation += 1 + _decorative_presentation_active = true + _schedule_next_decorative_fish( + FIRST_FISH_DELAY_MIN, + FIRST_FISH_DELAY_MAX + ) + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _stop_decorative_presentation() -> void: + _decorative_generation += 1 + _decorative_presentation_active = false + _decorative_fish_timer.stop() + if _decorative_fish_tween != null: + _decorative_fish_tween.kill() + _decorative_fish_tween = null + if is_instance_valid(_decorative_fish): + _decorative_fish.queue_free() + _decorative_fish = null + _decorative_bubble_event_timer.stop() + _decorative_bubble_cluster_timer.stop() + _pending_cluster_bubbles = 0 + for bubble_tween: Tween in _decorative_bubble_tweens.values(): + if bubble_tween != null: + bubble_tween.kill() + _decorative_bubble_tweens.clear() + for bubble: TextureRect in _decorative_bubbles: + if is_instance_valid(bubble): + bubble.queue_free() + _decorative_bubbles.clear() + + +func _schedule_next_decorative_fish( + minimum_delay: float, + maximum_delay: float, +) -> void: + if not _decorative_presentation_active or not visible: + return + _decorative_fish_timer.start( + _decorative_rng.randf_range(minimum_delay, maximum_delay) + ) + + +func _on_decorative_fish_timer_timeout() -> void: + if ( + not _decorative_presentation_active + or not visible + or is_instance_valid(_decorative_fish) + ): + return + _spawn_decorative_fish(_decorative_generation) + + +func _spawn_decorative_fish(generation: int) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not visible + or DECORATIVE_FISH_TEXTURES.is_empty() + ): + return + var layer_size: Vector2 = _decorative_fish_layer.size + if layer_size.x <= 1.0 or layer_size.y <= 1.0: + _schedule_next_decorative_fish(0.5, 1.0) + return + var texture: Texture2D = DECORATIVE_FISH_TEXTURES[ + _decorative_rng.randi_range(0, DECORATIVE_FISH_TEXTURES.size() - 1) + ] + var source_size: Vector2 = texture.get_size() + if source_size.x <= 0.0 or source_size.y <= 0.0: + _schedule_next_decorative_fish( + NEXT_FISH_DELAY_MIN, + NEXT_FISH_DELAY_MAX + ) + return + var longest_side: float = _decorative_rng.randf_range( + FISH_LONGEST_SIDE_MIN, + FISH_LONGEST_SIDE_MAX + ) + var presentation_scale: float = longest_side / maxf( + source_size.x, + source_size.y + ) + var presentation_size: Vector2 = source_size * presentation_scale + var fish_control := TextureRect.new() + fish_control.name = "DecorativeFish" + fish_control.texture = texture + fish_control.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + fish_control.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + fish_control.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + fish_control.mouse_filter = Control.MOUSE_FILTER_IGNORE + fish_control.custom_minimum_size = presentation_size + fish_control.size = presentation_size + fish_control.modulate.a = 0.46 + + var direction: float = ( + 1.0 + if _decorative_rng.randi_range(0, 1) == 1 + else -1.0 + ) + fish_control.flip_h = direction > 0.0 + var minimum_y: float = maxf(24.0, layer_size.y * 0.08) + var maximum_y: float = maxf( + minimum_y, + layer_size.y - presentation_size.y - maxf(24.0, layer_size.y * 0.08) + ) + var base_y: float = _decorative_rng.randf_range(minimum_y, maximum_y) + var bob_amplitude: float = _decorative_rng.randf_range(3.0, 8.0) + var bob_speed: float = _decorative_rng.randf_range(0.65, 1.15) + var crossing_duration: float = _decorative_rng.randf_range( + CROSSING_DURATION_MIN, + CROSSING_DURATION_MAX + ) + fish_control.position = Vector2( + -presentation_size.x - FISH_EDGE_MARGIN + if direction > 0.0 + else layer_size.x + FISH_EDGE_MARGIN, + base_y + ) + _decorative_fish_layer.add_child(fish_control) + _decorative_fish = fish_control + _decorative_fish_tween = create_tween() + _decorative_fish_tween.tween_method( + _update_decorative_fish.bind( + fish_control, + direction, + base_y, + bob_amplitude, + bob_speed, + crossing_duration, + generation + ), + 0.0, + 1.0, + crossing_duration + ) + _decorative_fish_tween.finished.connect( + _on_decorative_fish_finished.bind(fish_control, generation), + CONNECT_ONE_SHOT + ) + + +func _update_decorative_fish( + progress: float, + fish_control: TextureRect, + direction: float, + base_y: float, + bob_amplitude: float, + bob_speed: float, + crossing_duration: float, + generation: int, +) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not is_instance_valid(fish_control) + ): + return + var start_x: float = ( + -fish_control.size.x - FISH_EDGE_MARGIN + if direction > 0.0 + else _decorative_fish_layer.size.x + FISH_EDGE_MARGIN + ) + var end_x: float = ( + _decorative_fish_layer.size.x + FISH_EDGE_MARGIN + if direction > 0.0 + else -fish_control.size.x - FISH_EDGE_MARGIN + ) + fish_control.position.x = lerpf(start_x, end_x, progress) + var bobbed_y: float = ( + base_y + + sin(progress * crossing_duration * bob_speed) * bob_amplitude + ) + fish_control.position.y = clampf( + bobbed_y, + 8.0, + maxf(8.0, _decorative_fish_layer.size.y - fish_control.size.y - 8.0) + ) + + +func _on_decorative_fish_finished( + fish_control: TextureRect, + generation: int, +) -> void: + if generation != _decorative_generation: + return + _decorative_fish_tween = null + if is_instance_valid(fish_control): + fish_control.queue_free() + if _decorative_fish == fish_control: + _decorative_fish = null + _schedule_next_decorative_fish( + NEXT_FISH_DELAY_MIN, + NEXT_FISH_DELAY_MAX + ) + + +func _schedule_next_decorative_bubble_event( + minimum_delay: float, + maximum_delay: float, +) -> void: + if not _decorative_presentation_active or not visible: + return + _decorative_bubble_event_timer.start( + _decorative_rng.randf_range(minimum_delay, maximum_delay) + ) + + +func _on_decorative_bubble_event_timer_timeout() -> void: + if not _decorative_presentation_active or not visible: + return + var available_slots: int = ( + MAX_DECORATIVE_BUBBLES - _decorative_bubbles.size() + ) + if available_slots <= 0: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + return + var event_size: int = _choose_decorative_bubble_event_size() + event_size = mini(event_size, available_slots) + if not _spawn_decorative_bubble(_decorative_generation): + _schedule_next_decorative_bubble_event(0.5, 1.0) + return + _pending_cluster_bubbles = event_size - 1 + if _pending_cluster_bubbles > 0: + _start_decorative_bubble_cluster_timer() + else: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _choose_decorative_bubble_event_size() -> int: + var roll: float = _decorative_rng.randf() + if roll < 0.75: + return 1 + if roll < 0.95: + return 2 + return 3 + + +func _start_decorative_bubble_cluster_timer() -> void: + _decorative_bubble_cluster_timer.start( + _decorative_rng.randf_range( + BUBBLE_CLUSTER_DELAY_MIN, + BUBBLE_CLUSTER_DELAY_MAX + ) + ) + + +func _on_decorative_bubble_cluster_timer_timeout() -> void: + if ( + not _decorative_presentation_active + or not visible + or _pending_cluster_bubbles <= 0 + ): + _pending_cluster_bubbles = 0 + return + if _decorative_bubbles.size() < MAX_DECORATIVE_BUBBLES: + _spawn_decorative_bubble(_decorative_generation) + _pending_cluster_bubbles -= 1 + if _pending_cluster_bubbles > 0: + _start_decorative_bubble_cluster_timer() + else: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _spawn_decorative_bubble(generation: int) -> bool: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not visible + or _decorative_bubbles.size() >= MAX_DECORATIVE_BUBBLES + or DECORATIVE_BUBBLE_TEXTURES.is_empty() + ): + return false + var layer_size: Vector2 = _decorative_bubble_layer.size + if layer_size.x <= 1.0 or layer_size.y <= 1.0: + return false + var texture: Texture2D = DECORATIVE_BUBBLE_TEXTURES[ + _decorative_rng.randi_range( + 0, + DECORATIVE_BUBBLE_TEXTURES.size() - 1 + ) + ] + var presentation_scale: float = _decorative_rng.randf_range( + BUBBLE_SCALE_MIN, + BUBBLE_SCALE_MAX + ) + var presentation_size: Vector2 = texture.get_size() * presentation_scale + var bubble := TextureRect.new() + bubble.name = "DecorativeBubble" + bubble.texture = texture + bubble.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + bubble.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + bubble.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + bubble.size = presentation_size + bubble.modulate.a = _decorative_rng.randf_range( + BUBBLE_OPACITY_MIN, + BUBBLE_OPACITY_MAX + ) + _decorative_bubble_layer.add_child(bubble) + _decorative_bubbles.append(bubble) + + var normalized_x: float = _decorative_rng.randf_range(0.08, 0.92) + var drift_direction: float = ( + 1.0 if _decorative_rng.randi_range(0, 1) == 1 else -1.0 + ) + var horizontal_drift: float = ( + _decorative_rng.randf_range( + BUBBLE_DRIFT_MIN, + BUBBLE_DRIFT_MAX + ) + * drift_direction + ) + var wobble_amplitude: float = _decorative_rng.randf_range( + BUBBLE_WOBBLE_MIN, + BUBBLE_WOBBLE_MAX + ) + var wobble_cycles: float = _decorative_rng.randf_range(0.8, 1.6) + var wobble_phase: float = _decorative_rng.randf_range(0.0, TAU) + var travel_duration: float = _decorative_rng.randf_range( + BUBBLE_TRAVEL_DURATION_MIN, + BUBBLE_TRAVEL_DURATION_MAX + ) + var bubble_tween: Tween = create_tween() + _decorative_bubble_tweens[bubble.get_instance_id()] = bubble_tween + bubble_tween.tween_method( + _update_decorative_bubble.bind( + bubble, + normalized_x, + horizontal_drift, + wobble_amplitude, + wobble_cycles, + wobble_phase, + generation + ), + 0.0, + 1.0, + travel_duration + ) + bubble_tween.finished.connect( + _on_decorative_bubble_finished.bind(bubble, generation), + CONNECT_ONE_SHOT + ) + return true + + +func _update_decorative_bubble( + progress: float, + bubble: TextureRect, + normalized_x: float, + horizontal_drift: float, + wobble_amplitude: float, + wobble_cycles: float, + wobble_phase: float, + generation: int, +) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not is_instance_valid(bubble) + ): + return + var layer_size: Vector2 = _decorative_bubble_layer.size + var start_y: float = ( + layer_size.y + bubble.size.y + BUBBLE_EDGE_MARGIN + ) + var end_y: float = -bubble.size.y - BUBBLE_EDGE_MARGIN + var base_x: float = normalized_x * layer_size.x + var wobble: float = sin( + wobble_phase + progress * TAU * wobble_cycles + ) * wobble_amplitude + bubble.position = Vector2( + base_x + + horizontal_drift * progress + + wobble + - bubble.size.x * 0.5, + lerpf(start_y, end_y, progress) + ) + + +func _on_decorative_bubble_finished( + bubble: TextureRect, + generation: int, +) -> void: + if generation != _decorative_generation: + return + if is_instance_valid(bubble): + _decorative_bubble_tweens.erase(bubble.get_instance_id()) + _decorative_bubbles.erase(bubble) + bubble.queue_free() + + +func _exit_tree() -> void: + _stop_decorative_presentation() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index dce846f..b26271f 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,8 +1,17 @@ -[gd_scene load_steps=4 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="3_settings"] +[ext_resource type="Shader" path="res://ui/title_water_background.gdshader" id="4_water_shader"] +[ext_resource type="Texture2D" path="res://ui/assets/title/netfishing_logo.png" id="5_logo"] +[ext_resource type="Shader" path="res://ui/title_logo_underwater.gdshader" id="6_logo_shader"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] +shader = ExtResource("4_water_shader") + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_logo"] +shader = ExtResource("6_logo_shader") [node name="TitleScreen" type="Control"] unique_name_in_owner = true @@ -18,15 +27,78 @@ theme = ExtResource("2_theme") script = ExtResource("1_script") [node name="Background" type="ColorRect" parent="."] +material = SubResource("ShaderMaterial_title_water") layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -color = Color(0.047, 0.102, 0.145, 1) +mouse_filter = 2 + +[node name="DecorativeFishLayer" type="Control" parent="."] +unique_name_in_owner = true +z_index = 1 +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="DecorativeFishTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="DecorativeBubbleLayer" type="Control" parent="."] +unique_name_in_owner = true +z_index = 2 +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="DecorativeBubbleEventTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="DecorativeBubbleClusterTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="StartPromptCenter" type="CenterContainer" parent="."] +unique_name_in_owner = true +z_index = 12 +layout_mode = 1 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -104.0 +offset_bottom = -48.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 + +[node name="StartPromptLabel" type="Label" parent="StartPromptCenter"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_colors/font_outline_color = Color(0.02, 0.075, 0.11, 0.9) +theme_override_constants/outline_size = 3 +theme_override_font_sizes/font_size = 26 +text = "press any key to start" +horizontal_alignment = 1 +vertical_alignment = 1 [node name="Center" type="CenterContainer" parent="."] +z_index = 10 layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -34,75 +106,77 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="MainPanel" type="PanelContainer" parent="Center"] -custom_minimum_size = Vector2(440, 0) +[node name="MainContent" type="VBoxContainer" parent="Center"] layout_mode = 2 +theme_override_constants/separation = 8 +alignment = 1 -[node name="Margin" type="MarginContainer" parent="Center/MainPanel"] +[node name="TitleLogo" type="TextureRect" parent="Center/MainContent"] +unique_name_in_owner = true +custom_minimum_size = Vector2(640, 190) +material = SubResource("ShaderMaterial_title_logo") layout_mode = 2 -theme_override_constants/margin_left = 36 -theme_override_constants/margin_top = 30 -theme_override_constants/margin_right = 36 -theme_override_constants/margin_bottom = 30 +mouse_filter = 2 +texture_filter = 1 +texture = ExtResource("5_logo") +expand_mode = 1 +stretch_mode = 5 -[node name="Content" type="VBoxContainer" parent="Center/MainPanel/Margin"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Title" type="Label" parent="Center/MainPanel/Margin/Content"] -layout_mode = 2 -theme_override_font_sizes/font_size = 36 -text = "NETFISHING" -horizontal_alignment = 1 - -[node name="Subtitle" type="Label" parent="Center/MainPanel/Margin/Content"] -layout_mode = 2 -text = "A cozy social fishing game" -horizontal_alignment = 1 - -[node name="PlaytestLabel" type="Label" parent="Center/MainPanel/Margin/Content"] +[node name="PlaytestLabel" type="Label" parent="Center/MainContent"] layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 13 -text = "Pre-Alpha Playtest 0.1" +text = "pre-alpha playtest 0.1" horizontal_alignment = 1 -[node name="Spacer" type="Control" parent="Center/MainPanel/Margin/Content"] -custom_minimum_size = Vector2(0, 12) +[node name="Spacer" type="Control" parent="Center/MainContent"] +custom_minimum_size = Vector2(0, 4) layout_mode = 2 -[node name="ContinueButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="ButtonCenter" type="CenterContainer" parent="Center/MainContent"] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="ButtonStack" type="VBoxContainer" parent="Center/MainContent/ButtonCenter"] +unique_name_in_owner = true +custom_minimum_size = Vector2(360, 0) +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Continue" +text = "continue" -[node name="NewGameButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "New Game" +text = "new game" -[node name="SettingsButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Settings" +text = "settings" -[node name="DeleteSaveButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Delete Save" +text = "delete save" -[node name="QuitButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Quit" +text = "quit" -[node name="FeedbackLabel" type="Label" parent="Center/MainPanel/Margin/Content"] +[node name="FeedbackLabel" type="Label" parent="Center/MainContent"] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(0, 42) layout_mode = 2 text = "" @@ -141,7 +215,7 @@ theme_override_constants/separation = 14 [node name="ConfirmationText" type="Label" parent="ConfirmationPanel/Margin/Content"] unique_name_in_owner = true layout_mode = 2 -text = "Confirm?" +text = "confirm?" horizontal_alignment = 1 autowrap_mode = 2 @@ -154,13 +228,13 @@ alignment = 1 unique_name_in_owner = true custom_minimum_size = Vector2(150, 42) layout_mode = 2 -text = "Confirm" +text = "confirm" [node name="CancelConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"] unique_name_in_owner = true custom_minimum_size = Vector2(120, 42) layout_mode = 2 -text = "Cancel" +text = "cancel" [node name="SettingsPanel" parent="." instance=ExtResource("3_settings")] unique_name_in_owner = true diff --git a/ui/title_water_background.gdshader b/ui/title_water_background.gdshader new file mode 100644 index 0000000..c7faabe --- /dev/null +++ b/ui/title_water_background.gdshader @@ -0,0 +1,95 @@ +shader_type canvas_item; + +uniform vec4 surface_color : source_color = vec4(0.10, 0.45, 0.62, 1.0); +uniform vec4 middle_color : source_color = vec4(0.055, 0.285, 0.40, 1.0); +uniform vec4 depth_color : source_color = vec4(0.02, 0.16, 0.27, 1.0); +uniform vec2 virtual_pixel_density = vec2(320.0, 180.0); +uniform float color_step_count : hint_range(2.0, 16.0, 1.0) = 14.0; +uniform float continuous_depth_blend : hint_range(0.0, 0.25, 0.01) = 0.12; +uniform float wave_speed : hint_range(0.0, 0.5, 0.01) = 0.10; +uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4; +uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012; +uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018; + +void fragment() { + vec2 grid_size = max(virtual_pixel_density, vec2(1.0)); + vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size; + float motion = TIME * wave_speed; + + float broad_wave = sin( + pixel_uv.x * wave_scale + motion + ); + float secondary_wave = sin( + pixel_uv.x * 1.1 + pixel_uv.y * 1.6 - motion * 0.65 + ); + float movement_falloff = 1.0 - pixel_uv.y * 0.75; + float depth_displacement = ( + (broad_wave * 0.65 + secondary_wave * 0.35) + * wave_strength + * movement_falloff + ); + float continuous_depth = clamp( + pixel_uv.y + + depth_displacement, + 0.0, + 1.0 + ); + float depth_steps = max(2.0, floor(color_step_count)); + float band_index = floor( + continuous_depth * (depth_steps - 1.0) + 0.5 + ); + float band_depth = band_index / (depth_steps - 1.0); + float display_depth = mix( + band_depth, + continuous_depth, + clamp(continuous_depth_blend, 0.0, 0.25) + ); + + vec3 water_color; + if (display_depth < 0.5) { + water_color = mix( + surface_color.rgb, + middle_color.rgb, + display_depth * 2.0 + ); + } else { + water_color = mix( + middle_color.rgb, + depth_color.rgb, + (display_depth - 0.5) * 2.0 + ); + } + + float caustic_wave = clamp( + ( + sin( + pixel_uv.x * 4.0 + + pixel_uv.y * 1.2 + + motion * 0.7 + ) + * sin( + pixel_uv.x * 1.7 + - pixel_uv.y * 2.2 + - motion * 0.55 + ) + - 0.25 + ) / 0.75, + 0.0, + 1.0 + ); + float caustic_level = floor( + caustic_wave * 3.0 + ) / 3.0; + float upper_water_mask = 1.0 - smoothstep( + 0.18, + 0.34, + pixel_uv.y + ); + water_color += ( + vec3(0.05, 0.12, 0.14) + * caustic_level + * upper_water_mask + * caustic_strength + ); + COLOR = vec4(clamp(water_color, vec3(0.0), vec3(1.0)), 1.0); +} diff --git a/ui/title_water_background.gdshader.uid b/ui/title_water_background.gdshader.uid new file mode 100644 index 0000000..fda761b --- /dev/null +++ b/ui/title_water_background.gdshader.uid @@ -0,0 +1 @@ +uid://c8s1g7tn771du diff --git a/world/interactables/fishing_shop_world.tscn b/world/interactables/fishing_shop_world.tscn index d5946ce..7b87a81 100644 --- a/world/interactables/fishing_shop_world.tscn +++ b/world/interactables/fishing_shop_world.tscn @@ -60,7 +60,7 @@ material_override = SubResource("PrimaryMaterial") [node name="ShopName" type="Label3D" parent="Visuals"] position = Vector3(0, 3.7, 0) -text = "Fishing Shop" +text = "fishing shop" font_size = 42 outline_size = 9 billboard = 1 diff --git a/world/interactables/pelican_buyer_world.tscn b/world/interactables/pelican_buyer_world.tscn index bc204c4..2fde14c 100644 --- a/world/interactables/pelican_buyer_world.tscn +++ b/world/interactables/pelican_buyer_world.tscn @@ -35,7 +35,7 @@ material_override = SubResource("WoodMaterial") [node name="Sign" type="Label3D" parent="Visuals"] position = Vector3(0, 2.55, 0) -text = "Sell to Pelicans\nfrom the Cooler" +text = "sell to pelicans\nfrom the cooler" font_size = 30 outline_size = 8 billboard = 1 @@ -43,7 +43,7 @@ no_depth_test = true [node name="ConvenienceLabel" type="Label3D" parent="Visuals"] position = Vector3(0, 1.65, -1.15) -text = "Available anywhere • 0.25x" +text = "available anywhere • 0.25x" font_size = 20 outline_size = 6 billboard = 1 diff --git a/world/regions/coastal_dock_region.tscn b/world/regions/coastal_dock_region.tscn index c8b02da..475d91d 100644 --- a/world/regions/coastal_dock_region.tscn +++ b/world/regions/coastal_dock_region.tscn @@ -82,7 +82,7 @@ mesh = SubResource("CraneArmMesh") material_override = SubResource("BeaconMaterial") [node name="CoastLabel" type="Label3D" parent="Visuals"] position = Vector3(25, 3.2, 23) -text = "COAST" +text = "coast" font_size = 38 outline_size = 8 billboard = 1 diff --git a/world/regions/lake_region.tscn b/world/regions/lake_region.tscn index 0d7ba8f..7e9259b 100644 --- a/world/regions/lake_region.tscn +++ b/world/regions/lake_region.tscn @@ -71,7 +71,7 @@ mesh = SubResource("DockRampMesh") material_override = SubResource("DockMaterial") [node name="LakeLabel" type="Label3D" parent="Visuals"] position = Vector3(-25, 3, 20) -text = "LAKE" +text = "lake" font_size = 38 outline_size = 8 billboard = 1 diff --git a/world/regions/marsh_region.tscn b/world/regions/marsh_region.tscn index 50e7c9e..29fea93 100644 --- a/world/regions/marsh_region.tscn +++ b/world/regions/marsh_region.tscn @@ -90,7 +90,7 @@ mesh = SubResource("ReedMesh") material_override = SubResource("ReedMaterial") [node name="MarshLabel" type="Label3D" parent="Visuals"] position = Vector3(21, 3, 17) -text = "MARSH" +text = "marsh" font_size = 36 outline_size = 8 billboard = 1 diff --git a/world/regions/river_region.tscn b/world/regions/river_region.tscn index 6010084..02fcdd9 100644 --- a/world/regions/river_region.tscn +++ b/world/regions/river_region.tscn @@ -76,7 +76,7 @@ mesh = SubResource("RailMesh") material_override = SubResource("RailMaterial") [node name="RiverLabel" type="Label3D" parent="Visuals"] position = Vector3(-16, 3, 20) -text = "RIVER" +text = "river" font_size = 34 outline_size = 7 billboard = 1 diff --git a/world/regions/starter_pond_region.tscn b/world/regions/starter_pond_region.tscn index 7249437..f484beb 100644 --- a/world/regions/starter_pond_region.tscn +++ b/world/regions/starter_pond_region.tscn @@ -54,7 +54,7 @@ mesh = SubResource("WaterMesh") material_override = SubResource("WaterMaterial") [node name="PondLabel" type="Label3D" parent="Visuals"] position = Vector3(-14, 2.4, 11) -text = "POND" +text = "pond" font_size = 34 outline_size = 7 billboard = 1 diff --git a/world/regions/village_region.tscn b/world/regions/village_region.tscn index f6b46b1..10a4527 100644 --- a/world/regions/village_region.tscn +++ b/world/regions/village_region.tscn @@ -122,7 +122,7 @@ material_override = SubResource("RoofMaterial") [node name="VillageLabel" type="Label3D" parent="Visuals"] position = Vector3(0, 3.2, 7) -text = "VILLAGE" +text = "village" font_size = 42 outline_size = 8 billboard = 1