Compare commits

...

4 commits

112 changed files with 2472 additions and 739 deletions

View file

@ -0,0 +1,7 @@
[plugin]
name="NETfishing Shoreline Auto-Baker"
description="Rebuilds configured static shoreline ribbons after authored terrain reimports."
author="woofmeow"
version="1.0.0"
script="shoreline_auto_baker.gd"

View file

@ -0,0 +1,66 @@
@tool
extends EditorPlugin
const TERRAIN_RESOURCE := (
"res://art/exported/environment/terrain/starter_island.glb"
)
const REGION_SCENE := "res://world/regions/starter_island_region.tscn"
var _bake_pending := false
func _enter_tree() -> void:
var filesystem := get_editor_interface().get_resource_filesystem()
if not filesystem.resources_reimported.is_connected(_on_resources_reimported):
filesystem.resources_reimported.connect(_on_resources_reimported)
func _exit_tree() -> void:
var filesystem := get_editor_interface().get_resource_filesystem()
if filesystem.resources_reimported.is_connected(_on_resources_reimported):
filesystem.resources_reimported.disconnect(_on_resources_reimported)
func _on_resources_reimported(resources: PackedStringArray) -> void:
if not resources.has(TERRAIN_RESOURCE) or _bake_pending:
return
_bake_pending = true
call_deferred("_bake_configured_shorelines")
func _bake_configured_shorelines() -> void:
_bake_pending = false
var packed_region := ResourceLoader.load(
REGION_SCENE,
"PackedScene",
ResourceLoader.CACHE_MODE_REPLACE,
) as PackedScene
if packed_region == null:
push_error("Shoreline auto-bake could not load %s." % REGION_SCENE)
return
var region := packed_region.instantiate() as Node3D
add_child(region)
var baker := region.get_node_or_null(
"ShorelineRibbonBaker"
) as ShorelineRibbonBaker
if baker == null:
push_error("Shoreline auto-bake found no configured baker.")
region.queue_free()
return
var results := baker.rebuild_all()
for result: Dictionary in results:
if result.get("skipped", false):
continue
var output_path: String = result.get("output_path", "")
if not output_path.is_empty():
ResourceLoader.load(
output_path,
"ArrayMesh",
ResourceLoader.CACHE_MODE_REPLACE,
)
print(
"Shoreline auto-bake: %s, %d loops, %d triangles"
% [output_path, result.loop_count, result.triangle_count]
)
region.queue_free()
get_editor_interface().get_resource_filesystem().scan()

View file

@ -0,0 +1 @@
uid://cbk4xpalei5v3

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -232,17 +232,20 @@ smoothing overrides, and generated `.tres` path. Props, players, bobbers, and
gameplay areas are not scanned unless selected explicitly. Only `SALT_WATER`
bodies generate ribbons.
In the editor, select the baker and press **Rebuild Shoreline Ribbons**. The
equivalent validation command is:
The editor automatically rebuilds configured shoreline resources when an
authored terrain GLB is reimported. Select the baker and press **Rebuild
Shoreline Ribbons** only when intentionally forcing a rebuild without a terrain
reimport. The equivalent headless fallback and validation command is:
```sh
godot --headless --path . --script scripts/bake_shoreline_ribbons.gd
```
Use `debug_path_stage` only while comparing extraction stages and leave it Off
in committed scenes. Rebuild when saltwater terrain at the waterline, water
height, or generation bounds change. Normal gameplay loads committed generated
meshes and never runs extraction.
in committed scenes. A rebuild is required when saltwater terrain at the
waterline, water height, or generation bounds change; terrain reimports handle
the usual case automatically. Normal gameplay loads committed generated meshes
and never runs extraction.
### Facial-feature textures
@ -262,6 +265,24 @@ snapshots, so do not casually rename them. Preserve the established RGBA
transparent canvas. Restart a development build after adding an image; rebuild
exports to package new `res://` files.
### Texture sampling
NETfishing artwork always uses nearest-neighbor sampling. Do not enable linear,
bilinear, trilinear, or anisotropic texture filtering, and do not generate
mipmaps. Shader samplers must declare `filter_nearest`. The
`TextureSamplingPolicy` autoload applies nearest sampling to 2D canvas items,
Sprite3D presentations, imported 3D materials, and dynamically constructed
mesh materials at runtime.
After importing new artwork, normalize its tracked import metadata where
needed and run the focused policy check:
```sh
godot --headless --path . --script scripts/normalize_texture_imports.gd \
-- --apply --root res://path/to/new/artwork
godot --headless --path . --script tests/texture_sampling_validation.gd
```
### Bubble menus
Instance `ui/components/bubble_menu/bubble_button.tscn` for each action, or

View file

@ -9,4 +9,4 @@ display_name = "pelicans"
animal_name_singular = "pelican"
animal_name_plural = "pelicans"
payout_multiplier = 0.25
sale_message_template = "You sold your {fish} to the pelicans for ${payout}."
sale_message_template = "You sold your {fish} to the pelicans."

View file

@ -53,8 +53,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." % [
fish_name,
buyer_name,
payout,
]

View file

@ -165,9 +165,10 @@ func _validate_batch(
result.payout
)
else:
result.sale_message = "you sold %d fish to the %s for $%d." % [
result.sale_message = (
"you sold %d fish to the %s." % [
result.fish_count,
result.buyer_animal_name,
result.payout,
]
]
)
return result

View file

