diff --git a/art/exported/characters/faces/mouth/mouth_open_ah.png b/art/exported/characters/faces/mouth/mouth_open_ah.png new file mode 100644 index 0000000..b028f06 Binary files /dev/null and b/art/exported/characters/faces/mouth/mouth_open_ah.png differ diff --git a/art/exported/characters/faces/mouth/mouth_open_ah.png.import b/art/exported/characters/faces/mouth/mouth_open_ah.png.import new file mode 100644 index 0000000..5f8c2e7 --- /dev/null +++ b/art/exported/characters/faces/mouth/mouth_open_ah.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://44cqu18imydc" +path="res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/exported/characters/faces/mouth/mouth_open_ah.png" +dest_files=["res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.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/art/exported/characters/faces/mouth/mouth_open_oh.png b/art/exported/characters/faces/mouth/mouth_open_oh.png new file mode 100644 index 0000000..eba37ab Binary files /dev/null and b/art/exported/characters/faces/mouth/mouth_open_oh.png differ diff --git a/art/exported/characters/faces/mouth/mouth_open_oh.png.import b/art/exported/characters/faces/mouth/mouth_open_oh.png.import new file mode 100644 index 0000000..e4602aa --- /dev/null +++ b/art/exported/characters/faces/mouth/mouth_open_oh.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c78hwv0t3ph47" +path="res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/exported/characters/faces/mouth/mouth_open_oh.png" +dest_files=["res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.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/art/exported/characters/faces/mouth/mouth_open_smile.png b/art/exported/characters/faces/mouth/mouth_open_smile.png new file mode 100644 index 0000000..57675f8 Binary files /dev/null and b/art/exported/characters/faces/mouth/mouth_open_smile.png differ diff --git a/art/exported/characters/faces/mouth/mouth_open_smile.png.import b/art/exported/characters/faces/mouth/mouth_open_smile.png.import new file mode 100644 index 0000000..06e410b --- /dev/null +++ b/art/exported/characters/faces/mouth/mouth_open_smile.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b12jrc4ao3r16" +path="res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/exported/characters/faces/mouth/mouth_open_smile.png" +dest_files=["res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.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/art/patterns/pattern_moneyfish.png b/art/patterns/pattern_moneyfish.png index d223c44..6266f29 100644 Binary files a/art/patterns/pattern_moneyfish.png and b/art/patterns/pattern_moneyfish.png differ diff --git a/economy/fishing_shop_stock.gd b/economy/fishing_shop_stock.gd index e74232e..87795ad 100644 --- a/economy/fishing_shop_stock.gd +++ b/economy/fishing_shop_stock.gd @@ -14,8 +14,8 @@ const ITEM_PRICES: Dictionary[StringName, int] = { &"snails": 3, &"shrimp": 7, &"squid_chunks": 15, - &"whole_anchovy": 30, - &"whole_sardine": 60, + &"whole_anchovy": 60, + &"whole_sardine": 30, &"luminous_roe": 125, &"the_standby": 750, &"coffee": 20, @@ -28,8 +28,8 @@ const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = { &"snails": 400, &"shrimp": 1200, &"squid_chunks": 3500, - &"whole_anchovy": 9000, - &"whole_sardine": 22000, + &"whole_anchovy": 22000, + &"whole_sardine": 9000, &"luminous_roe": 50000, } const ITEM_ORDER: Array[StringName] = [ @@ -37,8 +37,8 @@ const ITEM_ORDER: Array[StringName] = [ &"snails", &"shrimp", &"squid_chunks", - &"whole_anchovy", &"whole_sardine", + &"whole_anchovy", &"luminous_roe", &"the_standby", FISH_FINDER_ID, diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd index 28ca93d..6ae4031 100644 --- a/fish/fish_quality.gd +++ b/fish/fish_quality.gd @@ -19,8 +19,8 @@ const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0] const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0] const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0] const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0] -const SARDINE_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] -const MINNOW_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] +const MINNOW_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] +const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] # Legacy profile multiplier retained for serialized/profile compatibility. @@ -158,9 +158,9 @@ static func rarity_weights_for_bait( elif active_bait_tags.has(&"bait_rarity_6"): weights.assign(LUMINOUS_ROE_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_5"): - weights.assign(MINNOW_ROLL_WEIGHTS) - elif active_bait_tags.has(&"bait_rarity_4"): weights.assign(SARDINE_ROLL_WEIGHTS) + elif active_bait_tags.has(&"bait_rarity_4"): + weights.assign(MINNOW_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_3"): weights.assign(SQUID_ROLL_WEIGHTS) elif active_bait_tags.has(&"bait_rarity_2"): diff --git a/items/catalog/item_catalog.tres b/items/catalog/item_catalog.tres index f8c0f5d..43377ba 100644 --- a/items/catalog/item_catalog.tres +++ b/items/catalog/item_catalog.tres @@ -20,4 +20,4 @@ [resource] script = ExtResource("1_script") -items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("13_anchovy"), ExtResource("14_sardine"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries")] +items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries")] diff --git a/items/catalog/luminous_roe.tres b/items/catalog/luminous_roe.tres index aae9fc6..7814f42 100644 --- a/items/catalog/luminous_roe.tres +++ b/items/catalog/luminous_roe.tres @@ -1,12 +1,14 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_roe_luminous.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"luminous_roe" display_name = "luminous roe" description = "rare glowing roe with the strongest rare catch odds." +icon = ExtResource("2_icon") category = 2 bait_tags = Array[StringName]([&"bait_rarity_6"]) stackable = true diff --git a/items/catalog/shrimp.tres b/items/catalog/shrimp.tres index a9a63d3..e308163 100644 --- a/items/catalog/shrimp.tres +++ b/items/catalog/shrimp.tres @@ -1,12 +1,14 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_shrimp.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"shrimp" display_name = "shrimp" description = "lively bait that noticeably improves rare catch chances." +icon = ExtResource("2_icon") category = 2 bait_tags = Array[StringName]([&"bait_rarity_2"]) stackable = true diff --git a/items/catalog/snails.tres b/items/catalog/snails.tres index 6bddbd4..12d4497 100644 --- a/items/catalog/snails.tres +++ b/items/catalog/snails.tres @@ -1,12 +1,14 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_snail.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"snails" display_name = "snails" description = "slow-moving bait with a better chance for rarer catches." +icon = ExtResource("2_icon") category = 2 bait_tags = Array[StringName]([&"bait_rarity_1"]) stackable = true diff --git a/items/catalog/squid_chunks.tres b/items/catalog/squid_chunks.tres index 108387b..dfe245f 100644 --- a/items/catalog/squid_chunks.tres +++ b/items/catalog/squid_chunks.tres @@ -1,12 +1,14 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_squid.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"squid_chunks" display_name = "squid chunks" description = "rich cut bait that strongly favors rarer catches." +icon = ExtResource("2_icon") category = 2 bait_tags = Array[StringName]([&"bait_rarity_3"]) stackable = true diff --git a/items/catalog/whole_anchovy.tres b/items/catalog/whole_anchovy.tres index 0aee243..cbc60b6 100644 --- a/items/catalog/whole_anchovy.tres +++ b/items/catalog/whole_anchovy.tres @@ -1,14 +1,16 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_sardine.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"whole_anchovy" display_name = "sardine" -description = "oily schooling bait with high rare catch potential." +description = "valuable oily schooling bait tuned for exceptional catches." +icon = ExtResource("2_icon") category = 2 -bait_tags = Array[StringName]([&"bait_rarity_4"]) +bait_tags = Array[StringName]([&"bait_rarity_5"]) stackable = true max_stack = 10 hotbar_allowed = false diff --git a/items/catalog/whole_sardine.tres b/items/catalog/whole_sardine.tres index d03ac81..c5d0163 100644 --- a/items/catalog/whole_sardine.tres +++ b/items/catalog/whole_sardine.tres @@ -1,14 +1,16 @@ -[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1"] +[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_minnow.png" id="2_icon"] [resource] script = ExtResource("1") item_id = &"whole_sardine" display_name = "minnow" -description = "valuable live bait tuned for exceptional catches." +description = "small live bait with high rare catch potential." +icon = ExtResource("2_icon") category = 2 -bait_tags = Array[StringName]([&"bait_rarity_5"]) +bait_tags = Array[StringName]([&"bait_rarity_4"]) stackable = true max_stack = 10 hotbar_allowed = false diff --git a/items/icons/bait/64_bait_minnow.png b/items/icons/bait/64_bait_minnow.png new file mode 100644 index 0000000..f083d65 Binary files /dev/null and b/items/icons/bait/64_bait_minnow.png differ diff --git a/items/icons/bait/64_bait_minnow.png.import b/items/icons/bait/64_bait_minnow.png.import new file mode 100644 index 0000000..67f57f5 --- /dev/null +++ b/items/icons/bait/64_bait_minnow.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://oc1nckfxeir0" +path="res://.godot/imported/64_bait_minnow.png-25bb645caffd0cad87b30d6b38b80ca4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_minnow.png" +dest_files=["res://.godot/imported/64_bait_minnow.png-25bb645caffd0cad87b30d6b38b80ca4.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/items/icons/bait/64_bait_roe_luminous.png b/items/icons/bait/64_bait_roe_luminous.png new file mode 100644 index 0000000..dc89fbd Binary files /dev/null and b/items/icons/bait/64_bait_roe_luminous.png differ diff --git a/items/icons/bait/64_bait_roe_luminous.png.import b/items/icons/bait/64_bait_roe_luminous.png.import new file mode 100644 index 0000000..b0a78d0 --- /dev/null +++ b/items/icons/bait/64_bait_roe_luminous.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bnqji6j6nigjq" +path="res://.godot/imported/64_bait_roe_luminous.png-cfa7eaf935630a20e2e8111f803c7741.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_roe_luminous.png" +dest_files=["res://.godot/imported/64_bait_roe_luminous.png-cfa7eaf935630a20e2e8111f803c7741.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/items/icons/bait/64_bait_sardine.png b/items/icons/bait/64_bait_sardine.png new file mode 100644 index 0000000..9161ba5 Binary files /dev/null and b/items/icons/bait/64_bait_sardine.png differ diff --git a/items/icons/bait/64_bait_sardine.png.import b/items/icons/bait/64_bait_sardine.png.import new file mode 100644 index 0000000..03f94bf --- /dev/null +++ b/items/icons/bait/64_bait_sardine.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddbyrbcbm40i4" +path="res://.godot/imported/64_bait_sardine.png-28b5d49b506c3007ebf15dcc8f198a94.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_sardine.png" +dest_files=["res://.godot/imported/64_bait_sardine.png-28b5d49b506c3007ebf15dcc8f198a94.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/items/icons/bait/64_bait_shrimp.png b/items/icons/bait/64_bait_shrimp.png new file mode 100644 index 0000000..8a20783 Binary files /dev/null and b/items/icons/bait/64_bait_shrimp.png differ diff --git a/items/icons/bait/64_bait_shrimp.png.import b/items/icons/bait/64_bait_shrimp.png.import new file mode 100644 index 0000000..8c65f4a --- /dev/null +++ b/items/icons/bait/64_bait_shrimp.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bpq47xg8tv6th" +path="res://.godot/imported/64_bait_shrimp.png-f68293fc8a81e803d5eeb1f268e4f3d3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_shrimp.png" +dest_files=["res://.godot/imported/64_bait_shrimp.png-f68293fc8a81e803d5eeb1f268e4f3d3.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/items/icons/bait/64_bait_snail.png b/items/icons/bait/64_bait_snail.png new file mode 100644 index 0000000..39e6c16 Binary files /dev/null and b/items/icons/bait/64_bait_snail.png differ diff --git a/items/icons/bait/64_bait_snail.png.import b/items/icons/bait/64_bait_snail.png.import new file mode 100644 index 0000000..de8d419 --- /dev/null +++ b/items/icons/bait/64_bait_snail.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dihevd23dh5x3" +path="res://.godot/imported/64_bait_snail.png-9a7a0c9f93be442a8a1d685a5324e3f6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_snail.png" +dest_files=["res://.godot/imported/64_bait_snail.png-9a7a0c9f93be442a8a1d685a5324e3f6.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/items/icons/bait/64_bait_squid.png b/items/icons/bait/64_bait_squid.png new file mode 100644 index 0000000..57e2b64 Binary files /dev/null and b/items/icons/bait/64_bait_squid.png differ diff --git a/items/icons/bait/64_bait_squid.png.import b/items/icons/bait/64_bait_squid.png.import new file mode 100644 index 0000000..41bf2b4 --- /dev/null +++ b/items/icons/bait/64_bait_squid.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kf3aglrd1en3" +path="res://.godot/imported/64_bait_squid.png-6ec6c9776ffcae93fd2fa312c8a9013d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/bait/64_bait_squid.png" +dest_files=["res://.godot/imported/64_bait_squid.png-6ec6c9776ffcae93fd2fa312c8a9013d.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/network/discovery_client.gd b/network/discovery_client.gd index e5a09c7..f2263e8 100644 --- a/network/discovery_client.gd +++ b/network/discovery_client.gd @@ -13,7 +13,12 @@ signal public_join_state_changed(state: int) const BASE_URL_SETTING: String = "network/discovery/base_url" const BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL" const SETTINGS_PATH: String = "user://network_discovery.cfg" -const DEFAULT_ROOM_NAME: String = "NETfishing Room" +const DEFAULT_ROOM_NAME: String = "Player's Server" +const LEGACY_DEFAULT_ROOM_NAME: String = "NETfishing Room" +const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = ( + "NETfishing Dedicated Server" +) +const ROOM_NAME_SUFFIX: String = "'s Server" const MAX_ROOM_NAME_LENGTH: int = 48 const HEARTBEAT_INTERVAL_SECONDS: float = 15.0 const REQUEST_TIMEOUT_SECONDS: float = 8.0 @@ -52,6 +57,7 @@ enum HostRequestKind { var _session: NetworkSession var _base_url: String = "" var _room_name: String = DEFAULT_ROOM_NAME +var _room_name_uses_default: bool = true var _discoverable: bool = false var _host_status_message: String = "" var _host_status_is_error: bool = false @@ -145,6 +151,9 @@ func _ready() -> void: func setup(session: NetworkSession) -> void: _session = session + if _room_name_uses_default: + _room_name = _default_room_name(_session.get_local_display_name()) + _save_settings() _session.set_session_display_name(_room_name) if not _session.state_changed.is_connected(_on_session_state_changed): _session.state_changed.connect(_on_session_state_changed) @@ -216,8 +225,12 @@ func set_room_name(value: String) -> bool: _set_host_status("Room name cannot be empty.", true) return false if cleaned == _room_name: + if _room_name_uses_default: + _room_name_uses_default = false + _save_settings() return true _room_name = cleaned + _room_name_uses_default = false _save_settings() if _session != null: _session.set_session_display_name(_room_name) @@ -278,6 +291,12 @@ func prepare_public_join(room: Dictionary) -> bool: if _session == null or not is_configured(): _set_public_join_state(PublicJoinState.ERROR) return false + if is_own_room(room): + _set_public_join_state(PublicJoinState.ERROR) + public_join_status_changed.emit( + "You are already hosting this room.", true + ) + return false var endpoint: String = room_endpoint(room) var room_id: String = str(room.get("room_id", "")).strip_edges() if endpoint.is_empty() or room_id.is_empty(): @@ -343,6 +362,13 @@ func room_endpoint(room: Dictionary) -> String: return "%s:%d" % [address, port] +func is_own_room(room: Dictionary) -> bool: + return ( + not _lease_room_id.is_empty() + and str(room.get("room_id", "")) == _lease_room_id + ) + + func _configured_base_url() -> String: var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges() if value.is_empty(): @@ -957,6 +983,16 @@ func _sanitize_room_name(value: String) -> String: return cleaned.left(MAX_ROOM_NAME_LENGTH) +func _default_room_name(player_name: String) -> String: + var cleaned_name: String = player_name.strip_edges() + if cleaned_name.is_empty(): + return DEFAULT_ROOM_NAME + return "%s%s" % [ + cleaned_name.left(MAX_ROOM_NAME_LENGTH - ROOM_NAME_SUFFIX.length()), + ROOM_NAME_SUFFIX, + ] + + func _load_settings() -> void: var config := ConfigFile.new() if config.load(SETTINGS_PATH) == OK: @@ -965,11 +1001,23 @@ func _load_settings() -> void: ) if not saved_name.is_empty(): _room_name = saved_name + _room_name_uses_default = bool(config.get_value( + "host", + "room_name_uses_default", + saved_name in [ + DEFAULT_ROOM_NAME, + LEGACY_DEFAULT_ROOM_NAME, + LEGACY_DEDICATED_DEFAULT_ROOM_NAME, + ], + )) func _save_settings() -> void: var config := ConfigFile.new() config.set_value("host", "room_name", _room_name) + config.set_value( + "host", "room_name_uses_default", _room_name_uses_default + ) var error: Error = config.save(SETTINGS_PATH) if error != OK: push_warning("Could not save the local NETfishing room name.") diff --git a/network/network_player_list_service.gd b/network/network_player_list_service.gd index 1a8136a..c4307f1 100644 --- a/network/network_player_list_service.gd +++ b/network/network_player_list_service.gd @@ -93,6 +93,10 @@ func is_open_host() -> bool: return _session.is_open_host() +func set_host_open(is_open: bool) -> bool: + return _session.set_host_open(is_open) + + func get_relationships() -> Array[Dictionary]: return _relationships.get_records() diff --git a/network/network_protocol.gd b/network/network_protocol.gd index cfe5a0b..b07d220 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -3,6 +3,7 @@ extends RefCounted const PROTOCOL_VERSION: int = 3 const GAME_BUILD: String = "prealpha" +const MAX_GAME_VERSION_LENGTH: int = 64 const MAX_DISPLAY_NAME_LENGTH: int = 24 const MAX_PROFILE_ID_LENGTH: int = 96 const MAX_NONCE_LENGTH: int = 96 @@ -37,9 +38,80 @@ enum RejectionCode { SERVER_SHUTTING_DOWN, UNSUPPORTED_CLIENT, BANNED, + CLIENT_OUTDATED, + SERVER_OUTDATED, + VERSION_MISMATCH, } +static func game_version() -> String: + return str(ProjectSettings.get_setting( + "application/config/version", "unknown" + )).strip_edges() + + +static func game_version_rejection( + client_version: String, + server_version: String, +) -> RejectionCode: + if client_version == server_version: + return RejectionCode.NONE + var comparison: int = _compare_game_versions( + client_version, server_version + ) + if comparison < 0: + return RejectionCode.CLIENT_OUTDATED + if comparison > 0: + return RejectionCode.SERVER_OUTDATED + return RejectionCode.VERSION_MISMATCH + + +static func _compare_game_versions(first: String, second: String) -> int: + var first_parts: PackedStringArray = first.split("-", true, 1) + var second_parts: PackedStringArray = second.split("-", true, 1) + var first_core: PackedStringArray = first_parts[0].split(".") + var second_core: PackedStringArray = second_parts[0].split(".") + for index: int in maxi(first_core.size(), second_core.size()): + var first_number: int = ( + int(first_core[index]) + if index < first_core.size() and first_core[index].is_valid_int() + else 0 + ) + var second_number: int = ( + int(second_core[index]) + if index < second_core.size() and second_core[index].is_valid_int() + else 0 + ) + if first_number != second_number: + return -1 if first_number < second_number else 1 + var first_rank: int = _prerelease_rank( + first_parts[1] if first_parts.size() > 1 else "" + ) + var second_rank: int = _prerelease_rank( + second_parts[1] if second_parts.size() > 1 else "" + ) + if first_rank != second_rank: + return -1 if first_rank < second_rank else 1 + return 0 + + +static func _prerelease_rank(label: String) -> int: + var normalized: String = label.to_lower() + if normalized.is_empty(): + return 5 + if normalized.begins_with("dev"): + return 0 + if normalized.begins_with("prealpha"): + return 1 + if normalized.begins_with("alpha"): + return 2 + if normalized.begins_with("beta"): + return 3 + if normalized.begins_with("rc"): + return 4 + return 2 + + static func make_identity_hello( public_key: String, fingerprint: String, @@ -48,6 +120,7 @@ static func make_identity_hello( ) -> Dictionary: return { "protocol_version": PROTOCOL_VERSION, + "game_version": game_version(), "public_key": public_key, "fingerprint": fingerprint, "client_nonce": client_nonce, @@ -62,6 +135,9 @@ static func validate_identity_hello(data: Variant) -> bool: var value: Dictionary = data return ( typeof(value.get("protocol_version")) == TYPE_INT + and typeof(value.get("game_version")) == TYPE_STRING + and not str(value["game_version"]).is_empty() + and str(value["game_version"]).length() <= MAX_GAME_VERSION_LENGTH and typeof(value.get("public_key")) == TYPE_STRING and str(value["public_key"]).to_utf8_buffer().size() <= MAX_PUBLIC_KEY_LENGTH and NetworkIdentityCrypto.valid_fingerprint(value.get("fingerprint")) @@ -85,6 +161,7 @@ static func make_client_hello( ) -> Dictionary: return { "protocol_version": PROTOCOL_VERSION, + "game_version": game_version(), "game_build": GAME_BUILD, "local_profile_id": profile_id, "display_name": display_name, @@ -108,6 +185,7 @@ static func validate_client_hello(data: Variant) -> String: var payload: Dictionary = data for key: String in [ "protocol_version", + "game_version", "game_build", "local_profile_id", "display_name", @@ -121,6 +199,12 @@ static func validate_client_hello(data: Variant) -> String: return "Handshake is missing %s." % key if typeof(payload["protocol_version"]) != TYPE_INT: return "Protocol version is invalid." + if ( + typeof(payload["game_version"]) != TYPE_STRING + or str(payload["game_version"]).is_empty() + or str(payload["game_version"]).length() > MAX_GAME_VERSION_LENGTH + ): + return "Game version is invalid." if typeof(payload["game_build"]) != TYPE_STRING: return "Game build is invalid." if typeof(payload["local_profile_id"]) != TYPE_STRING: @@ -171,6 +255,7 @@ static func validate_client_hello(data: Variant) -> String: static func client_profile_fields(data: Dictionary) -> Array: var appearance: Dictionary = data.get("cosmetic_snapshot", {}) return [ + str(data.get("game_version", "")), str(data.get("client_nonce", "")), str(data.get("identity_fingerprint", "")), str(data.get("local_profile_id", "")), @@ -204,6 +289,7 @@ static func make_server_hello( "accepted": accepted, "rejection_code": int(rejection_code), "protocol_version": PROTOCOL_VERSION, + "game_version": game_version(), "session_id": session_id, "assigned_peer_id": assigned_peer_id, "server_display_name": server_display_name, @@ -251,5 +337,20 @@ static func rejection_text(code: int) -> String: return "This game build is not supported by the server." RejectionCode.BANNED: return "You are not permitted to join this server." + RejectionCode.CLIENT_OUTDATED: + return ( + "Your NETfishing version is out of date. " + + "Update the game to join this server." + ) + RejectionCode.SERVER_OUTDATED: + return ( + "This server is out of date. " + + "The host needs to update NETfishing." + ) + RejectionCode.VERSION_MISMATCH: + return ( + "This game and server use different NETfishing versions. " + + "Update both to the latest release." + ) _: return "The server rejected the connection." diff --git a/network/network_session.gd b/network/network_session.gd index 3ad3892..1d332da 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -315,6 +315,10 @@ func get_session_display_name() -> String: return _session_display_name +func get_local_display_name() -> String: + return _profile.display_name if _profile != null else "" + + static func _can_bind_udp_port(port: int, bind_address: String = "*") -> bool: var probe := PacketPeerUDP.new() var error: Error = probe.bind(port, bind_address) @@ -838,6 +842,16 @@ func submit_identity_hello(data: Dictionary) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if not is_host() or sender_id <= 1 or not _pending_authentication.has(sender_id): return + var client_game_version: String = str(data.get("game_version", "")) + var version_rejection: NetworkProtocol.RejectionCode = ( + NetworkProtocol.game_version_rejection( + client_game_version, + NetworkProtocol.game_version(), + ) + ) + if version_rejection != NetworkProtocol.RejectionCode.NONE: + _reject_peer(sender_id, version_rejection) + return if ( not NetworkProtocol.validate_identity_hello(data) or int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION @@ -868,8 +882,10 @@ func submit_identity_hello(data: Dictionary) -> void: "client_nonce": str(data["client_nonce"]), "client_fingerprint": fingerprint, "client_public_key": public_pem, + "client_game_version": client_game_version, "server_nonce": server_nonce, "server_fingerprint": _host_identity.fingerprint, + "server_game_version": NetworkProtocol.game_version(), "session_id": _session_id, "generation": _operation_generation, "expires_at_msec": Time.get_ticks_msec() + 60000, @@ -886,7 +902,25 @@ func submit_identity_hello(data: Dictionary) -> void: @rpc("authority", "call_remote", "reliable", 0) func receive_server_identity_proof(data: Dictionary) -> void: - if state != State.AUTHENTICATING or not _valid_server_proof_shape(data): + if state != State.AUTHENTICATING: + return + var server_game_version: String = str(data.get("server_game_version", "")) + if server_game_version.is_empty(): + _fail_identity(NetworkProtocol.rejection_text( + NetworkProtocol.RejectionCode.SERVER_OUTDATED + )) + return + var version_rejection: NetworkProtocol.RejectionCode = ( + NetworkProtocol.game_version_rejection( + NetworkProtocol.game_version(), + server_game_version, + ) + ) + if version_rejection != NetworkProtocol.RejectionCode.NONE: + _fail_identity(NetworkProtocol.rejection_text(version_rejection)) + return + if not _valid_server_proof_shape(data): + _fail_identity("The server sent an invalid identity proof.") return if ( str(data["attempt_id"]) != str(_client_identity_attempt.get("attempt_id", "")) @@ -995,6 +1029,7 @@ func submit_client_identity_proof(data: Dictionary) -> void: _authenticated_identity_cache[sender_id] = { "fingerprint": challenge["client_fingerprint"], "public_key": challenge["client_public_key"], + "game_version": challenge["client_game_version"], } if ( _host_bans != null @@ -1041,6 +1076,16 @@ func submit_client_hello(data: Dictionary) -> void: if not validation_error.is_empty(): _reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE) return + var client_game_version: String = str(data.get("game_version", "")) + var version_rejection: NetworkProtocol.RejectionCode = ( + NetworkProtocol.game_version_rejection( + client_game_version, + NetworkProtocol.game_version(), + ) + ) + if version_rejection != NetworkProtocol.RejectionCode.NONE: + _reject_peer(sender_id, version_rejection) + return var client_capabilities: PackedStringArray = _sanitized_capabilities( data.get("capability_flags", []) ) @@ -1056,6 +1101,7 @@ func submit_client_hello(data: Dictionary) -> void: return if ( data["identity_fingerprint"] != identity["fingerprint"] + or client_game_version != str(identity.get("game_version", "")) or not NetworkIdentityCrypto.verify_fields( NetworkIdentityCrypto.load_public_key(identity["public_key"]), "handshake_client_profile", @@ -1179,6 +1225,7 @@ func receive_server_hello(data: Dictionary) -> void: if ( typeof(data.get("accepted")) != TYPE_BOOL or typeof(data.get("protocol_version")) != TYPE_INT + or typeof(data.get("game_version")) != TYPE_STRING or typeof(data.get("rejection_code")) != TYPE_INT ): _teardown_peer() @@ -1191,6 +1238,16 @@ func receive_server_hello(data: Dictionary) -> void: _teardown_peer() _fail(message) return + var version_rejection: NetworkProtocol.RejectionCode = ( + NetworkProtocol.game_version_rejection( + NetworkProtocol.game_version(), + str(data["game_version"]), + ) + ) + if version_rejection != NetworkProtocol.RejectionCode.NONE: + _teardown_peer() + _fail(NetworkProtocol.rejection_text(version_rejection)) + return if int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION: _teardown_peer() _fail("The server uses a different network protocol.") @@ -1457,6 +1514,8 @@ func _verify_spawn_identity(entry: Dictionary) -> bool: func _identity_proof_fields(data: Dictionary) -> Array: return [ NetworkProtocol.PROTOCOL_VERSION, + str(data.get("client_game_version", "")), + str(data.get("server_game_version", "")), str(data.get("attempt_id", "")), str(data.get("client_fingerprint", "")), str(data.get("client_nonce", "")), @@ -1497,6 +1556,10 @@ func _valid_server_proof_shape(data: Variant) -> bool: and str(value["server_nonce"]).length() == 64 and NetworkIdentityCrypto.valid_fingerprint(value.get("client_fingerprint")) and NetworkIdentityCrypto.valid_fingerprint(value.get("server_fingerprint")) + and typeof(value.get("client_game_version")) == TYPE_STRING + and str(value["client_game_version"]) == NetworkProtocol.game_version() + and typeof(value.get("server_game_version")) == TYPE_STRING + and str(value["server_game_version"]) == NetworkProtocol.game_version() and typeof(value.get("server_public_key")) == TYPE_STRING and str(value["server_public_key"]).to_utf8_buffer().size() <= NetworkProtocol.MAX_PUBLIC_KEY_LENGTH diff --git a/player/player.gd b/player/player.gd index b9b4133..733be49 100644 --- a/player/player.gd +++ b/player/player.gd @@ -84,6 +84,8 @@ enum PocketVisualTarget { CATCH_SHOWCASE, } const FIGHTING_EYES_ID: String = "alligator_eyes" +const CHARACTER_CALL_MOUTH_ID: String = "open_ah" +const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16 const BASE_REEL_SPEED: float = 0.16 # The target Android handheld exposes its physical right trigger through # Godot's left-trigger axis. Keep the role named here so the platform mapping @@ -158,6 +160,20 @@ func set_fighting_visual(active: bool) -> void: _update_character_animation() +func play_character_call_visual() -> void: + _character_call_visual_generation += 1 + var generation: int = _character_call_visual_generation + _character_call_mouth_active = true + _apply_presented_appearance() + await get_tree().create_timer( + CHARACTER_CALL_MOUTH_DURATION_SECONDS + ).timeout + if generation != _character_call_visual_generation: + return + _character_call_mouth_active = false + _apply_presented_appearance() + + func set_fishing_visual(active: bool) -> void: if ( active @@ -223,6 +239,10 @@ func _apply_presented_appearance() -> void: if _fighting_visual_active: presented_appearance = appearance_snapshot.duplicate(true) presented_appearance["eyes"] = FIGHTING_EYES_ID + if _character_call_mouth_active: + if presented_appearance == appearance_snapshot: + presented_appearance = appearance_snapshot.duplicate(true) + presented_appearance["mouth"] = CHARACTER_CALL_MOUTH_ID PlayerVisualPresenter.apply_appearance(_visuals, presented_appearance) @@ -382,6 +402,8 @@ var _pocket_visual_midpoint_callback: Callable var _pocket_visual_midpoint_called: bool = false var _pocket_visual_generation: int = 0 var _fighting_visual_active: bool = false +var _character_call_mouth_active: bool = false +var _character_call_visual_generation: int = 0 var _fishing_visual_active: bool = false var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE var _fishing_after_release_pending: bool = false diff --git a/tests/dedicated_server_config_validation.gd b/tests/dedicated_server_config_validation.gd index 888746f..7911f6d 100644 --- a/tests/dedicated_server_config_validation.gd +++ b/tests/dedicated_server_config_validation.gd @@ -46,6 +46,10 @@ func _run() -> void: assert(not invalid.is_valid()) var discovery := DiscoveryClient.new() + assert( + str(discovery.call("_default_room_name", "River")) + == "River's Server" + ) assert( DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS < float(DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS) @@ -62,7 +66,12 @@ func _run() -> void: discovery.get_public_join_state() == DiscoveryClient.PublicJoinState.IDLE ) - assert(bool(discovery.call("_valid_public_room", { + discovery.set("_lease_room_id", "local-room") + assert(discovery.is_own_room({"room_id": "local-room"})) + assert(not discovery.is_own_room({"room_id": "another-room"})) + discovery.set("_lease_room_id", "") + assert(not discovery.is_own_room({"room_id": "local-room"})) + var listed_room := { "room_id": "empty-dedicated-room", "room_name": "Empty Dedicated Room", "address": "203.0.113.10", @@ -73,7 +82,31 @@ func _run() -> void: "application/config/version", "unknown" )), "protocol_version": NetworkProtocol.PROTOCOL_VERSION, - }))) + } + assert(bool(discovery.call("_valid_public_room", listed_room))) + var incompatible_room: Dictionary = listed_room.duplicate(true) + incompatible_room["game_version"] = "0.0.0-alpha" + assert(not bool(discovery.call("_valid_public_room", incompatible_room))) + assert( + NetworkProtocol.game_version_rejection( + "0.6.4-alpha", "0.6.5-alpha" + ) == NetworkProtocol.RejectionCode.CLIENT_OUTDATED + ) + assert( + NetworkProtocol.game_version_rejection( + "0.6.6-alpha", "0.6.5-alpha" + ) == NetworkProtocol.RejectionCode.SERVER_OUTDATED + ) + assert( + NetworkProtocol.game_version_rejection( + "0.6.5-alpha", "0.6.5-alpha" + ) == NetworkProtocol.RejectionCode.NONE + ) + assert( + NetworkProtocol.game_version_rejection( + "0.6.5-beta", "0.6.5-alpha" + ) == NetworkProtocol.RejectionCode.SERVER_OUTDATED + ) discovery.free() print("DEDICATED_SERVER_CONFIG_VALIDATION_OK") quit() diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index 9b40e74..f7d0d12 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -552,9 +552,11 @@ func _test_fishing_shop_sale_ui( var shop_panel_style := shop_panel.get_theme_stylebox("panel") as StyleBoxFlat assert(shop_panel_style != null) assert(shop_panel_style.corner_radius_top_left == 24) - var buy_mode := shop.get_node("%BuyModeButton") as Button - var sell_mode := shop.get_node("%SellModeButton") as Button - assert(buy_mode.visible and sell_mode.visible) + assert(not shop.has_node("ShopPanel/Margin/Layout/ModeTabs")) + var shop_tabs: Array = shop.get("_shop_tabs") as Array + assert(shop_tabs.size() == 6) + var sell_mode := shop_tabs[5] as Button + assert(sell_mode != null and sell_mode.text == "Sell Fish") var stock_sections: Array[String] = [] for child: Node in shop.get_node("%SuppliesList").get_children(): if child is Label: @@ -605,8 +607,7 @@ func _test_fishing_shop_sale_ui( assert(player.wallet.get_balance() == balance_before + fish_catch.sale_value) assert(not sale_service.is_local_sale_pending()) assert(reservations.release(reservation_id)) - var back_button := shop.get_node("%BackToShopButton") as Button - await _activate_focused_button(back_button, ui_viewport) + await _activate_focused_button(shop_tabs[0] as Button, ui_viewport) await process_frame assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible) assert(not (shop.get_node("%ShopCoolerPage") as Control).visible) diff --git a/tests/exported_decal_hotfix_validation.gd b/tests/exported_decal_hotfix_validation.gd index be70325..fe279b0 100644 --- a/tests/exported_decal_hotfix_validation.gd +++ b/tests/exported_decal_hotfix_validation.gd @@ -1,5 +1,11 @@ extends SceneTree +const FEATURE_ASSET_ROOTS: Dictionary = { + "eyes": "res://art/exported/characters/faces/eyes", + "mouth": "res://art/exported/characters/faces/mouth", + "nose": "res://art/exported/characters/faces/noses", +} + func _initialize() -> void: call_deferred("_run") @@ -27,10 +33,10 @@ func _run() -> void: ).is_empty() ) - var expected_counts := {"eyes": 22, "mouth": 17, "nose": 17} - for category_id: String in expected_counts: + for category_id: String in FEATURE_ASSET_ROOTS: var options := CharacterCustomizationCatalog.options_for(category_id) - assert(options.size() == int(expected_counts[category_id]) + 1) + var expected_ids: Dictionary = _feature_ids_on_disk(category_id) + assert(options.size() == expected_ids.size() + 1) var seen_ids: Dictionary = {} for option: Dictionary in options: var option_id := str(option.get("id", "")) @@ -42,6 +48,30 @@ func _run() -> void: category_id, option_id ) != null ) + for expected_id: String in expected_ids: + assert(seen_ids.has(expected_id)) + for mouth_id: String in ["open_ah", "open_oh", "open_smile"]: + assert(CharacterCustomizationCatalog.texture_for( + "mouth", mouth_id + ) != null) print("Exported decal hotfix validation: PASS") quit() + + +func _feature_ids_on_disk(category_id: String) -> Dictionary: + var result: Dictionary = {} + var root_path: String = str(FEATURE_ASSET_ROOTS[category_id]) + var directory := DirAccess.open(root_path) + assert(directory != null) + for file_name: String in directory.get_files(): + if not file_name.to_lower().ends_with(".png"): + continue + var option_id: String = ( + CharacterCustomizationCatalog._feature_id_from_filename( + category_id, file_name + ) + ) + if not option_id.is_empty(): + result[option_id] = true + return result diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd index 24cad9d..6061a31 100644 --- a/tests/job_system_validation.gd +++ b/tests/job_system_validation.gd @@ -166,6 +166,9 @@ func _run() -> void: var session_summary := ( join_page.get_node("%SessionSummary") as Label ) + assert(not join_page.has_node( + "Paper/Margin/Layout/Actions/OpenCloseButton" + )) assert("UDP 18138" in session_summary.text) assert(session.set_host_open(true)) await process_frame diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 34923ec..f50f6e6 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -7,7 +7,7 @@ const SPEECH_SECONDS: float = 6.0 const DRAFT_SAVE_DELAY: float = 0.4 const IDLE_ALPHA: float = 0.58 const CHAT_SURFACE_COLOR := Color(0.025, 0.13, 0.19, 0.94) -const PANEL_WIDTH: float = 270.0 +const PANEL_WIDTH: float = 290.0 const COMPACT_HEIGHT: float = 250.0 const BOTTOM_MARGIN: float = 12.0 const EXPANDED_TOP_MARGIN: float = 94.0 @@ -31,9 +31,10 @@ const MOBILE_EXPANDED_WIDTH: float = 820.0 const MOBILE_COMPACT_HEIGHT: float = 220.0 const MOBILE_EXPANDED_HEIGHT: float = 390.0 const MOBILE_EDGE_MARGIN: float = 12.0 -const CLOCK_SIZE := Vector2(116.0, 34.0) +const CLOCK_SIZE := Vector2(174.0, 51.0) +const CLOCK_FONT_SIZE: int = 27 const CLOCK_EDGE_MARGIN: float = 10.0 -const WEATHER_ICON_SIZE := Vector2(34.0, 34.0) +const WEATHER_ICON_SIZE := Vector2(51.0, 51.0) const WEATHER_ICON_GAP: float = 6.0 const FISH_FINDER_EFFECT_ICON_SIZE := Vector2(18.0, 18.0) const FISH_FINDER_EFFECT_ICON: Texture2D = preload( @@ -383,7 +384,7 @@ func _build_ui() -> void: _clock_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _clock_label.mouse_filter = Control.MOUSE_FILTER_IGNORE _clock_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont) - _clock_label.add_theme_font_size_override("font_size", 18) + _clock_label.add_theme_font_size_override("font_size", CLOCK_FONT_SIZE) _clock_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 8e776bb..9f93cf8 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -26,8 +26,13 @@ const ShopInteractionType = preload( ) const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") +const UIMotionType = preload("res://ui/ui_motion.gd") +const FALLBACK_SUPPLY_ICON: Texture2D = preload( + "res://ui/icons/pictograms/x_light.png" +) signal menu_visibility_changed(is_open: bool) +signal menu_exit_started signal sell_fish_requested signal shop_cooler_return_requested signal shop_cooler_confirmation_cancel_requested @@ -46,6 +51,7 @@ enum ShopSection { SNACKS, EQUIPMENT, ART_SUPPLIES, + SELL_FISH, } const SHOP_SECTION_LABELS: Array[String] = [ @@ -54,15 +60,23 @@ const SHOP_SECTION_LABELS: Array[String] = [ "Snacks", "Equipment", "Art Supplies", + "Sell Fish", ] +const SUPPLY_ICON_GRID_COLUMNS: int = 9 +const SUPPLY_ICON_TILE_SIZE := Vector2(72.0, 72.0) +const SUPPLY_ICON_HOST_SIZE := Vector2(72.0, 84.0) +const SUPPLY_ICON_GRID_SEPARATION: int = 8 +const SUPPLY_PRICE_COLOR := Color("c3dfe6") +const SUPPLY_PRICE_FONT_SIZE: int = 15 +const SUPPLY_PRICE_Y: float = 60.0 +const SUPPLY_PRICE_HEIGHT: float = 24.0 +const SUPPLY_PRICE_HORIZONTAL_PADDING: float = 12.0 @onready var _wallet_label: Label = %WalletLabel @onready var _shop_panel: PanelContainer = %ShopPanel @onready var _shop_cooler_page: Control = %ShopCoolerPage @onready var _shop_cooler_mount: Control = %ShopCoolerMount -@onready var _back_to_shop_button: Button = %BackToShopButton -@onready var _buy_mode_button: Button = %BuyModeButton -@onready var _sell_mode_button: Button = %SellModeButton +@onready var _shop_body: HBoxContainer = $ShopPanel/Margin/Layout/Body @onready var _feedback: Label = %Feedback @onready var _shop_tab_bar: HBoxContainer = %ShopTabBar @onready var _upgrades_content: VBoxContainer = %Upgrades @@ -100,6 +114,8 @@ var _snapshot_stored: bool = false var _mouse_snapshot_stored: bool = false var _transaction_in_progress: bool = false var _closing: bool = false +var _close_generation: int = 0 +var _close_tween: Tween var _cooler_page_active: bool = false var _cooler_modal_open: bool = false var _shop_section: ShopSection = ShopSection.UPGRADES @@ -111,46 +127,57 @@ func _ready() -> void: _apply_shop_styles() _build_shop_tabs() %CloseButton.pressed.connect(close_shop) - _buy_mode_button.pressed.connect(_focus_buy_page) - _sell_mode_button.pressed.connect(_request_shop_cooler) - _back_to_shop_button.pressed.connect(shop_cooler_return_requested.emit) _reel_purchase.pressed.connect(_purchase_reel_speed) _barrier_purchase.pressed.connect(_purchase_barrier_power) _cooler_purchase.pressed.connect(_purchase_cooler_capacity) -func _request_shop_cooler() -> void: +func _request_shop_cooler() -> bool: if _transaction_in_progress: _feedback.text = "Finish the current transaction first." - return + return false if not _is_transaction_context_valid(): _feedback.text = "The fishing shop is no longer available." - return + return false sell_fish_requested.emit() + return _cooler_page_active -func _focus_buy_page() -> void: +func _focus_shop_section() -> void: _feedback.text = "" if _shop_section == ShopSection.UPGRADES: _reel_purchase.grab_focus() return - for child: Node in _supplies_list.get_children(): + if _shop_section == ShopSection.SELL_FISH: + _shop_tabs[int(ShopSection.SELL_FISH)].grab_focus() + return + for child: Node in _supplies_list.find_children( + "*", "Button", true, false + ): var stock_button := child as Button if stock_button != null and not stock_button.disabled: stock_button.grab_focus() return - _buy_mode_button.grab_focus() + var section_index: int = int(_shop_section) + if section_index >= 0 and section_index < _shop_tabs.size(): + _shop_tabs[section_index].grab_focus() func _build_shop_tabs() -> void: _shop_tab_bar.custom_minimum_size.y = 44.0 _shop_tab_bar.show() for section_index: int in range(ShopSection.size()): + if section_index == ShopSection.SELL_FISH: + var tab_group_spacer := Control.new() + tab_group_spacer.name = "SellTabSpacer" + tab_group_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tab_group_spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE + _shop_tab_bar.add_child(tab_group_spacer) var tab: OrganizerTab = OrganizerTabType.new() tab.text = SHOP_SECTION_LABELS[section_index] tab.palette_index = section_index tab.custom_minimum_size = Vector2(132.0, 42.0) - tab.size_flags_horizontal = Control.SIZE_EXPAND_FILL + tab.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN tab.focus_mode = Control.FOCUS_ALL tab.mouse_filter = Control.MOUSE_FILTER_STOP tab.pressed.connect(_select_shop_section.bind(section_index, true)) @@ -161,15 +188,38 @@ func _build_shop_tabs() -> void: func _select_shop_section(section_index: int, focus_content: bool) -> void: + if _closing: + return + if section_index == int(_shop_section): + _update_shop_tab_selection() + if focus_content: + _focus_shop_section() + return + if section_index == ShopSection.SELL_FISH: + if not _request_shop_cooler(): + _update_shop_tab_selection() + return + _shop_section = ShopSection.SELL_FISH + _update_shop_tab_selection() + return + if _cooler_page_active: + shop_cooler_return_requested.emit() _shop_section = section_index as ShopSection var showing_upgrades: bool = _shop_section == ShopSection.UPGRADES _upgrades_content.visible = showing_upgrades _supplies_content.visible = not showing_upgrades - for tab_index: int in range(_shop_tabs.size()): - _shop_tabs[tab_index].set_selected(tab_index == section_index, true) + _update_shop_tab_selection() _refresh_supplies() if focus_content and visible: - _focus_buy_page() + _focus_shop_section() + + +func _update_shop_tab_selection() -> void: + for tab_index: int in range(_shop_tabs.size()): + _shop_tabs[tab_index].set_selected( + tab_index == int(_shop_section), + true, + ) func _apply_shop_styles() -> void: @@ -193,20 +243,11 @@ func _apply_shop_styles() -> void: ) for button: BaseButton in [ %CloseButton, - _buy_mode_button, - _sell_mode_button, _reel_purchase, _barrier_purchase, _cooler_purchase, - _back_to_shop_button, ]: UtilityPageStyleType.apply_ocean_button(button) - _buy_mode_button.add_theme_stylebox_override( - "normal", - UtilityPageStyleType.ocean_button_style( - UtilityPageStyleType.OCEAN_SELECTED - ), - ) _feedback.add_theme_color_override( "font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY ) @@ -276,7 +317,10 @@ func open_shop() -> bool: or _buyer.id != MAIN_SHOP_BUYER_ID ): return false + _cancel_close_tween() + _close_generation += 1 _closing = false + modulate.a = 1.0 _transaction_in_progress = ( _network_shop != null and _network_shop.is_local_purchase_pending() @@ -296,7 +340,7 @@ func open_shop() -> bool: _shop_tab_bar.show() _select_shop_section(ShopSection.UPGRADES, false) _refresh_all() - _buy_mode_button.grab_focus() + _reel_purchase.grab_focus() menu_visibility_changed.emit(true) return true @@ -304,11 +348,13 @@ func open_shop() -> bool: func consume_escape() -> bool: if not visible: return false + if _closing: + return true if _cooler_page_active: if _cooler_modal_open: shop_cooler_confirmation_cancel_requested.emit() else: - shop_cooler_return_requested.emit() + _select_shop_section(ShopSection.UPGRADES, true) return true close_shop() return true @@ -319,11 +365,11 @@ func get_shop_cooler_mount() -> Control: func activate_shop_cooler_page() -> void: - _shop_tab_bar.hide() _cooler_page_active = true - _shop_panel.hide() + _shop_panel.show() + _shop_body.hide() + _feedback.hide() _shop_cooler_page.show() - _back_to_shop_button.disabled = false func deactivate_shop_cooler_page() -> void: @@ -331,16 +377,18 @@ func deactivate_shop_cooler_page() -> void: _shop_tab_bar.show() _cooler_page_active = false _cooler_modal_open = false - _back_to_shop_button.disabled = false + for tab: OrganizerTab in _shop_tabs: + tab.disabled = false _shop_cooler_page.hide() _shop_panel.show() - if visible: - _buy_mode_button.grab_focus() + _shop_body.show() + _feedback.show() func set_shop_cooler_modal_open(is_open: bool) -> void: _cooler_modal_open = is_open - _back_to_shop_button.disabled = is_open + for tab: OrganizerTab in _shop_tabs: + tab.disabled = is_open func close_shop( @@ -349,12 +397,47 @@ func close_shop( ) -> void: if not visible: return + if _closing: + if reason in [ + CloseReason.WATER_RECOVERY, + CloseReason.SESSION_END, + CloseReason.TEARDOWN, + ]: + _finish_close(reason, restore_controls, _close_generation) + return _closing = true _transaction_in_progress = false - hide() var current_viewport: Viewport = get_viewport() if current_viewport != null: current_viewport.gui_release_focus() + menu_exit_started.emit() + _close_generation += 1 + var generation: int = _close_generation + if reason in [CloseReason.USER, CloseReason.RANGE_EXIT]: + _close_tween = create_tween() + _close_tween.tween_property( + self, + "modulate:a", + 0.0, + UIMotionType.PLAYER_MENU_EXIT_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + _close_tween.finished.connect( + _finish_close.bind(reason, restore_controls, generation) + ) + return + _finish_close(reason, restore_controls, generation) + + +func _finish_close( + reason: CloseReason, + restore_controls: bool, + generation: int, +) -> void: + if generation != _close_generation or not visible: + return + _cancel_close_tween() + hide() + modulate.a = 1.0 if _fishing_spot != null and is_instance_valid(_fishing_spot): _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) if ( @@ -372,6 +455,13 @@ func close_shop( deactivate_shop_cooler_page() +func _cancel_close_tween() -> void: + if _close_tween == null: + return + _close_tween.kill() + _close_tween = null + + func close_for_range_exit() -> void: close_shop(CloseReason.RANGE_EXIT) @@ -404,9 +494,9 @@ func _refresh_all() -> void: func _refresh_wallet() -> void: _wallet_label.text = ( - "wallet: $%d" % _wallet.get_balance() + str(_wallet.get_balance()) if _wallet != null - else "wallet: $0" + else "0" ) @@ -475,7 +565,7 @@ func _refresh_supplies() -> void: for child: Node in _supplies_list.get_children(): _supplies_list.remove_child(child) child.queue_free() - if _shop_section == ShopSection.UPGRADES: + if _shop_section in [ShopSection.UPGRADES, ShopSection.SELL_FISH]: return _stock_title.text = SHOP_SECTION_LABELS[int(_shop_section)] var stock_item_ids: Array[StringName] = ( @@ -493,6 +583,9 @@ func _refresh_supplies() -> void: grouped_item_ids.append(item_id) stock_item_ids = grouped_item_ids var current_stock_group: StringName = StringName() + var current_stock_grid: GridContainer + if _shop_section in [ShopSection.SNACKS, ShopSection.EQUIPMENT]: + current_stock_grid = _add_stock_icon_grid() for item_id: StringName in stock_item_ids: var item: ItemDataType = _item_catalog.get_item_by_id(item_id) if item == null or not _item_belongs_in_current_section(item): @@ -503,10 +596,11 @@ func _refresh_supplies() -> void: ) if stock_group != current_stock_group: _add_stock_section(str(stock_group)) + current_stock_grid = _add_stock_icon_grid() current_stock_group = stock_group var button := Button.new() button.custom_minimum_size = Vector2(195, 54) - button.icon = item.icon + button.icon = item.icon if item.icon != null else FALLBACK_SUPPLY_ICON button.expand_icon = true button.alignment = HORIZONTAL_ALIGNMENT_LEFT var owned: int = _bag.get_quantity(item_id) @@ -520,6 +614,9 @@ func _refresh_supplies() -> void: var total_cost: int = FishingShopStockType.get_purchase_cost( item_id, quantity, bait_unlocked ) + var use_icon_tile: bool = ( + current_stock_grid != null + ) button.text = "%s\n$%d • owned %d" % [ item.display_name, FishingShopStockType.get_price(item_id), @@ -527,8 +624,8 @@ func _refresh_supplies() -> void: ] var item_tooltip_text: String = item.description if bait_topoff: - if item.icon != null: - button.custom_minimum_size = Vector2(72, 72) + if use_icon_tile: + button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN button.alignment = HORIZONTAL_ALIGNMENT_CENTER button.text = "" @@ -560,6 +657,17 @@ func _refresh_supplies() -> void: item.description, ] ) + elif use_icon_tile: + button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE + button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + button.alignment = HORIZONTAL_ALIGNMENT_CENTER + button.text = "" + item_tooltip_text = "%s\n$%d • owned %d\n%s" % [ + item.display_name, + FishingShopStockType.get_price(item_id), + owned, + item.description, + ] button.disabled = ( _transaction_in_progress or _closing @@ -577,19 +685,30 @@ func _refresh_supplies() -> void: ) UtilityPageStyleType.apply_ocean_button(button) button.pressed.connect(_purchase_supply.bind(item_id)) - _supplies_list.add_child(button) + if current_stock_grid != null: + _add_supply_icon_tile( + current_stock_grid, + button, + FishingShopStockType.get_price(item_id), + ) + else: + _supplies_list.add_child(button) if _shop_section == ShopSection.ART_SUPPLIES: _add_stock_section("art kit") - _add_art_kit_button() + var art_kit_grid := _add_stock_icon_grid() + _add_art_kit_button(art_kit_grid) _add_stock_section("markers") + var marker_grid := _add_stock_icon_grid() for product_id: StringName in ArtShopStockType.MARKER_PRODUCTS: - _add_art_upgrade_button(product_id) + _add_art_upgrade_button(product_id, marker_grid) _add_stock_section("brushes") + var brush_grid := _add_stock_icon_grid() for product_id: StringName in ArtShopStockType.BRUSH_PRODUCTS: - _add_art_upgrade_button(product_id) + _add_art_upgrade_button(product_id, brush_grid) _add_stock_section("grids") + var canvas_grid := _add_stock_icon_grid() for product_id: StringName in ArtShopStockType.GRID_PRODUCTS: - _add_art_upgrade_button(product_id) + _add_art_upgrade_button(product_id, canvas_grid) func _item_belongs_in_current_section(item: ItemDataType) -> bool: @@ -613,7 +732,21 @@ func _add_stock_section(title: String) -> void: _supplies_list.add_child(label) -func _add_art_kit_button() -> void: +func _add_stock_icon_grid() -> GridContainer: + var grid := GridContainer.new() + grid.columns = SUPPLY_ICON_GRID_COLUMNS + grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + grid.add_theme_constant_override( + "h_separation", SUPPLY_ICON_GRID_SEPARATION + ) + grid.add_theme_constant_override( + "v_separation", SUPPLY_ICON_GRID_SEPARATION + ) + _supplies_list.add_child(grid) + return grid + + +func _add_art_kit_button(parent: GridContainer) -> void: var item: ItemDataType = _item_catalog.get_item_by_id( ArtShopStockType.ART_KIT_ITEM_ID ) @@ -626,10 +759,19 @@ func _add_art_kit_button() -> void: ArtShopStockType.ART_KIT_PRICE, "owned" if owned else "not owned", ], - item.description, + "%s\n$%d • %s\n%s" % [ + item.display_name, + ArtShopStockType.ART_KIT_PRICE, + "owned" if owned else "not owned", + item.description, + ], + ) + _configure_supply_icon_tile(button, item.icon) + _add_supply_icon_tile( + parent, + button, + ArtShopStockType.ART_KIT_PRICE, ) - button.icon = item.icon - button.expand_icon = true button.disabled = ( owned or _transaction_in_progress @@ -642,7 +784,10 @@ func _add_art_kit_button() -> void: button.pressed.connect(_purchase_art_kit) -func _add_art_upgrade_button(product_id: StringName) -> void: +func _add_art_upgrade_button( + product_id: StringName, + parent: GridContainer, +) -> void: var kit_owned: bool = ( _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0 ) @@ -653,7 +798,18 @@ func _add_art_upgrade_button(product_id: StringName) -> void: ArtShopStockType.UPGRADE_PRICE, "unlocked" if unlocked else "locked", ], - ArtShopStockType.get_description(product_id), + "%s\n$%d • %s\n%s" % [ + ArtShopStockType.get_display_name(product_id), + ArtShopStockType.UPGRADE_PRICE, + "unlocked" if unlocked else "locked", + ArtShopStockType.get_description(product_id), + ], + ) + _configure_supply_icon_tile(button, FALLBACK_SUPPLY_ICON) + _add_supply_icon_tile( + parent, + button, + ArtShopStockType.UPGRADE_PRICE, ) button.disabled = ( not kit_owned @@ -667,17 +823,92 @@ func _add_art_upgrade_button(product_id: StringName) -> void: button.pressed.connect(_purchase_art_upgrade.bind(product_id)) -func _make_stock_button(button_text: String, tooltip: String) -> Button: +func _make_stock_button( + button_text: String, + tooltip: String, +) -> Button: var button := Button.new() button.custom_minimum_size = Vector2(195, 54) button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.text = button_text button.tooltip_text = tooltip UtilityPageStyleType.apply_ocean_button(button) - _supplies_list.add_child(button) return button +func _configure_supply_icon_tile( + button: Button, + icon: Texture2D, +) -> void: + button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE + button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + button.icon = icon if icon != null else FALLBACK_SUPPLY_ICON + button.expand_icon = true + button.alignment = HORIZONTAL_ALIGNMENT_CENTER + button.text = "" + + +func _add_supply_icon_tile( + parent: GridContainer, + button: Button, + price: int, +) -> void: + var tile_host := Control.new() + tile_host.custom_minimum_size = SUPPLY_ICON_HOST_SIZE + tile_host.mouse_filter = Control.MOUSE_FILTER_IGNORE + parent.add_child(tile_host) + button.position = Vector2.ZERO + button.size = SUPPLY_ICON_TILE_SIZE + tile_host.add_child(button) + var price_text := "$%d" % price + var price_text_width: float = UtilityPageStyleType.TuffyFont.get_string_size( + price_text, + HORIZONTAL_ALIGNMENT_LEFT, + -1.0, + SUPPLY_PRICE_FONT_SIZE, + ).x + var price_width: float = minf( + SUPPLY_ICON_TILE_SIZE.x, + ceilf(price_text_width + SUPPLY_PRICE_HORIZONTAL_PADDING), + ) + var price_bubble := PanelContainer.new() + price_bubble.name = "PriceBubble" + price_bubble.position = Vector2( + (SUPPLY_ICON_TILE_SIZE.x - price_width) * 0.5, + SUPPLY_PRICE_Y, + ) + price_bubble.size = Vector2(price_width, SUPPLY_PRICE_HEIGHT) + price_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + price_bubble.z_index = 2 + var price_style := UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_FIELD, + 12, + ) + price_style.content_margin_left = 6.0 + price_style.content_margin_right = 6.0 + price_style.content_margin_top = 0.0 + price_style.content_margin_bottom = 0.0 + price_bubble.add_theme_stylebox_override("panel", price_style) + tile_host.add_child(price_bubble) + var price_label := Label.new() + price_label.name = "Price" + price_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + price_label.text = price_text + price_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + price_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + price_label.add_theme_font_override( + "font", UtilityPageStyleType.TuffyFont + ) + price_label.add_theme_font_size_override( + "font_size", SUPPLY_PRICE_FONT_SIZE + ) + price_label.add_theme_color_override( + "font_color", SUPPLY_PRICE_COLOR + ) + price_label.add_theme_constant_override("outline_size", 0) + price_bubble.add_child(price_label) + + func _refresh_cooler_capacity() -> void: if _cooler_capacity == null: return diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 58d2961..1fd5314 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=8 format=3] [ext_resource type="Script" path="res://ui/fishing_shop.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -6,6 +6,7 @@ [ext_resource type="Texture2D" path="res://items/icons/shop/64_power_plus.png" id="4_barrier"] [ext_resource type="Texture2D" path="res://items/icons/placeholder/fish_coin.svg" id="5_coin"] [ext_resource type="Texture2D" path="res://items/icons/equipment/64_cooler_plus.png" id="6_cooler"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="7_close"] [node name="FishingShop" type="Control"] unique_name_in_owner = true @@ -34,7 +35,7 @@ mouse_filter = 0 [node name="ShopCoolerPage" type="Control" parent="."] unique_name_in_owner = true visible = false -z_index = 90 +z_index = 102 layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -53,18 +54,8 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 1 -[node name="BackToShopButton" type="Button" parent="ShopCoolerPage"] -unique_name_in_owner = true -z_index = 110 -layout_mode = 0 -offset_left = 1052.0 -offset_top = 54.0 -offset_right = 1218.0 -offset_bottom = 104.0 -text = "back to shop" - [node name="ShopPanel" type="PanelContainer" parent="."] -z_index = 2 +z_index = 101 unique_name_in_owner = true layout_mode = 1 anchors_preset = 8 @@ -101,41 +92,39 @@ text = "fishing shop" theme_override_font_sizes/font_size = 32 theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) -[node name="CoinIcon" type="TextureRect" parent="ShopPanel/Margin/Layout/Header"] -custom_minimum_size = Vector2(24, 24) +[node name="BalanceDisplay" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Header"] +custom_minimum_size = Vector2(0, 48) layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/separation = 6 +alignment = 1 + +[node name="CoinIcon" type="TextureRect" parent="ShopPanel/Margin/Layout/Header/BalanceDisplay"] +custom_minimum_size = Vector2(26, 26) +layout_mode = 2 +size_flags_vertical = 4 texture = ExtResource("5_coin") expand_mode = 1 stretch_mode = 5 texture_filter = 1 -[node name="WalletLabel" type="Label" parent="ShopPanel/Margin/Layout/Header"] +[node name="WalletLabel" type="Label" parent="ShopPanel/Margin/Layout/Header/BalanceDisplay"] unique_name_in_owner = true +custom_minimum_size = Vector2(0, 32) layout_mode = 2 -text = "wallet: $0" +size_flags_vertical = 4 +text = "0" +vertical_alignment = 1 [node name="CloseButton" type="Button" parent="ShopPanel/Margin/Layout/Header"] unique_name_in_owner = true +custom_minimum_size = Vector2(48, 48) layout_mode = 2 -text = "close" -custom_minimum_size = Vector2(104, 48) - -[node name="ModeTabs" type="HBoxContainer" parent="ShopPanel/Margin/Layout"] -layout_mode = 2 -alignment = 1 -theme_override_constants/separation = 10 - -[node name="BuyModeButton" type="Button" parent="ShopPanel/Margin/Layout/ModeTabs"] -unique_name_in_owner = true -custom_minimum_size = Vector2(180, 42) -layout_mode = 2 -text = "buy gear & supplies" - -[node name="SellModeButton" type="Button" parent="ShopPanel/Margin/Layout/ModeTabs"] -unique_name_in_owner = true -custom_minimum_size = Vector2(180, 42) -layout_mode = 2 -text = "sell fish" +tooltip_text = "Close shop" +flat = true +icon = ExtResource("7_close") +expand_icon = true +icon_max_width = 40 [node name="Feedback" type="Label" parent="ShopPanel/Margin/Layout"] unique_name_in_owner = true @@ -146,10 +135,10 @@ theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) [node name="ShopTabBar" type="HBoxContainer" parent="."] unique_name_in_owner = true -z_index = 1 -offset_left = 298.0 +z_index = 100 +offset_left = 180.0 offset_top = 26.0 -offset_right = 982.0 +offset_right = 1100.0 offset_bottom = 70.0 alignment = 1 theme_override_constants/separation = 6 diff --git a/ui/game_theme.tres b/ui/game_theme.tres index 7da4650..e21f90b 100644 --- a/ui/game_theme.tres +++ b/ui/game_theme.tres @@ -144,6 +144,17 @@ corner_radius_top_right = 10 corner_radius_bottom_right = 10 corner_radius_bottom_left = 10 +[sub_resource type="StyleBoxFlat" id="TooltipPanel"] +content_margin_left = 12.0 +content_margin_top = 8.0 +content_margin_right = 12.0 +content_margin_bottom = 8.0 +bg_color = Color(0.031, 0.122, 0.169, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + [resource] default_font = ExtResource("1_font") default_font_size = 23 @@ -175,3 +186,6 @@ OptionButton/styles/hover = SubResource("ButtonHover") OptionButton/styles/normal = SubResource("ButtonNormal") OptionButton/styles/pressed = SubResource("ButtonPressed") PanelContainer/styles/panel = SubResource("Panel") +TooltipLabel/colors/font_color = Color(0.765, 0.875, 0.902, 1) +TooltipLabel/font_sizes/font_size = 18 +TooltipPanel/styles/panel = SubResource("TooltipPanel") diff --git a/ui/game_ui.gd b/ui/game_ui.gd index a9db30d..67fb162 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -402,6 +402,7 @@ func setup( network_shop_service, ) _fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _fishing_shop.menu_exit_started.connect(_on_shop_exit_started) _fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested) _fishing_shop.shop_cooler_return_requested.connect( _on_shop_cooler_return_requested @@ -528,8 +529,11 @@ func _on_character_call_received( if _spawn_service == null: return var avatar: PlayerType = _spawn_service.get_avatar(peer_id) + if avatar == null: + return + avatar.play_character_call_visual() var audio_path: String = VoiceProfilesType.call_audio_path(call_id) - if avatar == null or not ResourceLoader.exists(audio_path, "AudioStream"): + if not ResourceLoader.exists(audio_path, "AudioStream"): return var stream: AudioStream = load(audio_path) as AudioStream if stream == null: @@ -2088,11 +2092,18 @@ func _on_player_menu_exit_started() -> void: player_menu_backdrop_visibility_changed.emit(false) +func _on_shop_exit_started() -> void: + if not _shop_open: + return + shop_backdrop_visibility_changed.emit(false) + + func _on_shop_visibility_changed(is_open: bool) -> void: _shop_open = is_open if is_open and _surface_drawing != null: _surface_drawing.deactivate() - shop_backdrop_visibility_changed.emit(is_open) + if is_open: + shop_backdrop_visibility_changed.emit(true) if not is_open and _player_menu.is_shop_cooler_mounted(): _player_menu.unmount_shop_cooler() _refresh_chat_availability() diff --git a/ui/hotbar.gd b/ui/hotbar.gd index ef7c505..fe26cc6 100644 --- a/ui/hotbar.gd +++ b/ui/hotbar.gd @@ -17,7 +17,7 @@ const FishingSpotType = preload("res://fishing/fishing_spot.gd") const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0) const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0) -const HOTBAR_PRESENTATION_SCALE: float = 1.00 +const HOTBAR_PRESENTATION_SCALE: float = 0.80 const HOTBAR_CANONICAL_POSITION := Vector2.ZERO const HOTBAR_MENU_POSITION := Vector2(-136.0, -90.0) const HOTBAR_GAMEPLAY_Z_INDEX: int = 35 @@ -215,9 +215,7 @@ func set_player_menu_context(enabled: bool) -> void: return _player_menu_context = enabled z_index = HOTBAR_MENU_Z_INDEX if enabled else HOTBAR_GAMEPLAY_Z_INDEX - _presentation_scale_root.position = ( - HOTBAR_MENU_POSITION if enabled else HOTBAR_CANONICAL_POSITION - ) + _presentation_scale_root.position = _presentation_position() func set_presentation_visible( @@ -363,11 +361,7 @@ func _apply_layout() -> void: _presentation_scale_root.scale = ( Vector2.ONE * HOTBAR_PRESENTATION_SCALE ) - _presentation_scale_root.position = ( - HOTBAR_MENU_POSITION - if _player_menu_context - else HOTBAR_CANONICAL_POSITION - ) + _presentation_scale_root.position = _presentation_position() var field_size := ( Vector2(580.0, 82.0) if _compact_layout @@ -382,6 +376,21 @@ func _apply_layout() -> void: slot.apply_layout(_compact_layout) +func _presentation_position() -> Vector2: + var scaled_size: Vector2 = ( + DESKTOP_REFERENCE_SIZE * HOTBAR_PRESENTATION_SCALE + ) + var bottom_centered_offset := Vector2( + (DESKTOP_REFERENCE_SIZE.x - scaled_size.x) * 0.5, + DESKTOP_REFERENCE_SIZE.y - scaled_size.y, + ) + return bottom_centered_offset + ( + HOTBAR_MENU_POSITION + if _player_menu_context + else HOTBAR_CANONICAL_POSITION + ) + + func _refresh() -> void: for slot: BubbleHotbarSlotType in _slots: slot.refresh() diff --git a/ui/network/join_game_page.gd b/ui/network/join_game_page.gd index b0b3d6d..1788ea5 100644 --- a/ui/network/join_game_page.gd +++ b/ui/network/join_game_page.gd @@ -42,7 +42,6 @@ const DIRECT_WORKFLOW_HELP: String = ( @onready var _delete_button: Button = %DeleteButton @onready var _cancel_button: Button = %CancelButton @onready var _back_button: Button = %BackButton -@onready var _open_close_button: Button = %OpenCloseButton @onready var _status: Label = %Status @onready var _session_summary: Label = %SessionSummary @onready var _delete_confirmation: BubbleConfirmationPage = ( @@ -75,7 +74,7 @@ func _ready() -> void: _discover_button, _direct_button, _saved_button, _recent_button, _refresh_button, _join_button, _save_button, _edit_button, _favorite_button, _delete_button, - _cancel_button, _back_button, _open_close_button, + _cancel_button, _back_button, ]: UtilityPageStyle.apply_ocean_button(button) UtilityPageStyle.apply_ocean_line_edit(_address) @@ -92,7 +91,6 @@ func _ready() -> void: _delete_button.pressed.connect(_on_delete_pressed) _cancel_button.pressed.connect(_on_cancel_pressed) _back_button.pressed.connect(_on_back_pressed) - _open_close_button.pressed.connect(_on_open_close_pressed) _address.text_submitted.connect(func(_value: String) -> void: if _name_entry_active: _name_edit.grab_focus() @@ -235,6 +233,9 @@ func _request_join() -> void: var endpoint_text: String = _address.text if _mode == Mode.DISCOVER: var room: Dictionary = _selected_discovery_room() + if _discovery != null and _discovery.is_own_room(room): + _set_status("You are already hosting this room.", true) + return if _discovery_room_is_full(room): _set_status("That room is full.", true) return @@ -441,12 +442,16 @@ func _refresh_entries() -> void: for room: Dictionary in _discovery_rooms: var player_count: int = int(room.get("current_players", 0)) var maximum: int = int(room.get("max_players", 0)) + var own_room: bool = ( + _discovery != null and _discovery.is_own_room(room) + ) _server_list.add_item( - "%s — %d / %d players%s" % [ + "%s — %d / %d players%s%s" % [ str(room.get("room_name", "Public room")), player_count, maximum, " — full" if player_count >= maximum else "", + " — your room" if own_room else "", ] ) return @@ -517,6 +522,12 @@ func _refresh() -> void: and selected and _discovery_room_is_full(_selected_discovery_room()) ) + or ( + discovery_mode + and selected + and _discovery != null + and _discovery.is_own_room(_selected_discovery_room()) + ) ) _join_button.text = "join\nnow" if direct else "join" _refresh_button.visible = discovery_mode and not _name_entry_active @@ -541,15 +552,6 @@ func _refresh() -> void: ) _delete_button.text = "remove" if _mode == Mode.RECENT else "delete" _cancel_button.visible = connecting - _open_close_button.visible = ( - _gameplay_context and _network_session.is_host() - ) - if _open_close_button.visible: - _open_close_button.text = ( - "close\ngame" - if _network_session.is_open_host() - else "open\ngame" - ) _session_summary.visible = ( _gameplay_context and _network_session.state != NetworkSession.State.INACTIVE @@ -647,7 +649,7 @@ func _format_entry_details(entry: SavedServerEntry) -> String: func _format_discovery_details(room: Dictionary) -> String: if room.is_empty(): return "Select a public room." - return "\n".join([ + var lines: Array[String] = [ "Room: %s" % str(room.get("room_name", "Public room")), "Players: %d / %d" % [ int(room.get("current_players", 0)), @@ -657,7 +659,10 @@ func _format_discovery_details(room: Dictionary) -> String: _discovery.room_endpoint(room) if _discovery != null else "—" ), "Direct UDP connection • ping is measured after joining", - ]) + ] + if _discovery != null and _discovery.is_own_room(room): + lines.append("This is the room you are currently hosting.") + return "\n".join(lines) func _selected_discovery_room() -> Dictionary: @@ -785,13 +790,6 @@ func _on_back_pressed() -> void: back_requested.emit() -func _on_open_close_pressed() -> void: - if _network_session == null or not _network_session.is_host(): - return - _network_session.set_host_open(not _network_session.is_open_host()) - _refresh() - - func _on_state_changed(_state: NetworkSession.State) -> void: _refresh() diff --git a/ui/network/join_game_page.tscn b/ui/network/join_game_page.tscn index 062a1f6..c4be4fa 100644 --- a/ui/network/join_game_page.tscn +++ b/ui/network/join_game_page.tscn @@ -308,13 +308,6 @@ custom_minimum_size = Vector2(82, 68) layout_mode = 2 text = "delete" -[node name="OpenCloseButton" type="Button" parent="Paper/Margin/Layout/Actions"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(88, 72) -layout_mode = 2 -text = "open\ngame" - [node name="CancelButton" type="Button" parent="Paper/Margin/Layout/Actions"] unique_name_in_owner = true visible = false diff --git a/ui/players_page.gd b/ui/players_page.gd index 49cc40b..8c93247 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -1,6 +1,8 @@ class_name PlayersPage extends Control +const TOGGLE_STATE_COLOR := Color("c3dfe6") + var _service: NetworkPlayerListService var _discovery: DiscoveryClient var _count_label: Label @@ -9,7 +11,10 @@ var _list: VBoxContainer var _status: Label var _host_settings_panel: PanelContainer var _room_name_edit: LineEdit -var _discoverable_toggle: CheckButton +var _online_toggle: Button +var _online_state_label: Label +var _discoverable_toggle: Button +var _discoverable_state_label: Label var _host_discovery_status: Label var _current_tab := 0 @@ -111,21 +116,26 @@ func _build_host_settings(root: VBoxContainer) -> void: _room_name_edit = LineEdit.new() _room_name_edit.custom_minimum_size = Vector2(300.0, 42.0) _room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH - _room_name_edit.placeholder_text = DiscoveryClient.DEFAULT_ROOM_NAME + _room_name_edit.placeholder_text = "Player Name's Server" UtilityPageStyle.apply_ocean_line_edit(_room_name_edit) _room_name_edit.text_submitted.connect( func(_value: String) -> void: _commit_room_name() ) _room_name_edit.focus_exited.connect(_commit_room_name) row.add_child(_room_name_edit) - _discoverable_toggle = CheckButton.new() - _discoverable_toggle.text = "list publicly" - _discoverable_toggle.toggled.connect(_on_discoverable_toggled) - UtilityPageStyle.apply_ocean_button(_discoverable_toggle) - row.add_child(_discoverable_toggle) + _online_toggle = _build_host_toggle("online", row) + _online_state_label = _online_toggle.get_node("StateBadge/State") as Label + _online_toggle.pressed.connect(_on_online_pressed) + _discoverable_toggle = _build_host_toggle("discovery", row) + _discoverable_state_label = ( + _discoverable_toggle.get_node("StateBadge/State") as Label + ) + _discoverable_toggle.pressed.connect(_on_discoverable_pressed) _host_discovery_status = Label.new() _host_discovery_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL _host_discovery_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _host_discovery_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _host_discovery_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _host_discovery_status.add_theme_font_size_override("font_size", 14) _host_discovery_status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY @@ -133,6 +143,56 @@ func _build_host_settings(root: VBoxContainer) -> void: row.add_child(_host_discovery_status) +func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button: + var button := Button.new() + button.custom_minimum_size = Vector2(150.0, 46.0) + button.text = label_text + button.alignment = HORIZONTAL_ALIGNMENT_CENTER + button.clip_contents = false + UtilityPageStyle.apply_ocean_button(button) + row.add_child(button) + var badge := PanelContainer.new() + badge.name = "StateBadge" + badge.mouse_filter = Control.MOUSE_FILTER_IGNORE + badge.z_index = 2 + button.add_child(badge) + badge.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) + badge.offset_left = -39.0 + badge.offset_top = -12.0 + badge.offset_right = 39.0 + badge.offset_bottom = 12.0 + var state_label := Label.new() + state_label.name = "State" + state_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + state_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + state_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + state_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont) + state_label.add_theme_font_size_override("font_size", 15) + state_label.add_theme_color_override("font_color", TOGGLE_STATE_COLOR) + badge.add_child(state_label) + return button + + +func _set_host_toggle_state( + button: Button, + state_label: Label, + state_text: String, + enabled: bool, +) -> void: + state_label.text = state_text + var badge := state_label.get_parent() as PanelContainer + badge.add_theme_stylebox_override( + "panel", + UtilityPageStyle.rounded_style( + UtilityPageStyle.OCEAN_SELECTED + if enabled + else UtilityPageStyle.OCEAN_FIELD, + 12, + ), + ) + button.queue_redraw() + + func _select_tab(index: int) -> void: if index == 2 and (_service == null or not _service.is_local_host()): return @@ -172,17 +232,38 @@ func _refresh_host_settings() -> void: return if not _room_name_edit.has_focus(): _room_name_edit.text = _discovery.get_room_name() - _discoverable_toggle.set_pressed_no_signal(_discovery.is_discoverable()) + var is_open: bool = _service.is_open_host() + var is_discoverable: bool = _discovery.is_discoverable() + _set_host_toggle_state( + _online_toggle, + _online_state_label, + "OPEN" if is_open else "CLOSED", + is_open, + ) + _set_host_toggle_state( + _discoverable_toggle, + _discoverable_state_label, + "ON" if is_discoverable else "OFF", + is_discoverable, + ) + _online_toggle.disabled = not _service.is_local_host() + _online_toggle.tooltip_text = ( + "Close this game to new connections." + if is_open + else "Open this game so other players can connect." + ) var can_publish: bool = ( _service.is_local_host() and _discovery.is_configured() - and _service.is_open_host() + and is_open ) _discoverable_toggle.disabled = not can_publish _discoverable_toggle.tooltip_text = ( - "Advertise this open game in the public room browser." + "Stop advertising this game in the public room browser." + if is_discoverable + else "Advertise this open game in the public room browser." if can_publish - else "Open the game before listing it publicly." + else "Open the game before enabling discovery." if _discovery.is_configured() else "Room discovery is not configured in this build." ) @@ -201,13 +282,18 @@ func _commit_room_name() -> void: _room_name_edit.text = _discovery.get_room_name() -func _on_discoverable_toggled(enabled: bool) -> void: +func _on_online_pressed() -> void: + if _service == null or not _service.is_local_host(): + return + _service.set_host_open(not _service.is_open_host()) + _refresh_host_settings() + + +func _on_discoverable_pressed() -> void: if _discovery == null: return - if not _discovery.set_discoverable(enabled): - _discoverable_toggle.set_pressed_no_signal( - _discovery.is_discoverable() - ) + _discovery.set_discoverable(not _discovery.is_discoverable()) + _refresh_host_settings() func _on_host_settings_changed( @@ -216,15 +302,25 @@ func _on_host_settings_changed( ) -> void: if _room_name_edit != null and not _room_name_edit.has_focus(): _room_name_edit.text = room_name - if _discoverable_toggle != null: - _discoverable_toggle.set_pressed_no_signal(discoverable) + if _discoverable_state_label != null: + _set_host_toggle_state( + _discoverable_toggle, + _discoverable_state_label, + "ON" if discoverable else "OFF", + discoverable, + ) _refresh_host_settings() func _on_host_status_changed(message: String, is_error: bool) -> void: if _host_discovery_status == null: return - _host_discovery_status.text = message + var tooltip_only: bool = message in [ + "Open the game before listing it publicly.", + "Open the game before enabling discovery.", + ] + _host_discovery_status.text = "" if tooltip_only else message + _host_discovery_status.visible = not _host_discovery_status.text.is_empty() _host_discovery_status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_DANGER diff --git a/ui/weather_icon.gd b/ui/weather_icon.gd index dbe41b2..b44bc9b 100644 --- a/ui/weather_icon.gd +++ b/ui/weather_icon.gd @@ -25,7 +25,7 @@ func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_PASS focus_mode = Control.FOCUS_NONE texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - custom_minimum_size = Vector2(34.0, 34.0) + custom_minimum_size = Vector2(51.0, 51.0) _update_tooltip() queue_redraw()