@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
[ext_resource type="Texture2D" path="res://items/icons/placeholder/art_kit.svg" id="2_icon"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit.png" id="2_icon"]
[resource]
script = ExtResource("1_item")

BIN
items/icons/art/art_kit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bm0kkevv62mu0"
path="res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"
uid="uid://datu7j7tq2k3e"
path="res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/placeholder/art_kit.svg"
dest_files=["res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"]
source_file="res://items/icons/art/art_kit.png"
dest_files=["res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"]
[params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://trqvs4dgd4rt"
path="res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"
uid="uid://cdbba3bvhuear"
path="res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/placeholder/fish_coin.svg"
dest_files=["res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"]
source_file="res://items/icons/art/art_kit_eraser.png"
dest_files=["res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"]
[params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dm24dy3ebvqmv"
path="res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_grid_finish_light.png"
dest_files=["res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cv6yicrysb0vs"
path="res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_grid_hidden_light.png"
dest_files=["res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dh2tu3ptqe6f7"
path="res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_grid_light.png"
dest_files=["res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckxdpi5sqerao"
path="res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_grid_restore_light.png"
dest_files=["res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b20kab6kxh7ap"
path="res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_marker.png"
dest_files=["res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect x="7" y="10" width="50" height="44" rx="12" fill="#35b9c7"/>
<rect x="12" y="15" width="40" height="34" rx="9" fill="#0d2c3a"/>
<circle cx="23" cy="27" r="6" fill="#f5eed9"/>
<circle cx="39" cy="27" r="6" fill="#ef5b62"/>
<path d="M18 42L30 33L35 38L46 30L50 42Z" fill="#ffd166"/>
</svg>

Before

Width:  |  Height:  |  Size: 388 B

View file

@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<circle cx="32" cy="32" r="26" fill="#ffd25a" stroke="#17243a" stroke-width="5"/>
<ellipse cx="31" cy="32" rx="14" ry="9" fill="#54c7c0" stroke="#17243a" stroke-width="4"/>
<path d="M44 32 l10-9 v18 z" fill="#54c7c0" stroke="#17243a" stroke-width="4" stroke-linejoin="round"/>
<circle cx="25" cy="29" r="2.5" fill="#17243a"/>
</svg>

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bf3jlmqhmmuge"
path="res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/shop/32_currency.png"
dest_files=["res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.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

View file

@ -383,7 +383,7 @@ static func _lifetime_title(chain_id: String, target: int) -> String:
"catch":
return "seasoned angler"
"sell":
return "fish coin regular"
return "market regular"
"level":
return "reach level %d" % target
"discover":

View file

@ -656,9 +656,7 @@ func _reward_claimed(reward: Dictionary) -> void:
var fish_coin: int = int(reward.get("fish_coin", 0))
var experience: int = int(reward.get("experience", 0))
reward_claimed.emit(title, fish_coin, experience)
status_changed.emit("payment received — $%d and %d xp" % [
fish_coin, experience,
])
status_changed.emit("payment received — %d xp" % experience)
changed.emit()

View file

@ -554,6 +554,7 @@ func _initialize_application(dedicated: bool) -> void:
_player,
_surface_drawings_root,
_player.bag,
_player.hotbar,
_player.art_unlocks,
)
_network_player_list.set_surface_drawing_service(

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,74 @@
extends Node
## Enforces NETfishing's nearest-neighbor-only artwork policy at runtime.
##
## Godot uses separate texture-filter enums for CanvasItem and 3D materials.
## Applying both here prevents imported models, dynamically created controls,
## and Sprite3D presentations from silently falling back to linear sampling.
func _enter_tree() -> void:
get_tree().node_added.connect(_on_node_added)
call_deferred("_enforce_existing_tree")
func _on_node_added(node: Node) -> void:
enforce_node(node)
# Some runtime presenters assign their mesh or material immediately after
# add_child(). Recheck once deferred so those resources are covered too.
call_deferred("_enforce_deferred_node", weakref(node))
func _enforce_existing_tree() -> void:
enforce_subtree(get_tree().root)
func _enforce_deferred_node(node_ref: WeakRef) -> void:
var node := node_ref.get_ref() as Node
if node != null:
enforce_node(node)
static func enforce_subtree(node: Node) -> void:
enforce_node(node)
for child: Node in node.get_children():
enforce_subtree(child)
static func enforce_node(node: Node) -> void:
if node is CanvasItem:
(node as CanvasItem).texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
if node is SpriteBase3D:
(node as SpriteBase3D).texture_filter = (
BaseMaterial3D.TEXTURE_FILTER_NEAREST
)
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
enforce_material(geometry.material_override)
enforce_material(geometry.material_overlay)
if node is MeshInstance3D:
enforce_mesh((node as MeshInstance3D).mesh)
if node is MultiMeshInstance3D:
var multimesh := (node as MultiMeshInstance3D).multimesh
if multimesh != null:
enforce_mesh(multimesh.mesh)
if node is GPUParticles3D:
var particles := node as GPUParticles3D
for pass_index: int in particles.draw_passes:
enforce_mesh(particles.get_draw_pass_mesh(pass_index))
if node is CPUParticles3D:
enforce_mesh((node as CPUParticles3D).mesh)
static func enforce_mesh(mesh: Mesh) -> void:
if mesh == null:
return
for surface_index: int in mesh.get_surface_count():
enforce_material(mesh.surface_get_material(surface_index))
static func enforce_material(material: Material) -> void:
if material is BaseMaterial3D:
(material as BaseMaterial3D).texture_filter = (
BaseMaterial3D.TEXTURE_FILTER_NEAREST
)

View file

@ -0,0 +1 @@
uid://dwd4wfn8bwk5i

View file

@ -464,7 +464,7 @@ func _build_authoritative_result(
current_state, product_id
)
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
rejection = "Not enough fish coin."
rejection = "Insufficient funds."
if not rejection.is_empty():
return _rejected_result(request, rejection)
return {
@ -633,7 +633,7 @@ func _validate_local_result(data: Dictionary) -> String:
):
return "Purchase could not be completed."
if not _wallet.can_afford(cost):
return "Not enough fish coin."
return "Insufficient funds."
match category:
NetworkShopProtocol.ProductCategory.SUPPLY:
var item: ItemDataType = (

View file

@ -41,6 +41,7 @@ var _spawn_service: PlayerSpawnService
var _relationships: PlayerRelationshipStore
var _local_player: Player
var _bag: PlayerBag
var _hotbar: PlayerHotbar
var _art_unlocks: PlayerArtUnlocks
var _drawing_root: Node3D
var _canvas_states: Dictionary[String, Dictionary] = {}
@ -87,6 +88,7 @@ func setup(
local_player: Player,
drawing_root: Node3D,
bag: PlayerBag,
hotbar: PlayerHotbar,
art_unlocks: PlayerArtUnlocks,
) -> void:
_session = session
@ -95,6 +97,7 @@ func setup(
_local_player = local_player
_drawing_root = drawing_root
_bag = bag
_hotbar = hotbar
_art_unlocks = art_unlocks
_refresh_local_unlocks()
if _session != null:
@ -123,20 +126,6 @@ func handle_input(
can_open: bool,
pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION,
) -> bool:
if event is InputEventKey:
var key_event := event as InputEventKey
if (
key_event.pressed
and not key_event.echo
and key_event.physical_keycode == KEY_P
):
if _active:
deactivate()
return true
if can_open and can_activate():
activate(pointer_screen_position)
return true
return false
if not _active:
return false
if not can_open:
@ -149,8 +138,7 @@ func handle_input(
if _placing_grid:
_set_placement_mode(false)
return true
deactivate()
return true
return false
if event is InputEventKey:
var key_event := event as InputEventKey
if not key_event.pressed or key_event.echo:
@ -172,28 +160,19 @@ func handle_input(
return true
if event is InputEventMouseButton:
var mouse_event := event as InputEventMouseButton
# Ordinary wheel input always belongs to Hotbar selection. Brush size is
# selected explicitly in the Art Kit toolbar.
if mouse_event.button_index in [
MOUSE_BUTTON_WHEEL_UP,
MOUSE_BUTTON_WHEEL_DOWN,
]:
return false
_set_pointer_from_input(
mouse_event.position,
pointer_screen_position,
)
_update_aim()
if (
mouse_event.shift_pressed
and mouse_event.button_index in [
MOUSE_BUTTON_WHEEL_UP,
MOUSE_BUTTON_WHEEL_DOWN,
]
):
return false
match mouse_event.button_index:
MOUSE_BUTTON_WHEEL_UP:
if mouse_event.pressed and not _placing_grid:
_set_brush_size(_brush_size + 1)
return true
MOUSE_BUTTON_WHEEL_DOWN:
if mouse_event.pressed and not _placing_grid:
_set_brush_size(_brush_size - 1)
return true
MOUSE_BUTTON_LEFT:
if _placing_grid:
if mouse_event.pressed:
@ -241,7 +220,7 @@ func activate(
if _active or not can_activate():
return
_active = true
_placing_grid = true
_placing_grid = false
_eraser_mode = false
_clear_armed_guide_action(false)
_refresh_local_unlocks()
@ -257,9 +236,7 @@ func activate(
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_update_aim()
_refresh_stencil_visibility()
_emit_hud_state(
"click to place a shared grid"
)
_emit_hud_state("marker selected")
func deactivate() -> void:
@ -288,7 +265,11 @@ func is_placement_mode() -> bool:
func can_activate() -> bool:
return _drawing_available() and _owns_art_kit()
return (
_drawing_available()
and _owns_art_kit()
and _art_kit_is_selected()
)
func set_placement_mode(enabled: bool) -> void:
@ -344,7 +325,7 @@ func arm_guide_action(action: int) -> bool:
_selected_canvas_id = ""
_update_aim()
_refresh_stencil_visibility()
_emit_hud_state("click a grid to %s it" % _guide_action_verb(action))
_emit_hud_state(_guide_action_status(action))
return true
@ -415,6 +396,15 @@ func _owns_art_kit() -> bool:
return _bag != null and _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
func _art_kit_is_selected() -> bool:
return (
_hotbar != null
and _hotbar.get_selected_assignment_kind()
== PlayerHotbar.AssignmentKind.ITEM
and _hotbar.get_selected_item_id() == ArtShopStockType.ART_KIT_ITEM_ID
)
func _local_entitlement() -> Dictionary:
return {
"has_kit": _owns_art_kit(),
@ -550,8 +540,7 @@ func request_canvas_at_surface(
tangent: Vector3,
) -> bool:
if (
not _drawing_available()
or not _owns_art_kit()
not can_activate()
or _art_unlocks == null
or not _art_unlocks.is_grid_size_unlocked(_grid_size)
or normal.is_zero_approx()
@ -581,8 +570,7 @@ func request_cell_edits(
stroke_id: String = "",
) -> bool:
if (
not _drawing_available()
or not _owns_art_kit()
not can_activate()
or canvas_id.is_empty()
or edits.is_empty()
):
@ -613,7 +601,7 @@ func request_guide_visibility(
should_be_visible: bool,
should_finalize: bool = false,
) -> bool:
if not _drawing_available() or not _owns_art_kit() or canvas_id.is_empty():
if not can_activate() or canvas_id.is_empty():
return false
var data: Dictionary = {
"request_id": _new_request_id("guide"),
@ -633,8 +621,7 @@ func request_guide_visibility(
func request_undo_last_stroke() -> bool:
if (
not _drawing_available()
or not _owns_art_kit()
not can_activate()
or _last_local_stroke_id.is_empty()
):
_emit_hud_state("nothing to undo")
@ -698,6 +685,9 @@ func get_canvas_state(canvas_id: String) -> Dictionary:
func _process(_delta: float) -> void:
if not _active:
return
if not can_activate():
deactivate()
return
_update_aim()
if _placing_grid:
return
@ -1874,8 +1864,10 @@ func _execute_armed_guide_action() -> void:
_restore_selected_guide()
GuideAction.FINALIZE:
_finalize_selected_guide()
_clear_armed_guide_action(true)
_emit_hud_state("")
# Guide actions are persistent modes. Keep the selected action armed so
# multiple grids can be handled without returning to the toolbar after
# every click; selecting the active toggle again explicitly exits it.
_emit_hud_state(_guide_action_status(action))
func _cancel_armed_guide_action() -> void:
@ -1904,12 +1896,18 @@ func _guide_action_verb(action: int) -> String:
GuideAction.HIDE:
return "hide"
GuideAction.RESTORE:
return "restore"
return "show"
GuideAction.FINALIZE:
return "finish"
return "use"
func _guide_action_status(action: int) -> String:
return "%s grid mode • click grids • toggle again to exit" % (
_guide_action_verb(action).capitalize()
)
func _remove_selected_guide() -> void:
if _selected_canvas_id.is_empty():
_emit_hud_state("aim at a placed grid before removing its guide")
@ -2163,7 +2161,7 @@ func _on_peer_removed(peer_id: int) -> void:
func _on_local_art_entitlement_changed() -> void:
if _active and not _owns_art_kit():
if _active and not can_activate():
deactivate()
_publish_local_entitlement()

View file

@ -5,7 +5,7 @@ render_mode unshaded, cull_back;
// base color and the red, green, and blue channels select the three authored
// accent colors. Alpha is intentionally ignored so artists may leave unused
// UV space transparent.
uniform sampler2D pattern_texture : filter_linear_mipmap_anisotropic, repeat_disable;
uniform sampler2D pattern_texture : filter_nearest, repeat_disable;
uniform vec4 base_color : source_color = vec4(1.0);
uniform vec4 red_zone_color : source_color = vec4(0.9, 0.8, 0.6, 1.0);
uniform vec4 green_zone_color : source_color = vec4(0.4, 0.2, 0.1, 1.0);

View file

@ -23,10 +23,12 @@ visible = false
[node name="HeldArtKitSprite" type="Sprite3D" parent="HeldArtKitDisplay"]
unique_name_in_owner = true
rotation = Vector3(0, 0, 4.71239)
pixel_size = 0.008
flip_h = true
shaded = false
double_sided = true
texture_filter = 1
texture_filter = 0
[node name="CatchDisplay" type="Node3D" parent="."]
unique_name_in_owner = true

View file

@ -34,6 +34,7 @@ const FishingRodAttachmentScene = preload(
const HeldItemAttachmentScene = preload(
"res://player/held_item_attachment.tscn"
)
const SprintDustTrailType = preload("res://player/sprint_dust_trail.gd")
signal retract_visual_finished
@ -90,6 +91,7 @@ const BLINK_DURATION_SECONDS: float = 0.11
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
const BASE_REEL_SPEED: float = 0.16
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
# The target Android handheld exposes its physical right trigger through
# Godot's left-trigger axis. Keep the role named here so the platform mapping
# remains isolated from camera behavior.
@ -348,6 +350,7 @@ var _held_item_attachment: BoneAttachment3D
var _held_fish_display: Node3D
var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
var _held_fish_sprite: Sprite3D
var _held_art_kit_display: Node3D
var _held_art_kit_sprite: Sprite3D
var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
@ -424,8 +427,11 @@ var _fishing_rod: Node3D
var _fishing_rod_tip: Marker3D
var _active_item_is_rod: bool = false
var _controller_mapping_manager: ControllerMappingManagerType
var _sprint_dust_landing_ready: bool = false
var _sprint_dust_airborne: bool = false
var _sprint_dust_fall_speed: float = 0.0
@onready var _sprint_dust: CPUParticles3D = %SprintDust
@onready var _sprint_dust: SprintDustTrailType = %SprintDust
func _ready() -> void:
@ -505,6 +511,7 @@ func _initialize_held_item_attachment() -> void:
_held_fish_sprite = attachment.get_node(
"HeldFishDisplay/HeldFishSprite"
) as Sprite3D
_held_art_kit_display = attachment.get_node("HeldArtKitDisplay") as Node3D
_held_art_kit_sprite = attachment.get_node(
"HeldArtKitDisplay/HeldArtKitSprite"
) as Sprite3D
@ -608,7 +615,7 @@ func _physics_process(delta: float) -> void:
func _process(delta: float) -> void:
_update_blink(delta)
_update_character_animation()
_update_sprint_dust()
_update_sprint_dust(delta)
# The hand bone supplies the attachment position, but its animated wrist
# rotation should not turn the flat fish/catch artwork edge-on. Keep each
# display aligned with the character's facing while it follows the hand.
@ -694,24 +701,54 @@ func _schedule_next_blink() -> void:
)
func _update_sprint_dust() -> void:
func _update_sprint_dust(delta: float) -> void:
if _sprint_dust == null:
return
var horizontal_speed_squared: float = (
velocity.x * velocity.x + velocity.z * velocity.z
)
var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
var grounded: bool = is_on_floor()
if _water_recovery_active:
_sprint_dust_airborne = false
_sprint_dust_fall_speed = 0.0
elif not _sprint_dust_landing_ready:
# Establish a real ground contact before arming landing effects. This
# prevents the player's initial placement from looking like a fall.
_sprint_dust_landing_ready = grounded
elif not grounded:
_sprint_dust_airborne = true
_sprint_dust_fall_speed = maxf(
_sprint_dust_fall_speed,
maxf(-velocity.y, 0.0),
)
elif _sprint_dust_airborne:
if _sprint_dust_fall_speed >= LANDING_DUST_MIN_FALL_SPEED:
var landing_facing: Vector3 = horizontal_velocity.normalized()
if landing_facing.is_zero_approx() and _visuals != null:
landing_facing = -_visuals.global_basis.z
_sprint_dust.emit_landing_burst(
global_position,
landing_facing,
)
_sprint_dust_airborne = false
_sprint_dust_fall_speed = 0.0
var horizontal_speed_squared: float = horizontal_velocity.length_squared()
var fastest_non_sprint_speed := maxf(
walk_speed,
maxf(sneak_speed, slow_walk_speed)
)
var running_threshold := fastest_non_sprint_speed + 0.5
_sprint_dust.emitting = (
is_on_floor()
var should_emit: bool = (
grounded
and not _sitting
and not _water_recovery_active
and horizontal_speed_squared
> running_threshold * running_threshold
)
_sprint_dust.update_trail(
delta,
global_position,
horizontal_velocity,
should_emit,
)
func _get_controller_camera_stick() -> Vector2:
@ -1675,6 +1712,8 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
_held_art_kit_visible = true
_held_art_kit_sprite.texture = icon
_held_art_kit_sprite.visible = true
if not previous_show_pose:
_held_art_kit_display.visible = false
_fishing_rod.visible = false
if item_changed:
_begin_pocket_visual(
@ -1682,8 +1721,11 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
previous_show_pose,
true,
Callable(),
false,
_apply_active_art_kit_visual,
)
else:
_apply_active_art_kit_visual()
_update_character_animation()
@ -1763,6 +1805,7 @@ func _apply_pending_held_fish() -> void:
func _clear_active_art_kit() -> void:
var was_visible: bool = _held_art_kit_visible
_held_art_kit_visible = false
_held_art_kit_display.visible = false
_held_art_kit_sprite.texture = null
_held_art_kit_sprite.visible = false
if was_visible:
@ -1772,6 +1815,12 @@ func _clear_active_art_kit() -> void:
_update_character_animation()
func _apply_active_art_kit_visual() -> void:
if not _held_art_kit_visible or _held_art_kit_sprite.texture == null:
return
_held_art_kit_display.visible = true
func _begin_pocket_visual(
target: PocketVisualTarget,
starts_in_show_pose: bool,

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=21 format=3]
[gd_scene load_steps=22 format=3]
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
@ -15,6 +15,7 @@
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
radius = 0.45
@ -25,7 +26,7 @@ material = ExtResource("12_blob_shadow")
size = Vector2(1.15, 0.72)
[sub_resource type="Shader" id="SprintDustShader"]
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec4 dust_color : source_color = vec4(0.47, 0.39, 0.27, 0.42);\nvarying float particle_phase;\n\nvoid vertex() {\n\tparticle_phase = INSTANCE_CUSTOM.y;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble = sin(angle * 5.0 + particle_phase * 8.0) * 0.07;\n\tfloat radius = length(centered_uv);\n\tfloat soft_shape = 1.0 - smoothstep(0.48 + edge_wobble, 1.0 + edge_wobble, radius);\n\tfloat fade_in = smoothstep(0.0, 0.12, particle_phase);\n\tfloat fade_out = 1.0 - smoothstep(0.38, 1.0, particle_phase);\n\tALBEDO = dust_color.rgb;\n\tALPHA = soft_shape * dust_color.a * fade_in * fade_out;\n}\n"
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec3 dust_color : source_color = vec3(0.61, 0.57, 0.53);\nvarying flat float puff_phase;\nvarying flat float puff_alpha;\n\nvoid vertex() {\n\tpuff_phase = INSTANCE_CUSTOM.x;\n\tpuff_alpha = INSTANCE_CUSTOM.w;\n\tVERTEX.xy *= INSTANCE_CUSTOM.yz;\n\tfloat puff_rotation = puff_phase * 6.28318530718;\n\tfloat rotation_sine = sin(puff_rotation);\n\tfloat rotation_cosine = cos(puff_rotation);\n\tVERTEX.xy = mat2(\n\t\tvec2(rotation_cosine, -rotation_sine),\n\t\tvec2(rotation_sine, rotation_cosine)\n\t) * VERTEX.xy;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble =\n\t\tsin(angle * 5.0 + puff_phase * 8.0) * 0.07\n\t\t+ sin(angle * 3.0 - puff_phase * 5.0) * 0.025;\n\tfloat radius = length(centered_uv);\n\tif (radius > 0.82 + edge_wobble) {\n\t\tdiscard;\n\t}\n\tALBEDO = dust_color;\n\tALPHA = puff_alpha;\n}\n"
[sub_resource type="ShaderMaterial" id="SprintDustMaterial"]
render_priority = 1
@ -33,7 +34,7 @@ shader = SubResource("SprintDustShader")
[sub_resource type="QuadMesh" id="SprintDustMesh"]
material = SubResource("SprintDustMaterial")
size = Vector2(0.38, 0.28)
size = Vector2(1, 1)
[node name="Player" type="CharacterBody3D"]
collision_layer = 2
@ -66,27 +67,11 @@ rotation_degrees = Vector3(-90, 0, 0)
cast_shadow = 0
mesh = SubResource("GroundShadowMesh")
[node name="SprintDust" type="CPUParticles3D" parent="."]
[node name="SprintDust" type="MultiMeshInstance3D" parent="."]
unique_name_in_owner = true
position = Vector3(0, 0.08, 0)
emitting = false
amount = 16
lifetime = 0.65
randomness = 0.55
visibility_aabb = AABB(-4, -2, -4, 8, 5, 8)
local_coords = false
emission_shape = 3
emission_box_extents = Vector3(0.24, 0.03, 0.16)
direction = Vector3(0, 1, 0)
spread = 65.0
gravity = Vector3(0, -0.8, 0)
initial_velocity_min = 0.35
initial_velocity_max = 0.8
angular_velocity_min = -90.0
angular_velocity_max = 90.0
scale_amount_min = 0.35
scale_amount_max = 0.72
mesh = SubResource("SprintDustMesh")
cast_shadow = 0
script = ExtResource("16_sprint_dust")
puff_mesh = SubResource("SprintDustMesh")
[node name="CharacterRig" parent="Visuals" instance=ExtResource("13_character")]
rotation_degrees = Vector3(0, 180, 0)

View file

@ -304,6 +304,7 @@ static func _runtime_fur_pattern_material(
static func _configure_matte_material(material: StandardMaterial3D) -> void:
if material == null:
return
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
# Character artwork is deliberately graphic and flat. Unshaded rendering
# keeps the authored fur and facial colors invariant across world weather,
# time of day, and the isolated Profile preview.

249
player/sprint_dust_trail.gd Normal file
View file

@ -0,0 +1,249 @@
class_name SprintDustTrail
extends MultiMeshInstance3D
const PUFF_COUNT: int = 12
const PUFF_SPACING: float = 0.46
const PUFF_LIFETIME: float = 0.62
const PUFF_GROW_TIME: float = 0.16
const PUFF_FADE_TIME: float = 0.22
const PUFF_BACK_OFFSET: float = 0.14
const PUFF_SIDE_OFFSET: float = 0.11
const PUFF_HEIGHT: float = 0.08
const PUFF_MIN_SCALE: float = 0.22
@export var puff_mesh: Mesh
var _puff_positions: Array[Vector3] = []
var _puff_ages := PackedFloat32Array()
var _puff_widths := PackedFloat32Array()
var _puff_heights := PackedFloat32Array()
var _puff_phases := PackedFloat32Array()
var _next_puff: int = 0
var _spawn_serial: int = 0
var _foot_side: float = -1.0
var _tracking_run: bool = false
var _previous_source_position: Vector3
var _distance_since_puff: float = 0.0
func _ready() -> void:
top_level = true
global_transform = Transform3D.IDENTITY
_initialize_pool()
func update_trail(
delta: float,
source_position: Vector3,
movement_velocity: Vector3,
is_running: bool,
) -> void:
_update_existing_puffs(delta)
custom_aabb = AABB(
source_position - Vector3(5.0, 2.0, 5.0),
Vector3(10.0, 5.0, 10.0),
)
if not is_running:
_tracking_run = false
_distance_since_puff = 0.0
return
var flat_source := Vector3(
source_position.x,
source_position.y,
source_position.z,
)
if not _tracking_run:
_tracking_run = true
_previous_source_position = flat_source
_distance_since_puff = 0.0
return
var segment := flat_source - _previous_source_position
segment.y = 0.0
var remaining_distance: float = segment.length()
if remaining_distance <= 0.0001:
_previous_source_position = flat_source
return
var segment_direction: Vector3 = segment / remaining_distance
var movement_direction := Vector3(
movement_velocity.x,
0.0,
movement_velocity.z,
).normalized()
if movement_direction.is_zero_approx():
movement_direction = segment_direction
var cursor: Vector3 = _previous_source_position
var distance_to_next: float = PUFF_SPACING - _distance_since_puff
while remaining_distance >= distance_to_next:
cursor += segment_direction * distance_to_next
_spawn_puff(cursor, movement_direction)
remaining_distance -= distance_to_next
_distance_since_puff = 0.0
distance_to_next = PUFF_SPACING
_distance_since_puff += remaining_distance
_previous_source_position = flat_source
func clear_trail() -> void:
_tracking_run = false
_distance_since_puff = 0.0
for index: int in PUFF_COUNT:
_puff_ages[index] = PUFF_LIFETIME
_write_inactive_puff(index)
func emit_landing_burst(
source_position: Vector3,
facing_direction: Vector3,
) -> void:
var direction := Vector3(
facing_direction.x,
0.0,
facing_direction.z,
).normalized()
if direction.is_zero_approx():
direction = Vector3.FORWARD
var first_direction: Vector3 = -direction
for puff_index: int in 5:
var spread_direction := first_direction.rotated(
Vector3.UP,
TAU * float(puff_index) / 5.0,
)
var radius: float = lerpf(
0.38,
0.46,
_stable_variation(_spawn_serial + puff_index + 81),
)
_spawn_puff_at(
source_position
+ spread_direction * radius
+ Vector3.UP * PUFF_HEIGHT,
0.95,
)
func get_active_puff_count() -> int:
var active_count: int = 0
for age: float in _puff_ages:
if age < PUFF_LIFETIME:
active_count += 1
return active_count
func get_first_active_puff_position() -> Vector3:
for index: int in PUFF_COUNT:
if _puff_ages[index] < PUFF_LIFETIME:
return _puff_positions[index]
return Vector3.INF
func _initialize_pool() -> void:
var pool := MultiMesh.new()
pool.transform_format = MultiMesh.TRANSFORM_3D
pool.use_custom_data = true
pool.instance_count = PUFF_COUNT
pool.mesh = puff_mesh
multimesh = pool
_puff_positions.resize(PUFF_COUNT)
_puff_ages.resize(PUFF_COUNT)
_puff_widths.resize(PUFF_COUNT)
_puff_heights.resize(PUFF_COUNT)
_puff_phases.resize(PUFF_COUNT)
clear_trail()
func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
var lateral := Vector3(-direction.z, 0.0, direction.x)
var position := (
source_position
- direction * PUFF_BACK_OFFSET
+ lateral * PUFF_SIDE_OFFSET * _foot_side
+ Vector3.UP * PUFF_HEIGHT
)
_spawn_puff_at(position)
_foot_side *= -1.0
func _spawn_puff_at(
position: Vector3,
size_multiplier: float = 1.0,
) -> void:
var variation: float = _stable_variation(_spawn_serial)
var second_variation: float = _stable_variation(_spawn_serial + 37)
var index: int = _next_puff
_puff_positions[index] = position
_puff_ages[index] = 0.0
_puff_widths[index] = (
lerpf(0.34, 0.43, variation) * size_multiplier
)
_puff_heights[index] = (
lerpf(0.23, 0.30, second_variation) * size_multiplier
)
_puff_phases[index] = variation
_write_puff(index, PUFF_MIN_SCALE, 1.0)
_next_puff = (_next_puff + 1) % PUFF_COUNT
_spawn_serial += 1
func _update_existing_puffs(delta: float) -> void:
if multimesh == null:
return
for index: int in PUFF_COUNT:
var age: float = _puff_ages[index]
if age >= PUFF_LIFETIME:
continue
age += delta
_puff_ages[index] = age
if age >= PUFF_LIFETIME:
_write_inactive_puff(index)
continue
var visual_scale: float = 1.0
if age < PUFF_GROW_TIME:
var grow_weight: float = smoothstep(
0.0,
1.0,
age / PUFF_GROW_TIME,
)
visual_scale = lerpf(PUFF_MIN_SCALE, 1.0, grow_weight)
var visual_alpha: float = 1.0
if age > PUFF_LIFETIME - PUFF_FADE_TIME:
var fade_weight: float = smoothstep(
0.0,
1.0,
(age - (PUFF_LIFETIME - PUFF_FADE_TIME))
/ PUFF_FADE_TIME,
)
visual_alpha = 1.0 - fade_weight
_write_puff(index, visual_scale, visual_alpha)
func _write_puff(
index: int,
visual_scale: float,
visual_alpha: float,
) -> void:
var puff_transform := Transform3D.IDENTITY
puff_transform.origin = _puff_positions[index]
multimesh.set_instance_transform(index, puff_transform)
multimesh.set_instance_custom_data(
index,
Color(
_puff_phases[index],
_puff_widths[index] * visual_scale,
_puff_heights[index] * visual_scale,
visual_alpha,
),
)
func _write_inactive_puff(index: int) -> void:
var inactive_transform := Transform3D.IDENTITY
inactive_transform.origin = _puff_positions[index]
multimesh.set_instance_transform(index, inactive_transform)
multimesh.set_instance_custom_data(index, Color(0.0, 0.0, 0.0, 0.0))
func _stable_variation(serial: int) -> float:
return fposmod(sin(float(serial + 1) * 12.9898) * 43758.5453, 1.0)

View file

@ -0,0 +1 @@
uid://o2gi5oqunsp8

View file

@ -16,10 +16,18 @@ run/main_scene="res://main/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://art/exported/system_icons/netfishing_256.png"
[autoload]
TextureSamplingPolicy="*res://main/texture_sampling_policy.gd"
[display]
window/stretch/aspect="expand"
[editor_plugins]
enabled=PackedStringArray("res://addons/netfishing_shoreline_baker/plugin.cfg")
[gui]
theme/custom="res://ui/game_theme.tres"

View file

@ -10,7 +10,7 @@ const DEFAULT_PROFILE: Dictionary = {
"compress/hdr_compression": 1,
"compress/normal_map": 0,
"compress/channel_pack": 0,
"mipmaps/generate": true,
"mipmaps/generate": false,
"mipmaps/limit": -1,
"roughness/mode": 0,
"roughness/src_normal": "",

View file

@ -21,6 +21,7 @@ readonly -a QUICK_TESTS=(
"tests/shoreline_ambience_validation.gd"
"tests/surface_drawing_validation.gd"
"tests/terrain_blender_material_validation.gd"
"tests/texture_sampling_validation.gd"
"tests/world_time_validation.gd"
"tests/world_weather_validation.gd"
)

View file

@ -27,6 +27,55 @@ func _run() -> void:
await physics_frame
var player := main.get("_player") as Player
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
assert(sprint_dust != null)
assert(sprint_dust.get_active_puff_count() == 0)
sprint_dust.update_trail(
0.016,
Vector3.ZERO,
Vector3(4.5, 0.0, 0.0),
true,
)
assert(sprint_dust.get_active_puff_count() == 0)
sprint_dust.update_trail(
0.016,
Vector3(0.5, 0.0, 0.0),
Vector3(4.5, 0.0, 0.0),
true,
)
assert(sprint_dust.get_active_puff_count() == 1)
var planted_puff_position: Vector3 = (
sprint_dust.get_first_active_puff_position()
)
sprint_dust.update_trail(
0.08,
Vector3(0.75, 0.0, 0.0),
Vector3(4.5, 0.0, 0.0),
true,
)
assert(
sprint_dust.get_first_active_puff_position()
== planted_puff_position
)
sprint_dust.update_trail(
0.08,
Vector3(0.75, 0.0, 0.0),
Vector3.ZERO,
false,
)
assert(sprint_dust.get_active_puff_count() == 1)
sprint_dust.clear_trail()
assert(sprint_dust.get_active_puff_count() == 0)
sprint_dust.emit_landing_burst(Vector3.ZERO, Vector3.FORWARD)
assert(sprint_dust.get_active_puff_count() == 5)
sprint_dust.update_trail(
0.08,
Vector3.ZERO,
Vector3.ZERO,
false,
)
assert(sprint_dust.get_active_puff_count() == 5)
sprint_dust.clear_trail()
var service := main.get_node(
"%NetworkSurfaceDrawingService"
) as NetworkSurfaceDrawingService
@ -210,9 +259,8 @@ func _run() -> void:
assert(game_ui.is_gameplay_hud_hidden())
assert(player_menu.visible)
assert(root.gui_get_focus_owner() is not LineEdit)
player_menu.close_menu()
for _frame: int in 12:
await process_frame
player_menu.close_menu(PlayerMenu.CloseReason.SESSION_END)
await process_frame
game_ui.call("_on_quick_action_selected", &"hud")
await process_frame
assert(not game_ui.is_gameplay_hud_hidden())
@ -220,14 +268,17 @@ func _run() -> void:
assert(not service.can_activate())
assert(not service.is_active() and not toolbar.visible)
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
assert(service.can_activate())
player.hotbar.select_slot(1)
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
assert(not service.can_activate())
var paint_key := InputEventKey.new()
paint_key.physical_keycode = KEY_P
paint_key.pressed = true
assert(service.handle_input(paint_key, true))
assert(not service.handle_input(paint_key, true))
assert(not service.is_active())
player.hotbar.select_slot(0)
await process_frame
assert(service.is_active() and service.is_placement_mode())
assert(service.is_active() and not service.is_placement_mode())
assert(toolbar.visible)
var ui_root := game_ui.get_node("%UIRoot") as Control
assert(toolbar.get_parent() == ui_root)
@ -242,6 +293,8 @@ func _run() -> void:
assert(toolbar.get_global_rect().end.y <= 720.0)
var top_panel := toolbar.get_node("%TopPanel") as PanelContainer
var color_panel := toolbar.get_node("%ColorPanel") as PanelContainer
assert(is_equal_approx(toolbar.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
assert(is_equal_approx(top_panel.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
assert(top_panel.position.is_equal_approx(Vector2.ZERO))
assert(color_panel.position.x > 0.0)
assert(is_equal_approx(color_panel.get_rect().end.x, top_panel.get_rect().end.x))
@ -266,7 +319,6 @@ func _run() -> void:
var hide_button := toolbar.get_node("%HideGuideButton") as Button
var restore_button := toolbar.get_node("%RestoreGuideButton") as Button
var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button
var close_button := toolbar.get_node("%CloseButton") as Button
assert(
mode_button != null
and eraser_button != null
@ -274,8 +326,44 @@ func _run() -> void:
and hide_button != null
and restore_button != null
and finalize_button != null
and close_button != null
)
assert(toolbar.get_node_or_null("%CloseButton") == null)
var expected_toolbar_icons: Dictionary[Button, String] = {
mode_button: "/art/art_kit_marker.png",
eraser_button: "/art/art_kit_eraser.png",
hide_button: "/art/art_kit_grid_hidden_light.png",
restore_button: "/art/art_kit_grid_restore_light.png",
finalize_button: "/art/art_kit_grid_finish_light.png",
}
for icon_button: Button in expected_toolbar_icons:
assert(icon_button.text.is_empty())
assert(icon_button.icon != null)
assert(
icon_button.icon.resource_path.ends_with(
expected_toolbar_icons[icon_button]
)
)
for icon_button: Button in [
eraser_button,
undo_button,
hide_button,
restore_button,
finalize_button,
]:
assert(icon_button.custom_minimum_size == Vector2(48, 44))
assert(icon_button.get_theme_constant("icon_max_width") == 40)
var icon_style := (
icon_button.get_theme_stylebox("normal") as StyleBoxFlat
)
assert(is_equal_approx(icon_style.content_margin_left, 4.0))
assert(is_equal_approx(icon_style.content_margin_top, 2.0))
assert(mode_button.custom_minimum_size == Vector2(48, 48))
assert(mode_button.get_theme_constant("icon_max_width") == 40)
assert(mode_button.get_index() > finalize_button.get_index())
assert(hide_button.button_group != null)
assert(hide_button.button_group == restore_button.button_group)
assert(hide_button.button_group == finalize_button.button_group)
assert(hide_button.button_group.allow_unpress)
assert(brush_option.item_count == 4)
assert(grid_option.item_count == 4)
assert(not brush_option.get_popup().is_item_disabled(0))
@ -303,7 +391,12 @@ func _run() -> void:
assert(service.get_grid_size() == 128)
mode_button.pressed.emit()
assert(not service.is_placement_mode())
assert(service.is_placement_mode())
assert(
mode_button.icon.resource_path.ends_with(
"/art/art_kit_grid_light.png"
)
)
eraser_button.pressed.emit()
assert(service.is_eraser_mode())
assert(eraser_button.button_pressed)
@ -318,60 +411,112 @@ func _run() -> void:
world_click.button_index = MOUSE_BUTTON_LEFT
world_click.pressed = true
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.HIDE
)
assert(service.is_placement_mode())
assert(not service.is_eraser_mode())
assert(hide_button.button_pressed)
hide_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode())
assert(service.is_eraser_mode())
assert(not service.is_placement_mode() and service.is_eraser_mode())
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.RESTORE
)
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
assert(not restore_button.button_pressed)
assert(finalize_button.button_pressed)
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
(color_buttons[&"ocean_teal"] as Button).pressed.emit()
assert(not service.is_eraser_mode())
undo_button.pressed.emit()
assert(service.is_active())
close_button.pressed.emit()
assert(not service.is_active() and not toolbar.visible)
assert(service.handle_input(paint_key, true))
await process_frame
assert(not service.handle_input(paint_key, true))
assert(service.is_active() and toolbar.visible)
assert(service.handle_input(paint_key, true))
await process_frame
assert(not service.is_active() and not toolbar.visible)
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
player.hotbar.select_slot(0)
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
await process_frame
var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
var pocket_animation_player := player.get(
"_character_animation_player"
) as AnimationPlayer
var pocket_animation_name: StringName = player.get(
"_pocket_visual_animation"
)
if (
not held_art_kit_display.visible
and pocket_animation_player != null
and not pocket_animation_name.is_empty()
and pocket_animation_player.has_animation(pocket_animation_name)
):
var pocket_animation: Animation = (
pocket_animation_player.get_animation(pocket_animation_name)
)
await create_timer(pocket_animation.length * 0.55).timeout
assert(held_art_kit_display.visible)
assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
assert(is_zero_approx(angle_difference(
held_art_kit_sprite.rotation.z,
-PI * 0.5,
)))
assert(held_art_kit_sprite.flip_h)
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
assert(service.is_active() and toolbar.visible)
assert(service.get_grid_size() == 128)
assert(service.handle_input(world_click, true))
assert(service.is_active() and toolbar.visible)
player.hotbar.select_slot(1)
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
var wheel_down := InputEventMouseButton.new()
wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
wheel_down.pressed = true
assert(not service.handle_input(wheel_down, true))
hotbar_ui._unhandled_input(wheel_down)
await process_frame
assert(player.hotbar.get_selected_slot() == 1)
assert(not service.is_active() and not toolbar.visible)
assert(service.handle_input(paint_key, true))
var number_one := InputEventKey.new()
number_one.physical_keycode = KEY_1
number_one.pressed = true
assert(not service.handle_input(number_one, true))
hotbar_ui._unhandled_input(number_one)
await process_frame
assert(service.is_active() and toolbar.visible)
assert(player.hotbar.get_selected_slot() == 0)
var number_two := InputEventKey.new()
number_two.physical_keycode = KEY_2
number_two.pressed = true
assert(not service.handle_input(number_two, true))
hotbar_ui._unhandled_input(number_two)
await process_frame
assert(player.hotbar.get_selected_slot() == 1)
assert(not service.is_active() and not toolbar.visible)
game_ui.call("_on_quick_action_selected", &"paint")
await process_frame
assert(player.hotbar.get_selected_slot() == 0)
assert(service.is_active() and toolbar.visible)
print("Art tools validation: PASS")
var session := main.get_node("%NetworkSession") as NetworkSession

View file

@ -47,6 +47,10 @@ func _run() -> void:
assert(body != null)
var body_material := body.material_override as StandardMaterial3D
assert(body_material != null)
assert(
body_material.texture_filter
== BaseMaterial3D.TEXTURE_FILTER_NEAREST
)
assert(body_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
assert(is_zero_approx(body_material.metallic))
assert(is_zero_approx(body_material.metallic_specular))

View file

@ -511,6 +511,8 @@ func _test_host_art_shop_purchase(
ArtShopStock.ART_KIT_ITEM_ID
)
assert(art_item != null and art_item.hotbar_allowed and art_item.equippable)
assert(art_item.icon != null)
assert(art_item.icon.resource_path.ends_with("/art/art_kit.png"))
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
for product_id: StringName in [
&"marker_ocean_teal", &"brush_2x", &"grid_32x",
@ -568,6 +570,41 @@ 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 upgrade_grid := shop.get_node("%UpgradeGrid") as GridContainer
assert(upgrade_grid != null and upgrade_grid.columns == 3)
assert((shop.get_node("%Upgrades") as Control).visible)
assert(not (shop.get_node("%Supplies") as Control).visible)
for upgrade_name: String in [
"ReelPurchase", "BarrierPurchase", "CoolerPurchase"
]:
var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
assert(upgrade_button != null)
assert(upgrade_button.size == Vector2(144.0, 144.0))
assert(upgrade_button.text.is_empty())
assert(upgrade_button.icon != null)
assert(upgrade_button.tooltip_text.contains("level"))
assert(not shop.has_node("%ReelLevel"))
assert(not shop.has_node("%BarrierEffect"))
assert(not shop.has_node("%CoolerLevel"))
var balance_coin := shop.get_node(
"ShopPanel/Margin/Layout/Header/BalanceDisplay/CoinIcon"
) as TextureRect
assert(balance_coin != null and balance_coin.texture != null)
assert(
balance_coin.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
for cost_name: String in ["ReelCost", "BarrierCost", "CoolerCost"]:
var cost_display := shop.get_node("%%%s" % cost_name) as CurrencyAmount
assert(cost_display != null)
var cost_icon := cost_display.get_node("Icon") as TextureRect
assert(cost_icon != null and cost_icon.texture != null)
assert(
cost_icon.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
assert(not shop.has_node("ShopPanel/Margin/Layout/ModeTabs"))
var shop_tabs: Array = shop.get("_shop_tabs") as Array
assert(shop_tabs.size() == 6)
@ -582,6 +619,37 @@ func _test_fishing_shop_sale_ui(
if child is Label:
stock_sections.append((child as Label).text)
assert(stock_sections == ["art kit", "markers", "brushes", "grids"])
var marker_icons := shop.find_children(
"MarkerIcon", "TextureRect", true, false
)
assert(marker_icons.size() == ArtShopStock.MARKER_PRODUCTS.size())
for index: int in marker_icons.size():
var marker_icon := marker_icons[index] as TextureRect
var marker_material := marker_icon.material as ShaderMaterial
var product_id: StringName = ArtShopStock.MARKER_PRODUCTS[index]
var color_id: StringName = PlayerArtUnlocks.color_id_for_product(
product_id
)
assert(marker_icon.texture.resource_path.ends_with("art_kit_marker.png"))
assert(marker_material != null)
assert(
marker_material.get_shader_parameter("marker_color")
== SurfaceDrawingPalette.get_color(color_id)
)
var price_bubbles := shop.find_children(
"PriceBubble", "PanelContainer", true, false
)
assert(not price_bubbles.is_empty())
for price_bubble: Node in price_bubbles:
var currency_icon := price_bubble.find_child(
"CurrencyIcon", true, false
) as TextureRect
assert(currency_icon != null and currency_icon.texture != null)
assert(
currency_icon.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
await _activate_pointer_control(sell_mode, ui_viewport)
await process_frame
assert(shop.visible and not player_menu.visible)

View file

@ -189,6 +189,27 @@ func _validate_starter_region_surfaces() -> void:
assert(not covered_ocean.is_fishable())
assert(covered_ocean.position.y > -0.45)
# The ocean fishing layer covers the island's complete editable footprint.
# Terrain at or above the waterline remains authoritative, while shoreline
# terrain lowered below the surface becomes fishable without reshaping a
# manually authored exclusion ring.
var editable_shallows_found := 0
for x_position: int in range(-24, 24, 2):
for z_position: int in range(-21, 22, 2):
var candidate: FishingSurfaceSampleType = (
fishing_spot.resolve_fishing_surface(
Vector3(x_position, -0.45, z_position),
4.0,
)
)
if (
candidate.is_fishable()
and candidate.water_region.water_type
== WaterType.Type.SALT_WATER
):
editable_shallows_found += 1
assert(editable_shallows_found > 0)
fishing_spot.queue_free()
region.queue_free()
await process_frame

View file

@ -0,0 +1,97 @@
extends SceneTree
const ShorelineMesh: ArrayMesh = preload(
"res://world/generated/shorelines/starter_ocean_shoreline.tres"
)
const GEOMETRY_EPSILON := 0.0001
const CROSSING_NEIGHBORHOOD := 32
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
assert(ShorelineMesh.get_surface_count() == 1)
var arrays := ShorelineMesh.surface_get_arrays(0)
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
assert(not vertices.is_empty())
assert(vertices.size() % 2 == 0)
assert(indices.size() == vertices.size() * 3)
var point_count := vertices.size() / 2
var land_edge := PackedVector2Array()
var water_edge := PackedVector2Array()
for index: int in point_count:
var land := vertices[index * 2]
var water := vertices[index * 2 + 1]
land_edge.append(Vector2(land.x, land.z))
water_edge.append(Vector2(water.x, water.z))
var ribbon_width := land.distance_to(water)
assert(ribbon_width >= 0.34)
assert(ribbon_width <= 1.5)
var crossing_count := (
_count_local_crossings(water_edge)
+ _count_local_crossings(land_edge)
)
if crossing_count > 0:
push_error(
"Shoreline ribbon contains %d local edge crossings."
% crossing_count
)
quit(1)
return
print("Shoreline ribbon validation: PASS")
quit()
func _count_local_crossings(points: PackedVector2Array) -> int:
var crossing_count := 0
for first: int in points.size():
var first_next := (first + 1) % points.size()
var checked_neighbors := mini(
CROSSING_NEIGHBORHOOD,
points.size() - 2,
)
for offset: int in range(2, checked_neighbors + 1):
var second := (first + offset) % points.size()
var second_next := (second + 1) % points.size()
if second_next == first:
continue
if _segments_cross(
points[first],
points[first_next],
points[second],
points[second_next],
):
crossing_count += 1
return crossing_count
func _segments_cross(
a: Vector2,
b: Vector2,
c: Vector2,
d: Vector2,
) -> bool:
if (
maxf(a.x, b.x) < minf(c.x, d.x) - GEOMETRY_EPSILON
or maxf(c.x, d.x) < minf(a.x, b.x) - GEOMETRY_EPSILON
or maxf(a.y, b.y) < minf(c.y, d.y) - GEOMETRY_EPSILON
or maxf(c.y, d.y) < minf(a.y, b.y) - GEOMETRY_EPSILON
):
return false
var first_direction := b - a
var second_direction := d - c
var denominator := first_direction.cross(second_direction)
if absf(denominator) <= GEOMETRY_EPSILON:
return false
var offset := c - a
var first_amount := offset.cross(second_direction) / denominator
var second_amount := offset.cross(first_direction) / denominator
return (
first_amount > GEOMETRY_EPSILON
and first_amount < 1.0 - GEOMETRY_EPSILON
and second_amount > GEOMETRY_EPSILON
and second_amount < 1.0 - GEOMETRY_EPSILON
)

View file

@ -0,0 +1 @@
uid://c73frygpfhred

View file

@ -32,6 +32,8 @@ func _run_host() -> void:
await physics_frame
var player := main.get("_player") as Player
assert(player.bag.add_item(&"art_kit", 1))
assert(player.hotbar.assign_item(0, &"art_kit"))
assert(player.hotbar.select_slot(0))
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
var service := main.get_node(
@ -140,6 +142,8 @@ func _run_client() -> void:
))
var player := main.get("_player") as Player
assert(player.bag.add_item(&"art_kit", 1))
assert(player.hotbar.assign_item(0, &"art_kit"))
assert(player.hotbar.select_slot(0))
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
var service := main.get_node(

View file

@ -29,12 +29,15 @@ func _run() -> void:
var player := main.get("_player") as Player
assert(player.bag.add_item(&"art_kit", 1))
assert(player.hotbar.assign_item(0, &"art_kit"))
assert(player.hotbar.select_slot(0))
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
var service := main.get_node(
"%NetworkSurfaceDrawingService"
) as NetworkSurfaceDrawingService
assert(service != null)
assert(session.supports_server_capability(SurfaceDrawingProtocol.CAPABILITY))
service.deactivate()
_validate_marker_controls(service, player)
var query := PhysicsRayQueryParameters3D.create(
player.global_position + Vector3.UP * 3.0,
@ -124,7 +127,7 @@ func _validate_marker_controls(
var activation_pointer := Vector2(824.0, 318.0)
service.activate(activation_pointer)
assert(service.is_active())
assert(service.is_placement_mode())
assert(not service.is_placement_mode())
assert(service.get_pointer_screen_position() == activation_pointer)
if DisplayServer.get_name() != "headless":
assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
@ -133,7 +136,7 @@ func _validate_marker_controls(
placement_key.physical_keycode = KEY_R
placement_key.pressed = true
assert(service.handle_input(placement_key, true))
assert(not service.is_placement_mode())
assert(service.is_placement_mode())
var pointer_before: Vector2 = service.get_pointer_screen_position()
var pointer_motion := InputEventMouseMotion.new()
@ -171,6 +174,6 @@ func _validate_marker_controls(
assert(not service.handle_input(camera_release, true))
assert(service.handle_input(placement_key, true))
assert(service.is_placement_mode())
assert(not service.is_placement_mode())
service.deactivate()
assert(Input.mouse_mode == prior_mouse_mode)

View file

@ -0,0 +1,148 @@
extends SceneTree
const POLICY := preload("res://main/texture_sampling_policy.gd")
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_runtime_policy()
_validate_repository_files("res://")
if failures.is_empty():
print("Texture sampling validation passed.")
quit(0)
return
for failure: String in failures:
printerr("Texture sampling validation: ", failure)
quit(1)
func _validate_runtime_policy() -> void:
var texture_rect := TextureRect.new()
texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
POLICY.enforce_node(texture_rect)
_check(
texture_rect.texture_filter == CanvasItem.TEXTURE_FILTER_NEAREST,
"CanvasItem policy must replace linear sampling with nearest sampling.",
)
texture_rect.free()
var sprite := Sprite3D.new()
sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
POLICY.enforce_node(sprite)
_check(
sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"Sprite3D policy must replace linear sampling with nearest sampling.",
)
sprite.free()
var material := StandardMaterial3D.new()
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
var quad := QuadMesh.new()
quad.material = material
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = quad
POLICY.enforce_node(mesh_instance)
_check(
material.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"3D material policy must replace linear sampling with nearest sampling.",
)
mesh_instance.free()
func _validate_repository_files(path: String) -> void:
var directory := DirAccess.open(path)
if directory == null:
failures.append("Could not scan %s." % path)
return
directory.list_dir_begin()
while true:
var name := directory.get_next()
if name.is_empty():
break
if name == ".godot" or name == ".git":
continue
var child_path := path.path_join(name)
if directory.current_is_dir():
_validate_repository_files(child_path)
continue
if name.ends_with(".import"):
_validate_import_file(child_path)
elif name.ends_with(".gdshader"):
_validate_shader_file(child_path)
elif name.ends_with(".tscn") or name.ends_with(".tres"):
_validate_serialized_resource(child_path)
elif name.ends_with(".gd") and child_path != get_script().resource_path:
_validate_script_file(child_path)
directory.list_dir_end()
func _validate_import_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
_check(
"mipmaps/generate=true" not in source,
"Texture import generates mipmaps: %s" % path,
)
func _validate_shader_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
var declaration_start := source.find("uniform sampler")
while declaration_start >= 0:
var declaration_end := source.find(";", declaration_start)
if declaration_end < 0:
_check(false, "Incomplete sampler declaration in %s." % path)
return
var declaration := source.substr(
declaration_start,
declaration_end - declaration_start,
)
_check(
"filter_nearest" in declaration,
"Shader sampler must declare filter_nearest: %s" % path,
)
declaration_start = source.find("uniform sampler", declaration_end + 1)
func _validate_serialized_resource(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
var uses_3d_filter_enum := false
for line: String in source.split("\n"):
if line.begins_with("["):
uses_3d_filter_enum = (
("type=\"Sprite3D\"" in line)
or ("type=\"AnimatedSprite3D\"" in line)
or ("type=\"StandardMaterial3D\"" in line)
or ("type=\"ORMMaterial3D\"" in line)
)
if not line.begins_with("texture_filter = "):
continue
var value := int(line.trim_prefix("texture_filter = "))
if uses_3d_filter_enum:
_check(
value == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"3D resource declares non-nearest filtering: %s" % path,
)
else:
_check(
value == CanvasItem.TEXTURE_FILTER_PARENT_NODE
or value == CanvasItem.TEXTURE_FILTER_NEAREST,
"Canvas resource declares non-nearest filtering: %s" % path,
)
func _validate_script_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
_check(
"TEXTURE_FILTER_LINEAR" not in source,
"Script explicitly requests linear texture filtering: %s" % path,
)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)

View file

@ -0,0 +1 @@
uid://dqm0j1xnkeox1

View file

@ -0,0 +1,15 @@
shader_type canvas_item;
uniform vec4 marker_color : source_color = vec4(1.0, 0.0, 0.0, 1.0);
void fragment() {
vec4 source = texture(TEXTURE, UV) * COLOR;
float red_dominance = source.r - max(source.g, source.b);
float marker_mask = smoothstep(0.04, 0.18, red_dominance);
vec3 colored_marker = marker_color.rgb * source.r;
COLOR = vec4(
mix(source.rgb, colored_marker, marker_mask),
source.a * marker_color.a
);
}

View file

@ -0,0 +1 @@
uid://bsg3cwrnjq6ev

View file

@ -5,6 +5,7 @@ extends PanelContainer
@onready var _heading: Label = %Heading
@onready var _value: Label = %Value
@onready var _currency_icon: TextureRect = %CurrencyIcon
func _ready() -> void:
@ -16,3 +17,10 @@ func _ready() -> void:
func set_content(heading: String, value: String) -> void:
_heading.text = heading
_value.text = value
_currency_icon.visible = false
func set_currency_amount(heading: String, amount: int) -> void:
_heading.text = heading
_value.text = str(amount)
_currency_icon.visible = true

View file

@ -1,7 +1,8 @@
[gd_scene load_steps=3 format=3]
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_status_bubble.gd" id="1_status"]
[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="2_profile"]
[ext_resource type="Texture2D" path="res://items/icons/shop/32_currency.png" id="3_currency"]
[node name="BubbleStatusBubble" type="PanelContainer"]
custom_minimum_size = Vector2(112, 62)
@ -23,7 +24,25 @@ theme_override_font_sizes/font_size = 13
text = "status"
horizontal_alignment = 1
[node name="Value" type="Label" parent="Labels"]
[node name="ValueRow" type="HBoxContainer" parent="Labels"]
layout_mode = 2
mouse_filter = 2
theme_override_constants/separation = 3
alignment = 1
[node name="CurrencyIcon" type="TextureRect" parent="Labels/ValueRow"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(18, 18)
layout_mode = 2
size_flags_vertical = 4
mouse_filter = 2
texture = ExtResource("3_currency")
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="Value" type="Label" parent="Labels/ValueRow"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2

View file

@ -0,0 +1,48 @@
class_name CurrencyAmount
extends HBoxContainer
@export var amount: int = 0:
set(value):
amount = value
_display_text = str(value)
_refresh_amount()
@export_range(8.0, 64.0, 1.0) var icon_size: float = 18.0:
set(value):
icon_size = value
_refresh_icon_size()
@onready var _icon: TextureRect = %Icon
@onready var _amount_label: Label = %Amount
var _display_text: String = "0"
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_refresh_icon_size()
_refresh_amount()
func set_amount(value: int) -> void:
amount = value
func set_amount_text(value: String) -> void:
_display_text = value
_refresh_amount()
func get_amount_label() -> Label:
if _amount_label != null:
return _amount_label
return get_node_or_null("Amount") as Label
func _refresh_amount() -> void:
if _amount_label != null:
_amount_label.text = _display_text
func _refresh_icon_size() -> void:
if _icon != null:
_icon.custom_minimum_size = Vector2.ONE * icon_size

View file

@ -0,0 +1 @@
uid://b7jx040ujcol

View file

@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/components/currency_amount.gd" id="1_script"]
[ext_resource type="Texture2D" path="res://items/icons/shop/32_currency.png" id="2_coin"]
[node name="CurrencyAmount" type="HBoxContainer"]
mouse_filter = 2
theme_override_constants/separation = 3
alignment = 1
script = ExtResource("1_script")
[node name="Icon" type="TextureRect" parent="."]
unique_name_in_owner = true
custom_minimum_size = Vector2(18, 18)
layout_mode = 2
size_flags_vertical = 4
mouse_filter = 2
texture = ExtResource("2_coin")
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="Amount" type="Label" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 4
mouse_filter = 2
text = "0"
vertical_alignment = 1

View file

@ -0,0 +1,28 @@
class_name CurrencyPresentation
extends RefCounted
const ICON_PATH: String = (
"res://items/icons/shop/32_currency.png"
)
const AMOUNT_SCENE: PackedScene = preload(
"res://ui/components/currency_amount.tscn"
)
static func instantiate_amount(
value: int,
icon_size: float = 18.0,
) -> CurrencyAmount:
var display := AMOUNT_SCENE.instantiate() as CurrencyAmount
display.icon_size = icon_size
display.amount = value
return display
static func bbcode_amount(value: int, icon_size: int = 18) -> String:
return "[img=%dx%d]%s[/img] %d" % [
icon_size,
icon_size,
ICON_PATH,
value,
]

View file

@ -0,0 +1 @@
uid://bcvy1l2jqqbmt

View file

@ -30,6 +30,15 @@ const UIMotionType = preload("res://ui/ui_motion.gd")
const FALLBACK_SUPPLY_ICON: Texture2D = preload(
"res://ui/icons/pictograms/x_light.png"
)
const ART_KIT_MARKER_ICON: Texture2D = preload(
"res://items/icons/art/art_kit_marker.png"
)
const ART_KIT_MARKER_SHADER: Shader = preload(
"res://ui/art_kit_marker_icon.gdshader"
)
const CURRENCY_ICON: Texture2D = preload(
"res://items/icons/shop/32_currency.png"
)
signal menu_visibility_changed(is_open: bool)
signal menu_exit_started
@ -71,6 +80,8 @@ 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
const SUPPLY_PRICE_ICON_SIZE: float = 18.0
const SUPPLY_PRICE_ICON_GAP: float = 3.0
@onready var _wallet_label: Label = %WalletLabel
@onready var _shop_panel: PanelContainer = %ShopPanel
@ -79,23 +90,20 @@ const SUPPLY_PRICE_HORIZONTAL_PADDING: float = 12.0
@onready var _shop_cooler_page: Control = %ShopCoolerPage
@onready var _shop_cooler_mount: Control = %ShopCoolerMount
@onready var _shop_body: HBoxContainer = $ShopPanel/Margin/Layout/Body
@onready var _feedback: Label = %Feedback
@onready var _feedback: RichTextLabel = %Feedback
@onready var _shop_tab_bar: HBoxContainer = %ShopTabBar
@onready var _upgrades_content: VBoxContainer = %Upgrades
@onready var _supplies_content: VBoxContainer = %Supplies
@onready var _stock_title: Label = %StockTitle
@onready var _reel_level: Label = %ReelLevel
@onready var _reel_effect: Label = %ReelEffect
@onready var _reel_cost: Label = %ReelCost
@onready var _reel_cost: CurrencyAmount = %ReelCost
@onready var _reel_price_bubble: PanelContainer = %ReelPriceBubble
@onready var _reel_purchase: Button = %ReelPurchase
@onready var _barrier_level: Label = %BarrierLevel
@onready var _barrier_effect: Label = %BarrierEffect
@onready var _barrier_cost: Label = %BarrierCost
@onready var _barrier_cost: CurrencyAmount = %BarrierCost
@onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble
@onready var _barrier_purchase: Button = %BarrierPurchase
@onready var _supplies_list: VBoxContainer = %SuppliesList
@onready var _cooler_level: Label = %CoolerLevel
@onready var _cooler_effect: Label = %CoolerEffect
@onready var _cooler_cost: Label = %CoolerCost
@onready var _cooler_cost: CurrencyAmount = %CoolerCost
@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
@onready var _cooler_purchase: Button = %CoolerPurchase
var _player: PlayerType
@ -136,17 +144,17 @@ func _ready() -> void:
func _request_shop_cooler() -> bool:
if _transaction_in_progress:
_feedback.text = "Finish the current transaction first."
_set_feedback("Finish the current transaction first.")
return false
if not _is_transaction_context_valid():
_feedback.text = "The fishing shop is no longer available."
_set_feedback("The fishing shop is no longer available.")
return false
sell_fish_requested.emit()
return _cooler_page_active
func _focus_shop_section() -> void:
_feedback.text = ""
_set_feedback("")
if _shop_section == ShopSection.UPGRADES:
_reel_purchase.grab_focus()
return
@ -193,6 +201,10 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
if _closing:
return
if section_index == int(_shop_section):
if section_index != ShopSection.SELL_FISH:
var showing_upgrades := section_index == ShopSection.UPGRADES
_upgrades_content.visible = showing_upgrades
_supplies_content.visible = not showing_upgrades
_update_shop_tab_selection()
if focus_content:
_focus_shop_section()
@ -236,13 +248,31 @@ func _apply_shop_styles() -> void:
"panel", shop_panel_style
)
for panel: PanelContainer in [
$ShopPanel/Margin/Layout/Body/Upgrades/ReelCard,
$ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard,
$ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard,
_reel_price_bubble,
_barrier_price_bubble,
_cooler_price_bubble,
]:
panel.add_theme_stylebox_override(
"panel", UtilityPageStyleType.row_style()
var price_style := UtilityPageStyleType.rounded_style(
UtilityPageStyleType.OCEAN_FIELD,
18,
)
price_style.content_margin_left = 8.0
price_style.content_margin_right = 8.0
price_style.content_margin_top = 2.0
price_style.content_margin_bottom = 2.0
panel.add_theme_stylebox_override(
"panel", price_style
)
for price: CurrencyAmount in [
_reel_cost,
_barrier_cost,
_cooler_cost,
]:
var amount_label := price.get_amount_label()
amount_label.add_theme_color_override(
"font_color", SUPPLY_PRICE_COLOR
)
amount_label.add_theme_font_size_override("font_size", 18)
for button: BaseButton in [
%CloseButton,
_reel_purchase,
@ -336,7 +366,7 @@ func open_shop() -> bool:
_player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_feedback.text = ""
_set_feedback("")
deactivate_shop_cooler_page()
show()
_shop_tab_bar.show()
@ -520,7 +550,6 @@ 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_purchase.disabled = (
reel_cost < 0
or _transaction_in_progress
@ -529,27 +558,30 @@ func _refresh_upgrades() -> void:
or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_reel_speed(_wallet)
)
_reel_purchase.tooltip_text = (
"Purchases are unavailable in this session."
if _network_shop == null or not _network_shop.can_request_purchase()
else ""
)
var reel_effect: String
if reel_cost < 0:
_reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier()
_reel_cost.text = "max"
reel_effect = "%.2f× reel speed · maximum level" % (
_upgrades.get_reel_speed_multiplier()
)
_reel_price_bubble.hide()
else:
_reel_effect.text = (
_reel_price_bubble.show()
reel_effect = (
"%.2f×%.2f×"
% [
_upgrades.get_reel_speed_multiplier(),
1.0 + float(reel_level + 1) * 0.10,
]
)
_reel_cost.text = "$%d" % reel_cost
_reel_purchase.text = "max" if reel_cost < 0 else "purchase"
_reel_cost.set_amount(reel_cost)
_reel_purchase.tooltip_text = _upgrade_tooltip(
"reel speed",
reel_level,
reel_effect,
)
_reel_purchase.accessibility_name = _reel_purchase.tooltip_text
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_purchase.disabled = (
barrier_cost < 0
or _transaction_in_progress
@ -558,20 +590,39 @@ func _refresh_upgrades() -> void:
or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_barrier_power(_wallet)
)
_barrier_purchase.tooltip_text = _reel_purchase.tooltip_text
var barrier_effect: String
if barrier_cost < 0:
_barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage()
_barrier_cost.text = "max"
barrier_effect = "%d damage · maximum level" % (
_upgrades.get_barrier_damage()
)
_barrier_price_bubble.hide()
else:
_barrier_effect.text = (
_barrier_price_bubble.show()
barrier_effect = (
"%d damage → %d damage"
% [
_upgrades.get_barrier_damage(),
barrier_level + 2,
]
)
_barrier_cost.text = "$%d" % barrier_cost
_barrier_purchase.text = "max" if barrier_cost < 0 else "purchase"
_barrier_cost.set_amount(barrier_cost)
_barrier_purchase.tooltip_text = _upgrade_tooltip(
"rod power",
barrier_level,
barrier_effect,
)
_barrier_purchase.accessibility_name = _barrier_purchase.tooltip_text
func _upgrade_tooltip(
upgrade_name: String,
level: int,
effect: String,
) -> String:
var tooltip := "%s\nlevel %d\n%s" % [upgrade_name, level, effect]
if _network_shop == null or not _network_shop.can_request_purchase():
tooltip += "\nPurchases are unavailable in this session."
return tooltip
func _refresh_supplies() -> void:
@ -632,9 +683,8 @@ func _refresh_supplies() -> void:
var use_icon_tile: bool = (
current_stock_grid != null
)
button.text = "%s\n$%downed %d" % [
button.text = "%s\nowned %d" % [
item.display_name,
FishingShopStockType.get_price(item_id),
owned,
]
var item_tooltip_text: String = item.description
@ -646,27 +696,24 @@ func _refresh_supplies() -> void:
button.text = ""
else:
button.text = (
"%s\nunlock $%d" % [item.display_name, total_cost]
"%s\nunlock and fill" % item.display_name
if not bait_unlocked
else "%s\n$%d each • %d/%d" % [
else "%s\n%d/%d owned" % [
item.display_name,
FishingShopStockType.get_price(item_id),
owned,
item.max_stack,
]
)
item_tooltip_text = (
"%s\nunlock $%d • fills to %d/%d\n%s" % [
"%s\nunlock and fill to %d/%d\n%s" % [
item.display_name,
total_cost,
item.max_stack,
item.max_stack,
item.description,
]
if not bait_unlocked
else "%s\n$%d each • %d/%d\n%s" % [
else "%s\n%d/%d owned\n%s" % [
item.display_name,
FishingShopStockType.get_price(item_id),
owned,
item.max_stack,
item.description,
@ -677,9 +724,8 @@ func _refresh_supplies() -> void:
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.text = ""
item_tooltip_text = "%s\n$%downed %d\n%s" % [
item_tooltip_text = "%s\nowned %d\n%s" % [
item.display_name,
FishingShopStockType.get_price(item_id),
owned,
item.description,
]
@ -704,7 +750,11 @@ func _refresh_supplies() -> void:
_add_supply_icon_tile(
current_stock_grid,
button,
FishingShopStockType.get_price(item_id),
(
total_cost
if bait_topoff and not bait_unlocked
else FishingShopStockType.get_price(item_id)
),
)
else:
_supplies_list.add_child(button)
@ -769,14 +819,12 @@ func _add_art_kit_button(parent: GridContainer) -> void:
return
var owned: bool = _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0
var button: Button = _make_stock_button(
"%s\n$%d%s" % [
"%s\n%s" % [
item.display_name,
ArtShopStockType.ART_KIT_PRICE,
"owned" if owned else "not owned",
],
"%s\n$%d%s\n%s" % [
"%s\n%s\n%s" % [
item.display_name,
ArtShopStockType.ART_KIT_PRICE,
"owned" if owned else "not owned",
item.description,
],
@ -808,19 +856,26 @@ func _add_art_upgrade_button(
)
var unlocked: bool = _art_unlocks.owns_product(product_id)
var button: Button = _make_stock_button(
"%s\n$%d%s" % [
"%s\n%s" % [
ArtShopStockType.get_display_name(product_id),
ArtShopStockType.UPGRADE_PRICE,
"unlocked" if unlocked else "locked",
],
"%s\n$%d%s\n%s" % [
"%s\n%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)
var marker_color_id: StringName = (
PlayerArtUnlocksType.color_id_for_product(product_id)
)
if marker_color_id.is_empty():
_configure_supply_icon_tile(button, FALLBACK_SUPPLY_ICON)
else:
_configure_marker_icon_tile(
button,
SurfaceDrawingPalette.get_color(marker_color_id),
)
_add_supply_icon_tile(
parent,
button,
@ -863,6 +918,29 @@ func _configure_supply_icon_tile(
button.text = ""
func _configure_marker_icon_tile(
button: Button,
marker_color: Color,
) -> void:
button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.icon = null
button.text = ""
var icon := TextureRect.new()
icon.name = "MarkerIcon"
icon.position = Vector2(4.0, 4.0)
icon.size = Vector2(64.0, 64.0)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
var marker_material := ShaderMaterial.new()
marker_material.shader = ART_KIT_MARKER_SHADER
marker_material.set_shader_parameter("marker_color", marker_color)
icon.material = marker_material
icon.texture = ART_KIT_MARKER_ICON
button.add_child(icon)
func _add_supply_icon_tile(
parent: GridContainer,
button: Button,
@ -875,7 +953,7 @@ func _add_supply_icon_tile(
button.position = Vector2.ZERO
button.size = SUPPLY_ICON_TILE_SIZE
tile_host.add_child(button)
var price_text := "$%d" % price
var price_text := str(price)
var price_text_width: float = UtilityPageStyleType.TuffyFont.get_string_size(
price_text,
HORIZONTAL_ALIGNMENT_LEFT,
@ -884,7 +962,12 @@ func _add_supply_icon_tile(
).x
var price_width: float = minf(
SUPPLY_ICON_TILE_SIZE.x,
ceilf(price_text_width + SUPPLY_PRICE_HORIZONTAL_PADDING),
ceilf(
price_text_width
+ SUPPLY_PRICE_ICON_SIZE
+ SUPPLY_PRICE_ICON_GAP
+ SUPPLY_PRICE_HORIZONTAL_PADDING
),
)
var price_bubble := PanelContainer.new()
price_bubble.name = "PriceBubble"
@ -905,6 +988,23 @@ func _add_supply_icon_tile(
price_style.content_margin_bottom = 0.0
price_bubble.add_theme_stylebox_override("panel", price_style)
tile_host.add_child(price_bubble)
var price_row := HBoxContainer.new()
price_row.name = "CurrencyAmount"
price_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
price_row.alignment = BoxContainer.ALIGNMENT_CENTER
price_row.add_theme_constant_override(
"separation", int(SUPPLY_PRICE_ICON_GAP)
)
price_bubble.add_child(price_row)
var price_icon := TextureRect.new()
price_icon.name = "CurrencyIcon"
price_icon.custom_minimum_size = Vector2.ONE * SUPPLY_PRICE_ICON_SIZE
price_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
price_icon.texture = CURRENCY_ICON
price_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
price_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
price_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
price_row.add_child(price_icon)
var price_label := Label.new()
price_label.name = "Price"
price_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
@ -921,7 +1021,7 @@ func _add_supply_icon_tile(
"font_color", SUPPLY_PRICE_COLOR
)
price_label.add_theme_constant_override("outline_size", 0)
price_bubble.add_child(price_label)
price_row.add_child(price_label)
func _refresh_cooler_capacity() -> void:
@ -930,7 +1030,6 @@ 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_purchase.disabled = (
cost < 0
or _transaction_in_progress
@ -939,18 +1038,25 @@ func _refresh_cooler_capacity() -> void:
or not _network_shop.can_request_purchase()
or not _cooler_capacity.can_purchase(_wallet)
)
_cooler_purchase.tooltip_text = _reel_purchase.tooltip_text
var cooler_effect: String
if cost < 0:
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity()
_cooler_cost.text = "max"
_cooler_purchase.text = "max"
cooler_effect = "%d fish · maximum level" % (
_cooler_capacity.get_capacity()
)
_cooler_price_bubble.hide()
else:
_cooler_effect.text = "%d%d fish" % [
_cooler_price_bubble.show()
cooler_effect = "%d%d fish" % [
_cooler_capacity.get_capacity(),
next_capacity,
]
_cooler_cost.text = "$%d" % cost
_cooler_purchase.text = "purchase"
_cooler_cost.set_amount(cost)
_cooler_purchase.tooltip_text = _upgrade_tooltip(
"cooler capacity",
level,
cooler_effect,
)
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
func _purchase_reel_speed() -> void:
@ -963,65 +1069,65 @@ 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."
_set_feedback("unable to complete purchase.")
return
var owned: int = _bag.get_quantity(item_id)
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
if item == null:
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id, owned, item.max_stack
)
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
_feedback.text = "Your Bag is full."
_set_feedback("Your Bag is full.")
return
var total_cost: int = FishingShopStockType.get_purchase_cost(
item_id, quantity, _bag.is_bait_unlocked(item_id)
)
if total_cost < 0 or not _wallet.can_afford(total_cost):
_feedback.text = "Not enough fish coin."
_set_feedback("Insufficient funds.")
return
if _network_shop == null:
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
_network_shop.request_supply(item_id)
func _purchase_art_kit() -> void:
if _network_shop == null or not _is_transaction_context_valid():
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
_network_shop.request_art_kit()
func _purchase_art_upgrade(product_id: StringName) -> void:
if _network_shop == null or not _is_transaction_context_valid():
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
_network_shop.request_art_upgrade(product_id)
func _purchase_cooler_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase."
_set_feedback("unable to complete purchase.")
return
var cost: int = _cooler_capacity.get_next_cost()
if cost < 0:
_feedback.text = "Upgrade is already at maximum."
_set_feedback("Upgrade is already at maximum.")
return
if not _wallet.can_afford(cost):
_feedback.text = "Not enough fish coin."
_set_feedback("Insufficient funds.")
return
if _network_shop == null:
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
_network_shop.request_cooler_capacity_upgrade()
func _purchase_upgrade(is_reel_speed: bool) -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase."
_set_feedback("unable to complete purchase.")
return
var cost: int = (
_upgrades.get_next_reel_speed_cost()
@ -1029,13 +1135,13 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
else _upgrades.get_next_barrier_power_cost()
)
if cost < 0:
_feedback.text = "Upgrade is already at maximum."
_set_feedback("Upgrade is already at maximum.")
return
if not _wallet.can_afford(cost):
_feedback.text = "Not enough fish coin."
_set_feedback("Insufficient funds.")
return
if _network_shop == null:
_feedback.text = "Purchase could not be completed."
_set_feedback("Purchase could not be completed.")
return
if is_reel_speed:
_network_shop.request_reel_speed_upgrade()
@ -1046,7 +1152,7 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
func _on_network_purchase_pending(_request_id: String) -> void:
_transaction_in_progress = true
if visible:
_feedback.text = "Purchasing…"
_set_feedback("Purchasing…")
_refresh_all()
@ -1062,14 +1168,21 @@ func _on_network_purchase_finished(
_transaction_in_progress = false
if not visible:
return
_feedback.text = (
"Purchase complete. $%d spent." % total_cost
_set_feedback(
(
"Purchase complete • %s spent."
% CurrencyPresentation.bbcode_amount(total_cost, 18)
)
if accepted
else message
)
_refresh_all()
func _set_feedback(message: String) -> void:
_feedback.text = "[center]%s[/center]" % message
func _is_transaction_context_valid() -> bool:
return (
visible

View file

@ -1,12 +1,13 @@
[gd_scene load_steps=8 format=3]
[gd_scene load_steps=9 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"]
[ext_resource type="Texture2D" path="res://items/icons/shop/64_speed_plus.png" id="3_reel"]
[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/shop/32_currency.png" 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"]
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="8_currency"]
[node name="FishingShop" type="Control"]
unique_name_in_owner = true
@ -126,12 +127,16 @@ icon = ExtResource("7_close")
expand_icon = true
icon_max_width = 40
[node name="Feedback" type="Label" parent="ShopPanel/Margin/Layout"]
[node name="Feedback" type="RichTextLabel" parent="ShopPanel/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 28)
layout_mode = 2
text = ""
horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
bbcode_enabled = true
fit_content = true
scroll_active = false
theme_override_colors/default_color = Color(1, 0.82, 0.4, 1)
theme_override_font_sizes/normal_font_size = 20
[node name="ShopTabBar" type="HBoxContainer" parent="."]
unique_name_in_owner = true
@ -184,164 +189,102 @@ layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 5
[node name="ReelCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
[node name="UpgradeScroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard"]
layout_mode = 2
theme_override_constants/margin_left = 9
theme_override_constants/margin_top = 7
theme_override_constants/margin_right = 9
theme_override_constants/margin_bottom = 7
[node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin"]
layout_mode = 2
theme_override_constants/separation = 9
[node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
texture = ExtResource("3_reel")
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="Data" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"]
[node name="UpgradeGrid" type="GridContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 20
theme_override_constants/v_separation = 20
columns = 3
[node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
[node name="ReelTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
custom_minimum_size = Vector2(144, 168)
layout_mode = 2
text = "reel speed"
theme_override_font_sizes/font_size = 22
mouse_filter = 2
[node name="ReelLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
[node name="ReelPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/ReelTile"]
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "reel speed"
icon = ExtResource("3_reel")
expand_icon = true
icon_max_width = 128
[node name="ReelPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/ReelTile"]
unique_name_in_owner = true
z_index = 2
offset_left = 22.0
offset_top = 128.0
offset_right = 122.0
offset_bottom = 160.0
mouse_filter = 2
[node name="ReelCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/ReelTile/ReelPriceBubble" instance=ExtResource("8_currency")]
unique_name_in_owner = true
layout_mode = 2
text = "level 0"
theme_override_font_sizes/font_size = 18
amount = 50
icon_size = 24.0
[node name="ReelEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
[node name="BarrierTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
custom_minimum_size = Vector2(144, 168)
layout_mode = 2
mouse_filter = 2
[node name="BarrierPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BarrierTile"]
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "rod power"
icon = ExtResource("4_barrier")
expand_icon = true
icon_max_width = 128
[node name="BarrierPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BarrierTile"]
unique_name_in_owner = true
z_index = 2
offset_left = 22.0
offset_top = 128.0
offset_right = 122.0
offset_bottom = 160.0
mouse_filter = 2
[node name="BarrierCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BarrierTile/BarrierPriceBubble" instance=ExtResource("8_currency")]
unique_name_in_owner = true
layout_mode = 2
text = "1.00× → 1.10×"
theme_override_font_sizes/font_size = 18
amount = 100
icon_size = 24.0
[node name="ReelCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"]
[node name="CoolerTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
custom_minimum_size = Vector2(144, 168)
layout_mode = 2
mouse_filter = 2
[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "cooler capacity"
icon = ExtResource("6_cooler")
expand_icon = true
icon_max_width = 128
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
unique_name_in_owner = true
z_index = 2
offset_left = 22.0
offset_top = 128.0
offset_right = 122.0
offset_bottom = 160.0
mouse_filter = 2
[node name="CoolerCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile/CoolerPriceBubble" instance=ExtResource("8_currency")]
unique_name_in_owner = true
layout_mode = 2
text = "$50"
theme_override_font_sizes/font_size = 18
[node name="ReelPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"]
unique_name_in_owner = true
layout_mode = 2
text = "purchase"
[node name="BarrierCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard"]
layout_mode = 2
theme_override_constants/margin_left = 9
theme_override_constants/margin_top = 7
theme_override_constants/margin_right = 9
theme_override_constants/margin_bottom = 7
[node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin"]
layout_mode = 2
theme_override_constants/separation = 9
[node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
texture = ExtResource("4_barrier")
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="Data" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
layout_mode = 2
text = "rod power"
theme_override_font_sizes/font_size = 22
[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"
theme_override_font_sizes/font_size = 18
[node name="BarrierEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "1 damage → 2 damage"
theme_override_font_sizes/font_size = 18
[node name="BarrierCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "$100"
theme_override_font_sizes/font_size = 18
[node name="BarrierPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"]
unique_name_in_owner = true
layout_mode = 2
text = "purchase"
[node name="CoolerCard" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades"]
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard"]
layout_mode = 2
theme_override_constants/margin_left = 9
theme_override_constants/margin_top = 7
theme_override_constants/margin_right = 9
theme_override_constants/margin_bottom = 7
[node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin"]
layout_mode = 2
theme_override_constants/separation = 9
[node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"]
custom_minimum_size = Vector2(88, 88)
layout_mode = 2
texture = ExtResource("6_cooler")
expand_mode = 1
stretch_mode = 5
texture_filter = 1
[node name="Data" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"]
layout_mode = 2
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"
theme_override_font_sizes/font_size = 22
[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"
theme_override_font_sizes/font_size = 18
[node name="CoolerEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "12 → 18 fish"
theme_override_font_sizes/font_size = 18
[node name="CoolerCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"]
unique_name_in_owner = true
layout_mode = 2
text = "$75"
theme_override_font_sizes/font_size = 18
[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"]
unique_name_in_owner = true
layout_mode = 2
text = "purchase"
amount = 75
icon_size = 24.0

View file

@ -122,6 +122,7 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural"
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
const ART_KIT_ITEM_ID: StringName = &"art_kit"
@onready var _canonical_stage: Control = %CanonicalStage
@ -164,6 +165,7 @@ var _network_chat_service: NetworkChatService
var _network_profile: NetworkProfilePreferences
var _spawn_service: PlayerSpawnService
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _player_menu_open: bool = false
var _gameplay_ui_enabled: bool = false
@ -177,6 +179,7 @@ var _item_effects: PlayerItemEffectsType
var _main_shop_buyer: FishBuyerProfileType
var _shop_interaction: ShopInteractionType
var _surface_drawing: NetworkSurfaceDrawingService
var _surface_drawing_hotbar_selected: bool = false
var _experience: PlayerExperienceType
var _experience_award_queue: Array[Dictionary] = []
var _experience_animation_active: bool = false
@ -293,6 +296,7 @@ func setup(
_network_profile = network_profile
_spawn_service = spawn_service
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
_settings_manager = settings_manager
_fishing_spot = fishing_spot
@ -448,16 +452,7 @@ func _input(event: InputEvent) -> void:
)
get_viewport().set_input_as_handled()
return
var drawing_can_open: bool = (
_gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and not _showcase_active
and _fishing_spot != null
and _fishing_spot.can_use_surface_drawing()
)
var drawing_can_open: bool = _can_surface_drawing_be_active()
if (
_surface_drawing != null
and _surface_drawing.handle_input(
@ -630,7 +625,7 @@ func _on_quick_action_selected(action_id: StringName) -> void:
&"online":
_player_menu.open_section(PlayerMenuType.Section.PLAYERS)
&"paint":
_toggle_surface_drawing()
_select_art_kit_hotbar_slot()
&"chat":
_chat_ui.open_chat()
&"freecam":
@ -1121,25 +1116,46 @@ static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
return Rect2(cursor_margin, bounds_size)
func _toggle_surface_drawing() -> void:
if _surface_drawing == null:
func _select_art_kit_hotbar_slot() -> void:
if _hotbar == null:
return
if _surface_drawing.is_active():
_surface_drawing.deactivate()
elif _surface_drawing.can_activate():
_surface_drawing.activate(_drawing_pointer_window_position())
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
if _hotbar.get_item_id(slot_index) == ART_KIT_ITEM_ID:
_hotbar.select_slot(slot_index)
return
func set_surface_drawing_hotbar_selected(is_selected: bool) -> void:
_surface_drawing_hotbar_selected = is_selected
_refresh_surface_drawing_activation()
func _refresh_surface_drawing_activation() -> void:
if _surface_drawing == null:
return
if is_selected:
if not _surface_drawing.is_active() and _surface_drawing.can_activate():
_surface_drawing.activate(_drawing_pointer_window_position())
elif _surface_drawing.is_active():
var should_be_active: bool = _can_surface_drawing_be_active()
if should_be_active and not _surface_drawing.is_active():
_surface_drawing.activate(_drawing_pointer_window_position())
elif not should_be_active and _surface_drawing.is_active():
_surface_drawing.deactivate()
func _can_surface_drawing_be_active() -> bool:
return (
_surface_drawing_hotbar_selected
and _gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and not _showcase_active
and _fishing_spot != null
and _fishing_spot.can_use_surface_drawing()
and _surface_drawing != null
and _surface_drawing.can_activate()
)
func _drawing_pointer_window_position() -> Vector2:
if _virtual_mouse_active:
return _virtual_mouse_window_position
@ -1462,6 +1478,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
_hotbar_ui.set_gameplay_input_enabled(true)
_refresh_fishing_panel_visibility()
call_deferred("_start_next_experience_animation")
_refresh_surface_drawing_activation()
func set_gameplay_hud_hidden(hidden: bool) -> void:
@ -1505,8 +1522,7 @@ func _refresh_gameplay_hud_visibility() -> void:
func set_system_menu_open(is_open: bool) -> void:
_system_menu_open = is_open
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_surface_drawing_activation()
_refresh_gameplay_hud_visibility()
_refresh_chat_availability()
_refresh_hotbar_visibility()
@ -1862,6 +1878,7 @@ func _on_showcase_changed(
showcase_visible: bool,
) -> void:
_showcase_active = showcase_visible
_refresh_surface_drawing_activation()
if not showcase_visible:
_set_fishing_panel_showcase_position(false)
_showcase_details.text = ""
@ -2103,8 +2120,7 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open
if is_open:
_end_virtual_mouse()
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_surface_drawing_activation()
_refresh_gameplay_hud_visibility()
if is_open:
_hotbar_ui.set_drag_enabled(false)
@ -2156,8 +2172,7 @@ func _on_shop_exit_started() -> void:
func _on_shop_visibility_changed(is_open: bool) -> void:
_shop_open = is_open
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_surface_drawing_activation()
if is_open:
shop_backdrop_visibility_changed.emit(true)
if not is_open and _player_menu.is_shop_cooler_mounted():
@ -2309,8 +2324,7 @@ func _emit_interactive_pointer_ui_changed() -> void:
func _on_chat_text_entry_ownership_changed(active: bool) -> void:
_chat_input_open = active
if active and _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_surface_drawing_activation()
_refresh_chat_availability()
_emit_interactive_pointer_ui_changed()

View file

@ -16,6 +16,9 @@ const InventoryNotepadType = preload(
const LogbookPortraitType = preload(
"res://ui/components/logbook_portrait.gd"
)
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const DETAIL_FADE_DURATION: float = 0.12
const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION
@ -756,19 +759,17 @@ func _build_known_details(fish: FishDataType) -> void:
_add_detail_row(left_stats, "number caught", "unknown")
_add_detail_row(right_stats, "rarity", fish.get_rarity_name().to_lower())
_add_detail_row(right_stats, "time of day", _availability_text(fish))
_add_detail_row(
_add_currency_detail_row(
right_stats,
"value range",
"%d%d fish coin" % [
FishQualityType.apply_sale_value(
fish.sell_value_min,
FishQualityType.Tier.BORING,
),
FishQualityType.apply_sale_value(
fish.sell_value_max,
FishQualityType.Tier.SHINY,
),
],
FishQualityType.apply_sale_value(
fish.sell_value_min,
FishQualityType.Tier.BORING,
),
FishQualityType.apply_sale_value(
fish.sell_value_max,
FishQualityType.Tier.SHINY,
),
)
_add_detail_row(
right_stats,
@ -878,6 +879,31 @@ func _add_detail_row(
parent.add_child(row)
func _add_currency_detail_row(
parent: VBoxContainer,
row_name: String,
minimum_value: int,
maximum_value: int,
) -> void:
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 2)
var heading := _field_label(row_name, 14)
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
heading.add_theme_color_override("font_color", MUTED_INK)
row.add_child(heading)
var value: CurrencyAmount = (
CurrencyPresentationType.instantiate_amount(minimum_value, 16.0)
)
value.set_amount_text("%d%d" % [minimum_value, maximum_value])
value.get_amount_label().add_theme_font_override(
"font", InventoryNotepadType.HANDWRITTEN_FONT
)
value.get_amount_label().add_theme_font_size_override("font_size", 16)
value.get_amount_label().add_theme_color_override("font_color", INK)
row.add_child(value)
parent.add_child(row)
func _make_stats_column() -> VBoxContainer:
var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL

View file

@ -15,6 +15,9 @@ const SALUTATION_LABELS := {
"good_luck_have_fun": "Good Luck Have Fun",
}
const FishQualityType = preload("res://fish/fish_quality.gd")
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
var _service: NetworkMailService
var _reservations: PlayerAssetReservationService
@ -36,14 +39,16 @@ var _signature: Label
var _attachment_kind: OptionButton
var _attachment_choice: OptionButton
var _attachment_amount: LineEdit
var _coin_available: Label
var _coin_available_heading: Label
var _coin_available: CurrencyAmount
var _attachment_amount_currency_icon: TextureRect
var _amount_minus: Button
var _amount_plus: Button
var _attachment_summary: Label
var _attachment_summary: RichTextLabel
var _send_button: Button
var _status: Label
var _letter_text: Label
var _letter_gift: Label
var _letter_gift: RichTextLabel
var _accept: Button
var _decline: Button
var _archive: Button
@ -212,7 +217,7 @@ func _build_compose() -> Control:
_attachment_kind = OptionButton.new()
_attachment_kind.position = Vector2(688, 48)
_attachment_kind.size = Vector2(280, 46)
for label: String in ["No gift", "Fish coin", "Fish", "Consumable"]:
for label: String in ["No gift", "Currency", "Fish", "Consumable"]:
_attachment_kind.add_item(label)
_attachment_kind.item_selected.connect(_refresh_attachment_choices)
page.add_child(_attachment_kind)
@ -225,13 +230,34 @@ func _build_compose() -> Control:
_update_attachment_summary()
)
page.add_child(_attachment_choice)
_coin_available = Label.new()
_coin_available.position = Vector2(688, 158)
_coin_available.size = Vector2(280, 28)
_coin_available_heading = Label.new()
_coin_available_heading.text = "Available"
_coin_available_heading.position = Vector2(688, 158)
_coin_available_heading.size = Vector2(80, 28)
page.add_child(_coin_available_heading)
_coin_available = CurrencyPresentationType.instantiate_amount(0, 18.0)
_coin_available.position = Vector2(770, 158)
_coin_available.size = Vector2(198, 28)
_coin_available.alignment = BoxContainer.ALIGNMENT_BEGIN
page.add_child(_coin_available)
_attachment_amount_currency_icon = TextureRect.new()
_attachment_amount_currency_icon.position = Vector2(746, 204)
_attachment_amount_currency_icon.size = Vector2(18, 18)
_attachment_amount_currency_icon.texture = preload(
"res://items/icons/shop/32_currency.png"
)
_attachment_amount_currency_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_attachment_amount_currency_icon.stretch_mode = (
TextureRect.STRETCH_KEEP_ASPECT_CENTERED
)
_attachment_amount_currency_icon.texture_filter = (
CanvasItem.TEXTURE_FILTER_NEAREST
)
_attachment_amount_currency_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
page.add_child(_attachment_amount_currency_icon)
_attachment_amount = LineEdit.new()
_attachment_amount.position = Vector2(746, 190)
_attachment_amount.size = Vector2(164, 46)
_attachment_amount.position = Vector2(770, 190)
_attachment_amount.size = Vector2(140, 46)
_attachment_amount.placeholder_text = "0"
_attachment_amount.text = "0"
_attachment_amount.text_changed.connect(_on_attachment_amount_changed)
@ -248,9 +274,12 @@ func _build_compose() -> Control:
_amount_plus.size = Vector2(48, 46)
_amount_plus.pressed.connect(_step_attachment_amount.bind(1))
page.add_child(_amount_plus)
_attachment_summary = Label.new()
_attachment_summary = RichTextLabel.new()
_attachment_summary.position = Vector2(688, 246)
_attachment_summary.size = Vector2(280, 78)
_attachment_summary.bbcode_enabled = true
_attachment_summary.fit_content = true
_attachment_summary.scroll_active = false
_attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_attachment_summary)
var cancel := Button.new()
@ -281,9 +310,12 @@ func _build_letter() -> Control:
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
page.add_child(_letter_text)
_letter_gift = Label.new()
_letter_gift = RichTextLabel.new()
_letter_gift.position = Vector2(730, 46)
_letter_gift.size = Vector2(230, 180)
_letter_gift.bbcode_enabled = true
_letter_gift.fit_content = true
_letter_gift.scroll_active = false
_letter_gift.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_letter_gift)
_accept = Button.new()
@ -479,13 +511,18 @@ func _refresh_attachment_choices(_index: int) -> void:
_attachment_amount.visible = amount_visible
_amount_minus.visible = amount_visible
_amount_plus.visible = amount_visible
_coin_available.visible = _attachment_kind.selected == 1
var currency_selected: bool = _attachment_kind.selected == 1
_coin_available_heading.visible = currency_selected
_coin_available.visible = currency_selected
_attachment_amount_currency_icon.visible = currency_selected
_attachment_amount.position.x = 770.0 if currency_selected else 746.0
_attachment_amount.size.x = 140.0 if currency_selected else 164.0
match _attachment_kind.selected:
0:
_attachment_choice.add_item("No attachment")
1:
_attachment_choice.add_item("Fish coin")
_coin_available.text = "Available: %d fish coin" % (
_attachment_choice.add_item("Currency")
_coin_available.set_amount(
_reservations.get_available_fish_coin()
)
2:
@ -705,7 +742,9 @@ func _attachment_text(attachment: Dictionary) -> String:
PlayerAssetReservationService.AttachmentType.NONE:
return "No gift attached."
PlayerAssetReservationService.AttachmentType.FISH_COIN:
return "%d fish coin" % int(attachment.get("amount", 0))
return CurrencyPresentationType.bbcode_amount(
int(attachment.get("amount", 0)), 18
)
PlayerAssetReservationService.AttachmentType.FISH:
var fish_catch := _inventory.get_catch_by_id(
StringName(str(attachment.get("catch_id", "")))

View file

@ -14,6 +14,9 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
@ -168,7 +171,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _notepad_binding: Label = %BindingDecoration
@onready var _notepad_title: Label = %NotepadTitle
@onready var _notepad_rule: ColorRect = %NotepadRule
@onready var _notepad_wallet_value: Label = %NotepadWalletValue
@onready var _notepad_wallet_value: CurrencyAmount = %NotepadWalletValue
@onready var _notepad_capacity_value: Label = %NotepadCapacityValue
@onready var _cooler_detail_texture: TextureRect = %CoolerDetailTexture
@onready var _cooler_detail_name: Label = %CoolerDetailName
@ -178,7 +181,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _cooler_weight_unit: Label = %CoolerWeightUnit
@onready var _cooler_offer_row: HBoxContainer = %CoolerOfferRow
@onready var _cooler_offer_label: Label = %CoolerOfferLabel
@onready var _cooler_offer_value: Label = %CoolerOfferValue
@onready var _cooler_offer_value: CurrencyAmount = %CoolerOfferValue
@onready var _cooler_selection_summary: Control = %CoolerSelectionSummary
@onready var _cooler_selection_empty: Label = %CoolerSelectionEmpty
@onready var _cooler_selected_count_row: HBoxContainer = %CoolerSelectedCountRow
@ -186,7 +189,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _cooler_selected_count_label: Label = %CoolerSelectedCountLabel
@onready var _cooler_combined_offer_row: HBoxContainer = %CoolerCombinedOfferRow
@onready var _cooler_combined_offer_label: Label = %CoolerCombinedOfferLabel
@onready var _cooler_combined_offer_value: Label = %CoolerCombinedOfferValue
@onready var _cooler_combined_offer_value: CurrencyAmount = %CoolerCombinedOfferValue
@onready var _favorite_bubble: NotepadInkActionType = %FavoriteBubble
@onready var _sell_bubble: NotepadInkActionType = %SellBubble
@onready var _sell_all_bubble: NotepadInkActionType = %SellAllBubble
@ -247,7 +250,7 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _content_stage: Control = %Content
@onready var _cooler_scroll: ScrollContainer = %CoolerScroll
@onready var _cooler_host: Control = %CoolerHost
@onready var _wallet_balance: Label = %WalletBalance
@onready var _wallet_balance: CurrencyAmount = %WalletBalance
@onready var _header: Control = %Header
@onready var _separator: Control = %Separator
@onready var _wallet_status: BubbleStatusBubbleType = %WalletStatus
@ -266,16 +269,16 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _bag_detail_data: Label = %BagDetailData
@onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection
@onready var _held_value: Label = %HeldValue
@onready var _held_value: CurrencyAmount = %HeldValue
@onready var _cooler_count: Label = %CoolerCount
@onready var _inventory_empty: Label = %InventoryEmpty
@onready var _selection_summary: Label = %SelectionSummary
@onready var _favorite_button: Button = %FavoriteButton
@onready var _sell_button: Button = %SellButton
@onready var _sale_unavailable: Label = %SaleUnavailable
@onready var _transaction_feedback: Label = %TransactionFeedback
@onready var _transaction_feedback: RichTextLabel = %TransactionFeedback
@onready var _sale_confirmation: PanelContainer = %SaleConfirmation
@onready var _confirmation_message: Label = %ConfirmationMessage
@onready var _confirmation_message: RichTextLabel = %ConfirmationMessage
@onready var _confirm_sale_button: Button = %ConfirmSaleButton
@onready var _cancel_sale_button: Button = %CancelSaleButton
@onready var _logbook_empty: Label = %LogbookEmpty
@ -1881,11 +1884,11 @@ func _apply_inventory_styles() -> void:
UtilityPageStyle.panel_style(),
)
_confirmation_message.add_theme_font_override(
"font",
"normal_font",
UtilityPageStyle.TuffyFont,
)
_confirmation_message.add_theme_color_override(
"font_color",
"default_color",
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
)
UtilityPageStyle.apply_ocean_button(_confirm_sale_button)
@ -2405,14 +2408,13 @@ func _update_shell_layout() -> void:
_notepad_capacity_value.size = (
Vector2(142.0, 17.0) if compact else Vector2(124.0, 23.0)
)
for status_value: Label in [
_notepad_wallet_value,
_notepad_capacity_value,
]:
status_value.add_theme_font_size_override(
"font_size",
10 if compact else 14,
)
_notepad_wallet_value.icon_size = 10.0 if compact else 14.0
_notepad_wallet_value.get_amount_label().add_theme_font_size_override(
"font_size", 10 if compact else 14
)
_notepad_capacity_value.add_theme_font_size_override(
"font_size", 10 if compact else 14
)
_cooler_detail_texture.position = (
Vector2(5.0, 84.0) if compact else Vector2(80.0, 137.0)
)
@ -2594,22 +2596,21 @@ func _layout_cooler_detail_text(compact: bool) -> void:
"font_size",
13 if compact else 18,
)
_cooler_offer_value.add_theme_font_size_override(
"font_size",
10 if compact else 15,
_cooler_offer_value.icon_size = 10.0 if compact else 15.0
_cooler_offer_value.get_amount_label().add_theme_font_size_override(
"font_size", 10 if compact else 15
)
func _layout_cooler_selection_text(compact: bool) -> void:
var row_height: float = 17.5 if compact else 20.0
for numeric_label: Label in [
_cooler_selected_count_value,
_cooler_combined_offer_value,
]:
numeric_label.add_theme_font_size_override(
"font_size",
10 if compact else 14,
)
_cooler_selected_count_value.add_theme_font_size_override(
"font_size", 10 if compact else 14
)
_cooler_combined_offer_value.icon_size = 10.0 if compact else 14.0
_cooler_combined_offer_value.get_amount_label().add_theme_font_size_override(
"font_size", 10 if compact else 14
)
for word_label: Label in [
_cooler_selection_empty,
_cooler_selected_count_label,
@ -3438,9 +3439,9 @@ func _refresh_economy_summary() -> void:
if _inventory != null
else 0
)
_wallet_balance.text = "wallet: $%d" % balance
_wallet_status.set_content("wallet", "$%d" % balance)
_notepad_wallet_value.text = "$%d" % balance
_wallet_balance.set_amount(balance)
_wallet_status.set_currency_amount("wallet", balance)
_notepad_wallet_value.set_amount(balance)
_capacity_status.set_content("cooler", "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
@ -3449,8 +3450,8 @@ func _refresh_economy_summary() -> void:
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
]
_held_value_status.set_content("held value", "$%d" % held_total)
_held_value.text = "held fish base value: $%d" % held_total
_held_value_status.set_currency_amount("held value", held_total)
_held_value.set_amount(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,
@ -3707,7 +3708,7 @@ func _on_catch_card_pressed(catch_id: StringName) -> void:
Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT)
)
_transaction_feedback.text = ""
_set_transaction_feedback("")
_refresh_inventory()
@ -3719,7 +3720,7 @@ func _on_fish_field_gui_input(event: InputEvent) -> void:
and not get_viewport().gui_is_dragging()
):
_fish_selection.clear()
_transaction_feedback.text = ""
_set_transaction_feedback("")
_refresh_inventory()
@ -3751,9 +3752,9 @@ func _clear_cooler_detail_stats() -> void:
_cooler_weight_value,
_cooler_weight_unit,
_cooler_offer_label,
_cooler_offer_value,
]:
label.text = ""
_cooler_offer_value.visible = false
func _set_cooler_selection_summary_empty() -> void:
@ -3764,10 +3765,10 @@ func _set_cooler_selection_summary_empty() -> void:
_cooler_selected_count_value,
_cooler_selected_count_label,
_cooler_combined_offer_label,
_cooler_combined_offer_value,
]:
label.visible = false
label.text = ""
_cooler_combined_offer_value.visible = false
func _set_cooler_selection_summary(
@ -3785,10 +3786,9 @@ func _set_cooler_selection_summary(
_cooler_selected_count_label.text = "fish selected"
if offer_value >= 0:
_cooler_combined_offer_label.text = "combined offer"
_cooler_combined_offer_value.text = "$%d" % offer_value
_cooler_combined_offer_value.set_amount(offer_value)
else:
_cooler_combined_offer_label.text = "offer unavailable"
_cooler_combined_offer_value.text = ""
func _update_inventory_detail(fish_catch: FishCatchType) -> void:
@ -3838,10 +3838,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
]
if buyer_offer >= 0 and active_buyer != null:
_cooler_offer_label.text = _get_offer_label(active_buyer)
_cooler_offer_value.text = "$%d" % buyer_offer
_cooler_offer_value.visible = true
_cooler_offer_value.set_amount(buyer_offer)
else:
_cooler_offer_label.text = "buyer unavailable"
_cooler_offer_value.text = ""
_cooler_offer_value.visible = false
_favorite_button.disabled = false
_favorite_button.text = (
"unfavorite" if fish_catch.is_favorited else "favorite"
@ -3970,12 +3971,8 @@ func _update_sale_summary() -> void:
or preview.status == FishSaleResultType.Status.FAVORITED
)
):
_selection_summary.text += "\n%s total offer: $%d" % [
active_buyer.display_name,
preview.payout,
]
_set_cooler_selection_summary(selected_count, preview.payout)
_offer_status.set_content(offer_label, "$%d" % preview.payout)
_offer_status.set_currency_amount(offer_label, preview.payout)
else:
_set_cooler_selection_summary(selected_count, -1)
_offer_status.set_content(offer_label, "unavailable")
@ -4058,7 +4055,7 @@ func _on_favorite_pressed() -> void:
fish_catch.catch_id,
not fish_catch.is_favorited
):
_transaction_feedback.text = (
_set_transaction_feedback(
"%s %s."
% [
fish_catch.fish.display_name,
@ -4074,7 +4071,7 @@ func _on_sell_pressed() -> void:
func _on_sell_all_pressed() -> void:
var catch_ids: Array[StringName] = _get_sell_all_catch_ids()
if catch_ids.is_empty():
_transaction_feedback.text = "No sellable fish in the cooler."
_set_transaction_feedback("No sellable fish in the cooler.")
return
_begin_sale_confirmation(catch_ids)
@ -4091,14 +4088,14 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
and _network_sale_service.is_local_sale_pending()
)
):
_transaction_feedback.text = "Selling…"
_set_transaction_feedback("Selling…")
return
if (
_network_sale_service == null
or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id)
):
_transaction_feedback.text = (
_set_transaction_feedback(
"Selling is not supported by this server."
)
return
@ -4114,7 +4111,7 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
active_buyer
)
if not preview.is_success():
_transaction_feedback.text = (
_set_transaction_feedback(
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
@ -4128,15 +4125,15 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
_confirmation_generation = _menu_generation
var fish_label: String = "fish"
_confirmation_message.text = (
"sell %d %s to the %s for $%d?\ncombined base value: $%d"
% [
preview.fish_count,
fish_label,
_get_buyer_display_group(active_buyer),
preview.payout,
preview.base_value,
]
)
"[center]sell %d %s to the %s for %s?\n"
+ "combined base value: %s[/center]"
) % [
preview.fish_count,
fish_label,
_get_buyer_display_group(active_buyer),
CurrencyPresentationType.bbcode_amount(preview.payout, 22),
CurrencyPresentationType.bbcode_amount(preview.base_value, 22),
]
_sale_confirmation.visible = true
if _shop_cooler_context_active:
shop_cooler_modal_changed.emit(true)
@ -4182,7 +4179,7 @@ func _on_confirm_sale_pressed() -> void:
and transaction_generation == _menu_generation
and (visible or _shop_cooler_context_active)
):
_transaction_feedback.text = "Selling…"
_set_transaction_feedback("Selling…")
func _can_use_shared_world_actions() -> bool:
@ -4195,7 +4192,7 @@ func _can_use_shared_world_actions() -> bool:
func _on_network_sale_pending(_request_id: String) -> void:
_sale_in_progress = true
if visible or _shop_cooler_context_active:
_transaction_feedback.text = "Selling…"
_set_transaction_feedback("Selling…")
_update_sale_summary()
@ -4213,11 +4210,12 @@ func _on_network_sale_finished(
return
_refresh_all()
var feedback_message: String = (
"Sale complete. $%d received." % payout
"Sale complete • %s received."
% CurrencyPresentationType.bbcode_amount(payout, 20)
if accepted
else message
)
_transaction_feedback.text = feedback_message
_set_transaction_feedback(feedback_message)
if not accepted:
_sale_unavailable.text = feedback_message
_sale_unavailable.visible = true
@ -4273,7 +4271,7 @@ func _revalidate_confirmation() -> void:
or _confirmation_buyer.id != _confirmation_buyer_id
):
_close_sale_confirmation()
_transaction_feedback.text = "sale selection is no longer available."
_set_transaction_feedback("sale selection is no longer available.")
return
var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids,
@ -4288,7 +4286,7 @@ func _revalidate_confirmation() -> void:
)
):
_close_sale_confirmation()
_transaction_feedback.text = (
_set_transaction_feedback(
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
@ -4308,6 +4306,10 @@ func _get_buyer_display_group(
return "buyer"
func _set_transaction_feedback(message: String) -> void:
_transaction_feedback.text = "[center]%s[/center]" % message
func _get_active_sale_buyer() -> FishBuyerProfileType:
if _sale_buyer_override != null and _sale_buyer_override.is_valid():
return _sale_buyer_override

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=33 format=3]
[gd_scene load_steps=34 format=3]
[ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
@ -31,6 +31,7 @@
[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/profile_simple.png" id="29_profile_simple"]
[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/online_simple.png" id="30_online_simple"]
[ext_resource type="Texture2D" path="res://ui/icons/tab_menu/close.png" id="31_close"]
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="32_currency"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
@ -224,7 +225,7 @@ theme_override_font_sizes/font_size = 19
text = "cooler notes"
horizontal_alignment = 1
[node name="NotepadWalletValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"]
[node name="NotepadWalletValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation" instance=ExtResource("32_currency")]
unique_name_in_owner = true
layout_mode = 0
offset_left = 18.0
@ -232,9 +233,7 @@ offset_top = 61.0
offset_right = 138.0
offset_bottom = 84.0
mouse_filter = 2
theme_override_colors/font_color = Color(0.16, 0.16, 0.14, 1)
theme_override_font_sizes/font_size = 14
text = "$0"
icon_size = 14.0
[node name="NotepadCapacityValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"]
unique_name_in_owner = true
@ -367,12 +366,11 @@ mouse_filter = 2
theme_override_colors/font_color = Color(0.16, 0.16, 0.14, 1)
theme_override_font_sizes/font_size = 18
[node name="CoolerOfferValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation/CoolerDetailStats/CoolerOfferRow"]
[node name="CoolerOfferValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation/CoolerDetailStats/CoolerOfferRow" instance=ExtResource("32_currency")]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.16, 0.16, 0.14, 1)
theme_override_font_sizes/font_size = 15
icon_size = 15.0
[node name="CoolerSelectionSummary" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"]
unique_name_in_owner = true
@ -442,12 +440,11 @@ theme_override_colors/font_color = Color(0.2, 0.18, 0.15, 0.86)
theme_override_font_sizes/font_size = 18
text = "combined offer"
[node name="CoolerCombinedOfferValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation/CoolerSelectionSummary/CoolerCombinedOfferRow"]
[node name="CoolerCombinedOfferValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation/CoolerSelectionSummary/CoolerCombinedOfferRow" instance=ExtResource("32_currency")]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.2, 0.18, 0.15, 0.86)
theme_override_font_sizes/font_size = 14
icon_size = 14.0
[node name="FavoriteBubble" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation" instance=ExtResource("12_ink_action")]
unique_name_in_owner = true
@ -965,24 +962,31 @@ unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="WalletBalance" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header"]
[node name="WalletHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header"]
layout_mode = 2
text = "wallet"
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
[node name="WalletBalance" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Header" instance=ExtResource("32_currency")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "wallet: $0"
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
alignment = 0
[node name="Separator" type="HSeparator" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
[node name="TransactionFeedback" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
[node name="TransactionFeedback" type="RichTextLabel" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 30)
layout_mode = 2
text = ""
horizontal_alignment = 1
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
theme_override_font_sizes/font_size = 21
bbcode_enabled = true
fit_content = true
scroll_active = false
theme_override_colors/default_color = Color(1, 0.82, 0.4, 1)
theme_override_font_sizes/normal_font_size = 21
[node name="Content" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true
@ -1057,12 +1061,16 @@ layout_mode = 2
text = "0 / 12"
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1)
[node name="HeldValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
[node name="HeldValueHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar"]
visible = false
layout_mode = 2
text = "held fish base value"
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
[node name="HeldValue" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection/SortBar" instance=ExtResource("32_currency")]
unique_name_in_owner = true
visible = false
layout_mode = 2
text = "held fish base value: $0"
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
[node name="InventoryBody" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"]
unique_name_in_owner = true
@ -1366,12 +1374,15 @@ layout_mode = 2
theme_override_constants/separation = 12
alignment = 1
[node name="ConfirmationMessage" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"]
[node name="ConfirmationMessage" type="RichTextLabel" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 64)
layout_mode = 2
theme_override_font_sizes/font_size = 22
bbcode_enabled = true
fit_content = true
scroll_active = false
theme_override_font_sizes/normal_font_size = 22
autowrap_mode = 2
horizontal_alignment = 1
[node name="ConfirmationButtons" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"]
layout_mode = 2

View file

@ -2,7 +2,13 @@ class_name SurfaceDrawingToolbar
extends Control
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
const TOOLBAR_WIDTH: float = 580.0
const MARKER_MODE_ICON: Texture2D = preload(
"res://items/icons/art/art_kit_marker.png"
)
const GRID_MODE_ICON: Texture2D = preload(
"res://items/icons/art/art_kit_grid_light.png"
)
const TOOLBAR_WIDTH: float = 510.0
const TOOLBAR_HEIGHT: float = 373.0
const COLOR_RAIL_WIDTH: float = 46.0
@ -17,7 +23,6 @@ const COLOR_RAIL_WIDTH: float = 46.0
@onready var _hide_guide_button: Button = %HideGuideButton
@onready var _restore_guide_button: Button = %RestoreGuideButton
@onready var _finalize_guide_button: Button = %FinalizeGuideButton
@onready var _close_button: Button = %CloseButton
var _service: NetworkSurfaceDrawingService
var _unlocks: PlayerArtUnlocks
@ -38,6 +43,9 @@ func _ready() -> void:
_make_button_round(button, 22)
_mode_button.custom_minimum_size = Vector2(48, 48)
_make_button_round(_mode_button, 24)
_enlarge_action_icon(_mode_button)
for button: Button in _action_buttons():
_enlarge_action_icon(button)
_mode_button.pressed.connect(_toggle_mode)
_eraser_button.pressed.connect(_toggle_eraser)
_undo_button.pressed.connect(_undo_last_stroke)
@ -50,7 +58,6 @@ func _ready() -> void:
_finalize_guide_button.pressed.connect(
_arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.FINALIZE)
)
_close_button.pressed.connect(_close_toolbar)
_brush_option.item_selected.connect(_select_brush)
_grid_option.item_selected.connect(_select_grid)
_build_options()
@ -75,7 +82,6 @@ func _action_buttons() -> Array[Button]:
_hide_guide_button,
_restore_guide_button,
_finalize_guide_button,
_close_button,
]
@ -91,6 +97,25 @@ func _make_button_round(button: Button, radius: int) -> void:
button.add_theme_stylebox_override(style_name, style)
func _enlarge_action_icon(button: Button) -> void:
# The shared ocean button uses generous text padding, leaving only a
# 20-pixel icon area in these compact bubbles. Keep the 48x44 control and
# hitbox intact while giving icon-only actions a full 40x40 presentation.
button.add_theme_constant_override("icon_max_width", 40)
for style_name: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var existing: StyleBox = button.get_theme_stylebox(style_name)
var style := existing.duplicate() as StyleBoxFlat
if style == null:
continue
style.content_margin_left = 4.0
style.content_margin_right = 4.0
style.content_margin_top = 2.0
style.content_margin_bottom = 2.0
button.add_theme_stylebox_override(style_name, style)
func setup(
service: NetworkSurfaceDrawingService,
unlocks: PlayerArtUnlocks,
@ -125,6 +150,8 @@ func _apply_dock_side() -> void:
offset_right = 0.0 if _dock_right else TOOLBAR_WIDTH
offset_top = 0.0
offset_bottom = TOOLBAR_HEIGHT
_top_panel.offset_left = 0.0
_top_panel.offset_right = TOOLBAR_WIDTH
_color_panel.offset_left = (
TOOLBAR_WIDTH - COLOR_RAIL_WIDTH if _dock_right else 0.0
)
@ -255,11 +282,6 @@ func _arm_guide_action(action: int) -> void:
_service.arm_guide_action(action)
func _close_toolbar() -> void:
if _service != null:
_service.deactivate()
func _on_unlocks_changed(_unlock_mask: int) -> void:
_refresh_unlocks()
@ -276,7 +298,14 @@ func _on_service_state_changed(
visible = is_active
if not is_active:
return
_mode_button.text = "" if mode_name == "place grid" else ""
_mode_button.icon = (
GRID_MODE_ICON if mode_name == "place grid" else MARKER_MODE_ICON
)
_mode_button.accessibility_name = (
"switch to marker mode"
if mode_name == "place grid"
else "switch to grid mode"
)
_mode_button.tooltip_text = (
"Switch to marker mode"
if mode_name == "place grid"

View file

@ -1,10 +1,17 @@
[gd_scene load_steps=6 format=3]
[gd_scene load_steps=11 format=3]
[ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/undo_light.png" id="3_undo_light"]
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/check_mark_light.png" id="4_check_mark_light"]
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="5_x_light"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_eraser.png" id="4_art_kit_eraser"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_marker.png" id="5_art_kit_marker"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_light.png" id="6_grid_light"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_hidden_light.png" id="7_grid_hidden"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_restore_light.png" id="8_grid_restore"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_finish_light.png" id="9_grid_finish"]
[sub_resource type="ButtonGroup" id="GuideActionGroup"]
allow_unpress = true
[node name="SurfaceDrawingToolbar" type="Control"]
visible = false
@ -13,7 +20,7 @@ layout_mode = 0
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -580.0
offset_left = -510.0
offset_bottom = 373.0
grow_horizontal = 0
mouse_filter = 2
@ -23,19 +30,13 @@ script = ExtResource("1_script")
[node name="TopPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
layout_mode = 0
offset_right = 580.0
offset_right = 510.0
offset_bottom = 60.0
[node name="Top" type="HBoxContainer" parent="TopPanel"]
layout_mode = 2
theme_override_constants/separation = 5
[node name="ModeButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "switch between grid and marker mode"
text = "▦"
[node name="BrushOption" type="OptionButton" parent="TopPanel/Top"]
unique_name_in_owner = true
custom_minimum_size = Vector2(80, 48)
@ -51,9 +52,12 @@ tooltip_text = "grid size"
[node name="EraserButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
texture_filter = 1
tooltip_text = "toggle eraser"
toggle_mode = true
text = "⌫"
accessibility_name = "toggle eraser"
icon = ExtResource("4_art_kit_eraser")
expand_icon = true
[node name="UndoButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
@ -67,42 +71,51 @@ expand_icon = true
[node name="HideGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "hide grid on next click"
texture_filter = 1
tooltip_text = "toggle hide grid mode"
toggle_mode = true
text = "◌"
button_group = SubResource("GuideActionGroup")
accessibility_name = "toggle hide grid mode"
icon = ExtResource("7_grid_hidden")
expand_icon = true
[node name="RestoreGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "restore hidden grid on next click"
texture_filter = 1
tooltip_text = "toggle show grid mode"
toggle_mode = true
text = "▤"
button_group = SubResource("GuideActionGroup")
accessibility_name = "toggle show grid mode"
icon = ExtResource("8_grid_restore")
expand_icon = true
[node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
texture_filter = 1
tooltip_text = "finish grid on next click"
tooltip_text = "toggle finalize grid mode"
toggle_mode = true
accessibility_name = "finish grid on next click"
icon = ExtResource("4_check_mark_light")
button_group = SubResource("GuideActionGroup")
accessibility_name = "toggle finalize grid mode"
icon = ExtResource("9_grid_finish")
expand_icon = true
[node name="CloseButton" type="Button" parent="TopPanel/Top"]
[node name="ModeButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true
layout_mode = 2
texture_filter = 1
tooltip_text = "close art tools"
accessibility_name = "close art tools"
icon = ExtResource("5_x_light")
tooltip_text = "switch between grid and marker mode"
accessibility_name = "switch to grid mode"
icon = ExtResource("5_art_kit_marker")
expand_icon = true
[node name="ColorPanel" type="PanelContainer" parent="."]
unique_name_in_owner = true
layout_mode = 0
offset_left = 534.0
offset_left = 464.0
offset_top = 54.0
offset_right = 580.0
offset_right = 510.0
offset_bottom = 373.0
[node name="ColorList" type="VBoxContainer" parent="ColorPanel"]

View file

@ -1,6 +1,10 @@
class_name TheNetPage
extends Control
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
enum View {
DAILY,
LIFETIME,
@ -278,15 +282,11 @@ func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void:
)
content.add_child(count)
var reward := Label.new()
reward.custom_minimum_size.x = 98.0
reward.text = "$%d%d xp" % [
int(job.get("fish_coin", 0)), int(job.get("experience", 0)),
]
reward.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
reward.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
var reward := _make_reward_display(
int(job.get("fish_coin", 0)),
int(job.get("experience", 0)),
)
reward.custom_minimum_size.x = 128.0
content.add_child(reward)
var claim := Button.new()
@ -326,17 +326,19 @@ func _build_payment_rows() -> void:
var content := HBoxContainer.new()
content.add_theme_constant_override("separation", 14)
row.add_child(content)
var reward_details := VBoxContainer.new()
reward_details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var label := Label.new()
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.text = "%s\n$%d%d xp" % [
str(reward.get("title", "completed job")),
int(reward.get("fish_coin", 0)),
int(reward.get("experience", 0)),
]
label.text = str(reward.get("title", "completed job"))
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
content.add_child(label)
reward_details.add_child(label)
reward_details.add_child(_make_reward_display(
int(reward.get("fish_coin", 0)),
int(reward.get("experience", 0)),
))
content.add_child(reward_details)
var claim := Button.new()
claim.text = "claim"
claim.custom_minimum_size = Vector2(110.0, 42.0)
@ -363,6 +365,27 @@ func _claim(claim_id: String) -> void:
_refresh()
func _make_reward_display(fish_coin: int, experience: int) -> HBoxContainer:
var reward := HBoxContainer.new()
reward.mouse_filter = Control.MOUSE_FILTER_IGNORE
reward.add_theme_constant_override("separation", 7)
var currency: CurrencyAmount = (
CurrencyPresentationType.instantiate_amount(fish_coin, 18.0)
)
currency.get_amount_label().add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
reward.add_child(currency)
var experience_label := Label.new()
experience_label.text = "%d xp" % experience
experience_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
experience_label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
reward.add_child(experience_label)
return reward
func _add_empty(message: String) -> void:
var empty := Label.new()
empty.text = message

Some files were not shown because too many files have changed in this diff Show more