Polish world, shop, and artwork presentation

This commit is contained in:
Alexander Sellite 2026-08-13 14:53:39 -04:00
parent 7d38bc962b
commit 117fdbfdaa
94 changed files with 1771 additions and 579 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/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" 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` gameplay areas are not scanned unless selected explicitly. Only `SALT_WATER`
bodies generate ribbons. bodies generate ribbons.
In the editor, select the baker and press **Rebuild Shoreline Ribbons**. The The editor automatically rebuilds configured shoreline resources when an
equivalent validation command is: 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 ```sh
godot --headless --path . --script scripts/bake_shoreline_ribbons.gd godot --headless --path . --script scripts/bake_shoreline_ribbons.gd
``` ```
Use `debug_path_stage` only while comparing extraction stages and leave it Off Use `debug_path_stage` only while comparing extraction stages and leave it Off
in committed scenes. Rebuild when saltwater terrain at the waterline, water in committed scenes. A rebuild is required when saltwater terrain at the
height, or generation bounds change. Normal gameplay loads committed generated waterline, water height, or generation bounds change; terrain reimports handle
meshes and never runs extraction. the usual case automatically. Normal gameplay loads committed generated meshes
and never runs extraction.
### Facial-feature textures ### 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 transparent canvas. Restart a development build after adding an image; rebuild
exports to package new `res://` files. 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 ### Bubble menus
Instance `ui/components/bubble_menu/bubble_button.tscn` for each action, or 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_singular = "pelican"
animal_name_plural = "pelicans" animal_name_plural = "pelicans"
payout_multiplier = 0.25 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 var buyer_name: String = animal_name_plural
if buyer_name.is_empty(): if buyer_name.is_empty():
buyer_name = display_name buyer_name = display_name
return "you sold your %s to the %s for $%d." % [ return "you sold your %s to the %s." % [
fish_name, fish_name,
buyer_name, buyer_name,
payout,
] ]

View file

@ -165,9 +165,10 @@ func _validate_batch(
result.payout result.payout
) )
else: 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.fish_count,
result.buyer_animal_name, result.buyer_animal_name,
result.payout, ]
] )
return result return result

View file

@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] [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] [resource]
script = ExtResource("1_item") 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" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bm0kkevv62mu0" uid="uid://datu7j7tq2k3e"
path="res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex" path="res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://items/icons/placeholder/art_kit.svg" source_file="res://items/icons/art/art_kit.png"
dest_files=["res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"] dest_files=["res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"]
[params] [params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 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: 1.3 KiB

View file

@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://trqvs4dgd4rt" uid="uid://cdbba3bvhuear"
path="res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex" path="res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://items/icons/placeholder/fish_coin.svg" source_file="res://items/icons/art/art_kit_eraser.png"
dest_files=["res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"] dest_files=["res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"]
[params] [params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=1 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: 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": "catch":
return "seasoned angler" return "seasoned angler"
"sell": "sell":
return "fish coin regular" return "market regular"
"level": "level":
return "reach level %d" % target return "reach level %d" % target
"discover": "discover":

View file

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

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

View file

@ -344,7 +344,7 @@ func arm_guide_action(action: int) -> bool:
_selected_canvas_id = "" _selected_canvas_id = ""
_update_aim() _update_aim()
_refresh_stencil_visibility() _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 return true
@ -1874,8 +1874,10 @@ func _execute_armed_guide_action() -> void:
_restore_selected_guide() _restore_selected_guide()
GuideAction.FINALIZE: GuideAction.FINALIZE:
_finalize_selected_guide() _finalize_selected_guide()
_clear_armed_guide_action(true) # Guide actions are persistent modes. Keep the selected action armed so
_emit_hud_state("") # 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: func _cancel_armed_guide_action() -> void:
@ -1904,12 +1906,18 @@ func _guide_action_verb(action: int) -> String:
GuideAction.HIDE: GuideAction.HIDE:
return "hide" return "hide"
GuideAction.RESTORE: GuideAction.RESTORE:
return "restore" return "show"
GuideAction.FINALIZE: GuideAction.FINALIZE:
return "finish" return "finish"
return "use" 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: func _remove_selected_guide() -> void:
if _selected_canvas_id.is_empty(): if _selected_canvas_id.is_empty():
_emit_hud_state("aim at a placed grid before removing its guide") _emit_hud_state("aim at a placed grid before removing its guide")

View file

@ -5,7 +5,7 @@ render_mode unshaded, cull_back;
// base color and the red, green, and blue channels select the three authored // base color and the red, green, and blue channels select the three authored
// accent colors. Alpha is intentionally ignored so artists may leave unused // accent colors. Alpha is intentionally ignored so artists may leave unused
// UV space transparent. // 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 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 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); 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"] [node name="HeldArtKitSprite" type="Sprite3D" parent="HeldArtKitDisplay"]
unique_name_in_owner = true unique_name_in_owner = true
rotation = Vector3(0, 0, 4.71239)
pixel_size = 0.008 pixel_size = 0.008
flip_h = true
shaded = false shaded = false
double_sided = true double_sided = true
texture_filter = 1 texture_filter = 0
[node name="CatchDisplay" type="Node3D" parent="."] [node name="CatchDisplay" type="Node3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true

View file

@ -350,6 +350,7 @@ var _held_item_attachment: BoneAttachment3D
var _held_fish_display: Node3D var _held_fish_display: Node3D
var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
var _held_fish_sprite: Sprite3D var _held_fish_sprite: Sprite3D
var _held_art_kit_display: Node3D
var _held_art_kit_sprite: Sprite3D var _held_art_kit_sprite: Sprite3D
var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
@ -510,6 +511,7 @@ func _initialize_held_item_attachment() -> void:
_held_fish_sprite = attachment.get_node( _held_fish_sprite = attachment.get_node(
"HeldFishDisplay/HeldFishSprite" "HeldFishDisplay/HeldFishSprite"
) as Sprite3D ) as Sprite3D
_held_art_kit_display = attachment.get_node("HeldArtKitDisplay") as Node3D
_held_art_kit_sprite = attachment.get_node( _held_art_kit_sprite = attachment.get_node(
"HeldArtKitDisplay/HeldArtKitSprite" "HeldArtKitDisplay/HeldArtKitSprite"
) as Sprite3D ) as Sprite3D
@ -1710,6 +1712,8 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
_held_art_kit_visible = true _held_art_kit_visible = true
_held_art_kit_sprite.texture = icon _held_art_kit_sprite.texture = icon
_held_art_kit_sprite.visible = true _held_art_kit_sprite.visible = true
if not previous_show_pose:
_held_art_kit_display.visible = false
_fishing_rod.visible = false _fishing_rod.visible = false
if item_changed: if item_changed:
_begin_pocket_visual( _begin_pocket_visual(
@ -1717,8 +1721,11 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
previous_show_pose, previous_show_pose,
true, true,
Callable(), Callable(),
false,
_apply_active_art_kit_visual,
) )
else: else:
_apply_active_art_kit_visual()
_update_character_animation() _update_character_animation()
@ -1798,6 +1805,7 @@ func _apply_pending_held_fish() -> void:
func _clear_active_art_kit() -> void: func _clear_active_art_kit() -> void:
var was_visible: bool = _held_art_kit_visible var was_visible: bool = _held_art_kit_visible
_held_art_kit_visible = false _held_art_kit_visible = false
_held_art_kit_display.visible = false
_held_art_kit_sprite.texture = null _held_art_kit_sprite.texture = null
_held_art_kit_sprite.visible = false _held_art_kit_sprite.visible = false
if was_visible: if was_visible:
@ -1807,6 +1815,12 @@ func _clear_active_art_kit() -> void:
_update_character_animation() _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( func _begin_pocket_visual(
target: PocketVisualTarget, target: PocketVisualTarget,
starts_in_show_pose: bool, starts_in_show_pose: bool,

View file

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

View file

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

View file

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

View file

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

View file

@ -325,6 +325,25 @@ func _run() -> void:
and finalize_button != null and finalize_button != null
and close_button != null and close_button != null
) )
assert(eraser_button.text.is_empty())
assert(eraser_button.icon != null)
assert(
eraser_button.icon.resource_path.ends_with(
"/art/art_kit_eraser.png"
)
)
for icon_button: Button in [eraser_button, undo_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(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(brush_option.item_count == 4)
assert(grid_option.item_count == 4) assert(grid_option.item_count == 4)
assert(not brush_option.get_popup().is_item_disabled(0)) assert(not brush_option.get_popup().is_item_disabled(0))
@ -367,30 +386,42 @@ func _run() -> void:
world_click.button_index = MOUSE_BUTTON_LEFT world_click.button_index = MOUSE_BUTTON_LEFT
world_click.pressed = true world_click.pressed = true
assert(service.handle_input(world_click, 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( assert(
service.get_armed_guide_action() service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE == NetworkSurfaceDrawingService.GuideAction.NONE
) )
assert(not service.is_placement_mode()) assert(not service.is_placement_mode() and service.is_eraser_mode())
assert(service.is_eraser_mode())
restore_button.pressed.emit() restore_button.pressed.emit()
assert( assert(
service.get_armed_guide_action() service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.RESTORE == 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() finalize_button.pressed.emit()
assert( assert(
service.get_armed_guide_action() service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE == NetworkSurfaceDrawingService.GuideAction.FINALIZE
) )
assert(not restore_button.button_pressed)
assert(finalize_button.button_pressed)
assert(service.handle_input(world_click, true)) 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()) assert(not service.is_placement_mode() and service.is_eraser_mode())
(color_buttons[&"ocean_teal"] as Button).pressed.emit() (color_buttons[&"ocean_teal"] as Button).pressed.emit()
assert(not service.is_eraser_mode()) assert(not service.is_eraser_mode())
@ -410,6 +441,32 @@ func _run() -> void:
player.hotbar.select_slot(0) player.hotbar.select_slot(0)
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
await process_frame 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(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
assert(service.is_active() and toolbar.visible) assert(service.is_active() and toolbar.visible)
assert(service.get_grid_size() == 128) assert(service.get_grid_size() == 128)

View file

@ -47,6 +47,10 @@ func _run() -> void:
assert(body != null) assert(body != null)
var body_material := body.material_override as StandardMaterial3D var body_material := body.material_override as StandardMaterial3D
assert(body_material != null) assert(body_material != null)
assert(
body_material.texture_filter
== BaseMaterial3D.TEXTURE_FILTER_NEAREST
)
assert(body_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED) assert(body_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
assert(is_zero_approx(body_material.metallic)) assert(is_zero_approx(body_material.metallic))
assert(is_zero_approx(body_material.metallic_specular)) 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 ArtShopStock.ART_KIT_ITEM_ID
) )
assert(art_item != null and art_item.hotbar_allowed and art_item.equippable) 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)) assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
for product_id: StringName in [ for product_id: StringName in [
&"marker_ocean_teal", &"brush_2x", &"grid_32x", &"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 var shop_panel_style := shop_panel.get_theme_stylebox("panel") as StyleBoxFlat
assert(shop_panel_style != null) assert(shop_panel_style != null)
assert(shop_panel_style.corner_radius_top_left == 24) 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")) assert(not shop.has_node("ShopPanel/Margin/Layout/ModeTabs"))
var shop_tabs: Array = shop.get("_shop_tabs") as Array var shop_tabs: Array = shop.get("_shop_tabs") as Array
assert(shop_tabs.size() == 6) assert(shop_tabs.size() == 6)
@ -582,6 +619,37 @@ func _test_fishing_shop_sale_ui(
if child is Label: if child is Label:
stock_sections.append((child as Label).text) stock_sections.append((child as Label).text)
assert(stock_sections == ["art kit", "markers", "brushes", "grids"]) 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 _activate_pointer_control(sell_mode, ui_viewport)
await process_frame await process_frame
assert(shop.visible and not player_menu.visible) 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(not covered_ocean.is_fishable())
assert(covered_ocean.position.y > -0.45) 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() fishing_spot.queue_free()
region.queue_free() region.queue_free()
await process_frame 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

@ -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 _heading: Label = %Heading
@onready var _value: Label = %Value @onready var _value: Label = %Value
@onready var _currency_icon: TextureRect = %CurrencyIcon
func _ready() -> void: func _ready() -> void:
@ -16,3 +17,10 @@ func _ready() -> void:
func set_content(heading: String, value: String) -> void: func set_content(heading: String, value: String) -> void:
_heading.text = heading _heading.text = heading
_value.text = value _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="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="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"] [node name="BubbleStatusBubble" type="PanelContainer"]
custom_minimum_size = Vector2(112, 62) custom_minimum_size = Vector2(112, 62)
@ -23,7 +24,25 @@ theme_override_font_sizes/font_size = 13
text = "status" text = "status"
horizontal_alignment = 1 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
mouse_filter = 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( const FALLBACK_SUPPLY_ICON: Texture2D = preload(
"res://ui/icons/pictograms/x_light.png" "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_visibility_changed(is_open: bool)
signal menu_exit_started 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_Y: float = 60.0
const SUPPLY_PRICE_HEIGHT: float = 24.0 const SUPPLY_PRICE_HEIGHT: float = 24.0
const SUPPLY_PRICE_HORIZONTAL_PADDING: float = 12.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 _wallet_label: Label = %WalletLabel
@onready var _shop_panel: PanelContainer = %ShopPanel @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_page: Control = %ShopCoolerPage
@onready var _shop_cooler_mount: Control = %ShopCoolerMount @onready var _shop_cooler_mount: Control = %ShopCoolerMount
@onready var _shop_body: HBoxContainer = $ShopPanel/Margin/Layout/Body @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 _shop_tab_bar: HBoxContainer = %ShopTabBar
@onready var _upgrades_content: VBoxContainer = %Upgrades @onready var _upgrades_content: VBoxContainer = %Upgrades
@onready var _supplies_content: VBoxContainer = %Supplies @onready var _supplies_content: VBoxContainer = %Supplies
@onready var _stock_title: Label = %StockTitle @onready var _stock_title: Label = %StockTitle
@onready var _reel_level: Label = %ReelLevel @onready var _reel_cost: CurrencyAmount = %ReelCost
@onready var _reel_effect: Label = %ReelEffect @onready var _reel_price_bubble: PanelContainer = %ReelPriceBubble
@onready var _reel_cost: Label = %ReelCost
@onready var _reel_purchase: Button = %ReelPurchase @onready var _reel_purchase: Button = %ReelPurchase
@onready var _barrier_level: Label = %BarrierLevel @onready var _barrier_cost: CurrencyAmount = %BarrierCost
@onready var _barrier_effect: Label = %BarrierEffect @onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble
@onready var _barrier_cost: Label = %BarrierCost
@onready var _barrier_purchase: Button = %BarrierPurchase @onready var _barrier_purchase: Button = %BarrierPurchase
@onready var _supplies_list: VBoxContainer = %SuppliesList @onready var _supplies_list: VBoxContainer = %SuppliesList
@onready var _cooler_level: Label = %CoolerLevel @onready var _cooler_cost: CurrencyAmount = %CoolerCost
@onready var _cooler_effect: Label = %CoolerEffect @onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
@onready var _cooler_cost: Label = %CoolerCost
@onready var _cooler_purchase: Button = %CoolerPurchase @onready var _cooler_purchase: Button = %CoolerPurchase
var _player: PlayerType var _player: PlayerType
@ -136,17 +144,17 @@ func _ready() -> void:
func _request_shop_cooler() -> bool: func _request_shop_cooler() -> bool:
if _transaction_in_progress: if _transaction_in_progress:
_feedback.text = "Finish the current transaction first." _set_feedback("Finish the current transaction first.")
return false return false
if not _is_transaction_context_valid(): 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 return false
sell_fish_requested.emit() sell_fish_requested.emit()
return _cooler_page_active return _cooler_page_active
func _focus_shop_section() -> void: func _focus_shop_section() -> void:
_feedback.text = "" _set_feedback("")
if _shop_section == ShopSection.UPGRADES: if _shop_section == ShopSection.UPGRADES:
_reel_purchase.grab_focus() _reel_purchase.grab_focus()
return return
@ -193,6 +201,10 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
if _closing: if _closing:
return return
if section_index == int(_shop_section): 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() _update_shop_tab_selection()
if focus_content: if focus_content:
_focus_shop_section() _focus_shop_section()
@ -236,13 +248,31 @@ func _apply_shop_styles() -> void:
"panel", shop_panel_style "panel", shop_panel_style
) )
for panel: PanelContainer in [ for panel: PanelContainer in [
$ShopPanel/Margin/Layout/Body/Upgrades/ReelCard, _reel_price_bubble,
$ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard, _barrier_price_bubble,
$ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard, _cooler_price_bubble,
]: ]:
panel.add_theme_stylebox_override( var price_style := UtilityPageStyleType.rounded_style(
"panel", UtilityPageStyleType.row_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 [ for button: BaseButton in [
%CloseButton, %CloseButton,
_reel_purchase, _reel_purchase,
@ -336,7 +366,7 @@ func open_shop() -> bool:
_player.set_camera_input_enabled(false) _player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_feedback.text = "" _set_feedback("")
deactivate_shop_cooler_page() deactivate_shop_cooler_page()
show() show()
_shop_tab_bar.show() _shop_tab_bar.show()
@ -520,7 +550,6 @@ func _refresh_upgrades() -> void:
return return
var reel_level: int = _upgrades.get_reel_speed_level() var reel_level: int = _upgrades.get_reel_speed_level()
var reel_cost: int = _upgrades.get_next_reel_speed_cost() var reel_cost: int = _upgrades.get_next_reel_speed_cost()
_reel_level.text = "level %d" % reel_level
_reel_purchase.disabled = ( _reel_purchase.disabled = (
reel_cost < 0 reel_cost < 0
or _transaction_in_progress or _transaction_in_progress
@ -529,27 +558,30 @@ func _refresh_upgrades() -> void:
or not _network_shop.can_request_purchase() or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_reel_speed(_wallet) or not _upgrades.can_purchase_reel_speed(_wallet)
) )
_reel_purchase.tooltip_text = ( var reel_effect: String
"Purchases are unavailable in this session."
if _network_shop == null or not _network_shop.can_request_purchase()
else ""
)
if reel_cost < 0: if reel_cost < 0:
_reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier() reel_effect = "%.2f× reel speed · maximum level" % (
_reel_cost.text = "max" _upgrades.get_reel_speed_multiplier()
)
_reel_price_bubble.hide()
else: else:
_reel_effect.text = ( _reel_price_bubble.show()
reel_effect = (
"%.2f×%.2f×" "%.2f×%.2f×"
% [ % [
_upgrades.get_reel_speed_multiplier(), _upgrades.get_reel_speed_multiplier(),
1.0 + float(reel_level + 1) * 0.10, 1.0 + float(reel_level + 1) * 0.10,
] ]
) )
_reel_cost.text = "$%d" % reel_cost _reel_cost.set_amount(reel_cost)
_reel_purchase.text = "max" if reel_cost < 0 else "purchase" _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_level: int = _upgrades.get_barrier_power_level()
var barrier_cost: int = _upgrades.get_next_barrier_power_cost() var barrier_cost: int = _upgrades.get_next_barrier_power_cost()
_barrier_level.text = "level %d" % barrier_level
_barrier_purchase.disabled = ( _barrier_purchase.disabled = (
barrier_cost < 0 barrier_cost < 0
or _transaction_in_progress or _transaction_in_progress
@ -558,20 +590,39 @@ func _refresh_upgrades() -> void:
or not _network_shop.can_request_purchase() or not _network_shop.can_request_purchase()
or not _upgrades.can_purchase_barrier_power(_wallet) or not _upgrades.can_purchase_barrier_power(_wallet)
) )
_barrier_purchase.tooltip_text = _reel_purchase.tooltip_text var barrier_effect: String
if barrier_cost < 0: if barrier_cost < 0:
_barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage() barrier_effect = "%d damage · maximum level" % (
_barrier_cost.text = "max" _upgrades.get_barrier_damage()
)
_barrier_price_bubble.hide()
else: else:
_barrier_effect.text = ( _barrier_price_bubble.show()
barrier_effect = (
"%d damage → %d damage" "%d damage → %d damage"
% [ % [
_upgrades.get_barrier_damage(), _upgrades.get_barrier_damage(),
barrier_level + 2, barrier_level + 2,
] ]
) )
_barrier_cost.text = "$%d" % barrier_cost _barrier_cost.set_amount(barrier_cost)
_barrier_purchase.text = "max" if barrier_cost < 0 else "purchase" _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: func _refresh_supplies() -> void:
@ -632,9 +683,8 @@ func _refresh_supplies() -> void:
var use_icon_tile: bool = ( var use_icon_tile: bool = (
current_stock_grid != null current_stock_grid != null
) )
button.text = "%s\n$%downed %d" % [ button.text = "%s\nowned %d" % [
item.display_name, item.display_name,
FishingShopStockType.get_price(item_id),
owned, owned,
] ]
var item_tooltip_text: String = item.description var item_tooltip_text: String = item.description
@ -646,27 +696,24 @@ func _refresh_supplies() -> void:
button.text = "" button.text = ""
else: else:
button.text = ( button.text = (
"%s\nunlock $%d" % [item.display_name, total_cost] "%s\nunlock and fill" % item.display_name
if not bait_unlocked if not bait_unlocked
else "%s\n$%d each • %d/%d" % [ else "%s\n%d/%d owned" % [
item.display_name, item.display_name,
FishingShopStockType.get_price(item_id),
owned, owned,
item.max_stack, item.max_stack,
] ]
) )
item_tooltip_text = ( item_tooltip_text = (
"%s\nunlock $%d • fills to %d/%d\n%s" % [ "%s\nunlock and fill to %d/%d\n%s" % [
item.display_name, item.display_name,
total_cost,
item.max_stack, item.max_stack,
item.max_stack, item.max_stack,
item.description, item.description,
] ]
if not bait_unlocked if not bait_unlocked
else "%s\n$%d each • %d/%d\n%s" % [ else "%s\n%d/%d owned\n%s" % [
item.display_name, item.display_name,
FishingShopStockType.get_price(item_id),
owned, owned,
item.max_stack, item.max_stack,
item.description, item.description,
@ -677,9 +724,8 @@ func _refresh_supplies() -> void:
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.alignment = HORIZONTAL_ALIGNMENT_CENTER button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.text = "" button.text = ""
item_tooltip_text = "%s\n$%downed %d\n%s" % [ item_tooltip_text = "%s\nowned %d\n%s" % [
item.display_name, item.display_name,
FishingShopStockType.get_price(item_id),
owned, owned,
item.description, item.description,
] ]
@ -704,7 +750,11 @@ func _refresh_supplies() -> void:
_add_supply_icon_tile( _add_supply_icon_tile(
current_stock_grid, current_stock_grid,
button, button,
FishingShopStockType.get_price(item_id), (
total_cost
if bait_topoff and not bait_unlocked
else FishingShopStockType.get_price(item_id)
),
) )
else: else:
_supplies_list.add_child(button) _supplies_list.add_child(button)
@ -769,14 +819,12 @@ func _add_art_kit_button(parent: GridContainer) -> void:
return return
var owned: bool = _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0 var owned: bool = _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0
var button: Button = _make_stock_button( var button: Button = _make_stock_button(
"%s\n$%d%s" % [ "%s\n%s" % [
item.display_name, item.display_name,
ArtShopStockType.ART_KIT_PRICE,
"owned" if owned else "not owned", "owned" if owned else "not owned",
], ],
"%s\n$%d%s\n%s" % [ "%s\n%s\n%s" % [
item.display_name, item.display_name,
ArtShopStockType.ART_KIT_PRICE,
"owned" if owned else "not owned", "owned" if owned else "not owned",
item.description, item.description,
], ],
@ -808,19 +856,26 @@ func _add_art_upgrade_button(
) )
var unlocked: bool = _art_unlocks.owns_product(product_id) var unlocked: bool = _art_unlocks.owns_product(product_id)
var button: Button = _make_stock_button( var button: Button = _make_stock_button(
"%s\n$%d%s" % [ "%s\n%s" % [
ArtShopStockType.get_display_name(product_id), ArtShopStockType.get_display_name(product_id),
ArtShopStockType.UPGRADE_PRICE,
"unlocked" if unlocked else "locked", "unlocked" if unlocked else "locked",
], ],
"%s\n$%d%s\n%s" % [ "%s\n%s\n%s" % [
ArtShopStockType.get_display_name(product_id), ArtShopStockType.get_display_name(product_id),
ArtShopStockType.UPGRADE_PRICE,
"unlocked" if unlocked else "locked", "unlocked" if unlocked else "locked",
ArtShopStockType.get_description(product_id), 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( _add_supply_icon_tile(
parent, parent,
button, button,
@ -863,6 +918,29 @@ func _configure_supply_icon_tile(
button.text = "" 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( func _add_supply_icon_tile(
parent: GridContainer, parent: GridContainer,
button: Button, button: Button,
@ -875,7 +953,7 @@ func _add_supply_icon_tile(
button.position = Vector2.ZERO button.position = Vector2.ZERO
button.size = SUPPLY_ICON_TILE_SIZE button.size = SUPPLY_ICON_TILE_SIZE
tile_host.add_child(button) 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( var price_text_width: float = UtilityPageStyleType.TuffyFont.get_string_size(
price_text, price_text,
HORIZONTAL_ALIGNMENT_LEFT, HORIZONTAL_ALIGNMENT_LEFT,
@ -884,7 +962,12 @@ func _add_supply_icon_tile(
).x ).x
var price_width: float = minf( var price_width: float = minf(
SUPPLY_ICON_TILE_SIZE.x, 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() var price_bubble := PanelContainer.new()
price_bubble.name = "PriceBubble" price_bubble.name = "PriceBubble"
@ -905,6 +988,23 @@ func _add_supply_icon_tile(
price_style.content_margin_bottom = 0.0 price_style.content_margin_bottom = 0.0
price_bubble.add_theme_stylebox_override("panel", price_style) price_bubble.add_theme_stylebox_override("panel", price_style)
tile_host.add_child(price_bubble) 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() var price_label := Label.new()
price_label.name = "Price" price_label.name = "Price"
price_label.mouse_filter = Control.MOUSE_FILTER_IGNORE price_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
@ -921,7 +1021,7 @@ func _add_supply_icon_tile(
"font_color", SUPPLY_PRICE_COLOR "font_color", SUPPLY_PRICE_COLOR
) )
price_label.add_theme_constant_override("outline_size", 0) 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: func _refresh_cooler_capacity() -> void:
@ -930,7 +1030,6 @@ func _refresh_cooler_capacity() -> void:
var level: int = _cooler_capacity.get_level() var level: int = _cooler_capacity.get_level()
var cost: int = _cooler_capacity.get_next_cost() var cost: int = _cooler_capacity.get_next_cost()
var next_capacity: int = _cooler_capacity.get_next_capacity() var next_capacity: int = _cooler_capacity.get_next_capacity()
_cooler_level.text = "level %d" % level
_cooler_purchase.disabled = ( _cooler_purchase.disabled = (
cost < 0 cost < 0
or _transaction_in_progress or _transaction_in_progress
@ -939,18 +1038,25 @@ func _refresh_cooler_capacity() -> void:
or not _network_shop.can_request_purchase() or not _network_shop.can_request_purchase()
or not _cooler_capacity.can_purchase(_wallet) or not _cooler_capacity.can_purchase(_wallet)
) )
_cooler_purchase.tooltip_text = _reel_purchase.tooltip_text var cooler_effect: String
if cost < 0: if cost < 0:
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity() cooler_effect = "%d fish · maximum level" % (
_cooler_cost.text = "max" _cooler_capacity.get_capacity()
_cooler_purchase.text = "max" )
_cooler_price_bubble.hide()
else: else:
_cooler_effect.text = "%d%d fish" % [ _cooler_price_bubble.show()
cooler_effect = "%d%d fish" % [
_cooler_capacity.get_capacity(), _cooler_capacity.get_capacity(),
next_capacity, next_capacity,
] ]
_cooler_cost.text = "$%d" % cost _cooler_cost.set_amount(cost)
_cooler_purchase.text = "purchase" _cooler_purchase.tooltip_text = _upgrade_tooltip(
"cooler capacity",
level,
cooler_effect,
)
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
func _purchase_reel_speed() -> void: func _purchase_reel_speed() -> void:
@ -963,65 +1069,65 @@ func _purchase_barrier_power() -> void:
func _purchase_supply(item_id: StringName) -> void: func _purchase_supply(item_id: StringName) -> void:
if _transaction_in_progress or not _is_transaction_context_valid(): if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase." _set_feedback("unable to complete purchase.")
return return
var owned: int = _bag.get_quantity(item_id) var owned: int = _bag.get_quantity(item_id)
var item: ItemDataType = _item_catalog.get_item_by_id(item_id) var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
if item == null: if item == null:
_feedback.text = "Purchase could not be completed." _set_feedback("Purchase could not be completed.")
return return
var quantity: int = FishingShopStockType.get_purchase_quantity( var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id, owned, item.max_stack item_id, owned, item.max_stack
) )
if quantity <= 0 or not _bag.can_add_item(item_id, quantity): 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 return
var total_cost: int = FishingShopStockType.get_purchase_cost( var total_cost: int = FishingShopStockType.get_purchase_cost(
item_id, quantity, _bag.is_bait_unlocked(item_id) item_id, quantity, _bag.is_bait_unlocked(item_id)
) )
if total_cost < 0 or not _wallet.can_afford(total_cost): if total_cost < 0 or not _wallet.can_afford(total_cost):
_feedback.text = "Not enough fish coin." _set_feedback("Insufficient funds.")
return return
if _network_shop == null: if _network_shop == null:
_feedback.text = "Purchase could not be completed." _set_feedback("Purchase could not be completed.")
return return
_network_shop.request_supply(item_id) _network_shop.request_supply(item_id)
func _purchase_art_kit() -> void: func _purchase_art_kit() -> void:
if _network_shop == null or not _is_transaction_context_valid(): 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 return
_network_shop.request_art_kit() _network_shop.request_art_kit()
func _purchase_art_upgrade(product_id: StringName) -> void: func _purchase_art_upgrade(product_id: StringName) -> void:
if _network_shop == null or not _is_transaction_context_valid(): 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 return
_network_shop.request_art_upgrade(product_id) _network_shop.request_art_upgrade(product_id)
func _purchase_cooler_capacity() -> void: func _purchase_cooler_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid(): if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase." _set_feedback("unable to complete purchase.")
return return
var cost: int = _cooler_capacity.get_next_cost() var cost: int = _cooler_capacity.get_next_cost()
if cost < 0: if cost < 0:
_feedback.text = "Upgrade is already at maximum." _set_feedback("Upgrade is already at maximum.")
return return
if not _wallet.can_afford(cost): if not _wallet.can_afford(cost):
_feedback.text = "Not enough fish coin." _set_feedback("Insufficient funds.")
return return
if _network_shop == null: if _network_shop == null:
_feedback.text = "Purchase could not be completed." _set_feedback("Purchase could not be completed.")
return return
_network_shop.request_cooler_capacity_upgrade() _network_shop.request_cooler_capacity_upgrade()
func _purchase_upgrade(is_reel_speed: bool) -> void: func _purchase_upgrade(is_reel_speed: bool) -> void:
if _transaction_in_progress or not _is_transaction_context_valid(): if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase." _set_feedback("unable to complete purchase.")
return return
var cost: int = ( var cost: int = (
_upgrades.get_next_reel_speed_cost() _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() else _upgrades.get_next_barrier_power_cost()
) )
if cost < 0: if cost < 0:
_feedback.text = "Upgrade is already at maximum." _set_feedback("Upgrade is already at maximum.")
return return
if not _wallet.can_afford(cost): if not _wallet.can_afford(cost):
_feedback.text = "Not enough fish coin." _set_feedback("Insufficient funds.")
return return
if _network_shop == null: if _network_shop == null:
_feedback.text = "Purchase could not be completed." _set_feedback("Purchase could not be completed.")
return return
if is_reel_speed: if is_reel_speed:
_network_shop.request_reel_speed_upgrade() _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: func _on_network_purchase_pending(_request_id: String) -> void:
_transaction_in_progress = true _transaction_in_progress = true
if visible: if visible:
_feedback.text = "Purchasing…" _set_feedback("Purchasing…")
_refresh_all() _refresh_all()
@ -1062,14 +1168,21 @@ func _on_network_purchase_finished(
_transaction_in_progress = false _transaction_in_progress = false
if not visible: if not visible:
return return
_feedback.text = ( _set_feedback(
"Purchase complete. $%d spent." % total_cost (
"Purchase complete • %s spent."
% CurrencyPresentation.bbcode_amount(total_cost, 18)
)
if accepted if accepted
else message else message
) )
_refresh_all() _refresh_all()
func _set_feedback(message: String) -> void:
_feedback.text = "[center]%s[/center]" % message
func _is_transaction_context_valid() -> bool: func _is_transaction_context_valid() -> bool:
return ( return (
visible 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="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="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_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/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://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="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"] [node name="FishingShop" type="Control"]
unique_name_in_owner = true unique_name_in_owner = true
@ -126,12 +127,16 @@ icon = ExtResource("7_close")
expand_icon = true expand_icon = true
icon_max_width = 40 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 28)
layout_mode = 2 layout_mode = 2
text = "" text = ""
horizontal_alignment = 1 bbcode_enabled = true
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) 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="."] [node name="ShopTabBar" type="HBoxContainer" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
@ -184,164 +189,102 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/separation = 5 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 layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard"] [node name="UpgradeGrid" type="GridContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll"]
layout_mode = 2 unique_name_in_owner = true
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"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 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 layout_mode = 2
text = "reel speed" mouse_filter = 2
theme_override_font_sizes/font_size = 22
[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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "level 0" amount = 50
theme_override_font_sizes/font_size = 18 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "1.00× → 1.10×" amount = 100
theme_override_font_sizes/font_size = 18 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "$50" amount = 75
theme_override_font_sizes/font_size = 18 icon_size = 24.0
[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"

View file

@ -16,6 +16,9 @@ const InventoryNotepadType = preload(
const LogbookPortraitType = preload( const LogbookPortraitType = preload(
"res://ui/components/logbook_portrait.gd" "res://ui/components/logbook_portrait.gd"
) )
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const DETAIL_FADE_DURATION: float = 0.12 const DETAIL_FADE_DURATION: float = 0.12
const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION 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(left_stats, "number caught", "unknown")
_add_detail_row(right_stats, "rarity", fish.get_rarity_name().to_lower()) _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(right_stats, "time of day", _availability_text(fish))
_add_detail_row( _add_currency_detail_row(
right_stats, right_stats,
"value range", "value range",
"%d%d fish coin" % [ FishQualityType.apply_sale_value(
FishQualityType.apply_sale_value( fish.sell_value_min,
fish.sell_value_min, FishQualityType.Tier.BORING,
FishQualityType.Tier.BORING, ),
), FishQualityType.apply_sale_value(
FishQualityType.apply_sale_value( fish.sell_value_max,
fish.sell_value_max, FishQualityType.Tier.SHINY,
FishQualityType.Tier.SHINY, ),
),
],
) )
_add_detail_row( _add_detail_row(
right_stats, right_stats,
@ -878,6 +879,31 @@ func _add_detail_row(
parent.add_child(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: func _make_stats_column() -> VBoxContainer:
var column := VBoxContainer.new() var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL 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", "good_luck_have_fun": "Good Luck Have Fun",
} }
const FishQualityType = preload("res://fish/fish_quality.gd") const FishQualityType = preload("res://fish/fish_quality.gd")
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
var _service: NetworkMailService var _service: NetworkMailService
var _reservations: PlayerAssetReservationService var _reservations: PlayerAssetReservationService
@ -36,14 +39,16 @@ var _signature: Label
var _attachment_kind: OptionButton var _attachment_kind: OptionButton
var _attachment_choice: OptionButton var _attachment_choice: OptionButton
var _attachment_amount: LineEdit 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_minus: Button
var _amount_plus: Button var _amount_plus: Button
var _attachment_summary: Label var _attachment_summary: RichTextLabel
var _send_button: Button var _send_button: Button
var _status: Label var _status: Label
var _letter_text: Label var _letter_text: Label
var _letter_gift: Label var _letter_gift: RichTextLabel
var _accept: Button var _accept: Button
var _decline: Button var _decline: Button
var _archive: Button var _archive: Button
@ -212,7 +217,7 @@ func _build_compose() -> Control:
_attachment_kind = OptionButton.new() _attachment_kind = OptionButton.new()
_attachment_kind.position = Vector2(688, 48) _attachment_kind.position = Vector2(688, 48)
_attachment_kind.size = Vector2(280, 46) _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.add_item(label)
_attachment_kind.item_selected.connect(_refresh_attachment_choices) _attachment_kind.item_selected.connect(_refresh_attachment_choices)
page.add_child(_attachment_kind) page.add_child(_attachment_kind)
@ -225,13 +230,34 @@ func _build_compose() -> Control:
_update_attachment_summary() _update_attachment_summary()
) )
page.add_child(_attachment_choice) page.add_child(_attachment_choice)
_coin_available = Label.new() _coin_available_heading = Label.new()
_coin_available.position = Vector2(688, 158) _coin_available_heading.text = "Available"
_coin_available.size = Vector2(280, 28) _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) 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 = LineEdit.new()
_attachment_amount.position = Vector2(746, 190) _attachment_amount.position = Vector2(770, 190)
_attachment_amount.size = Vector2(164, 46) _attachment_amount.size = Vector2(140, 46)
_attachment_amount.placeholder_text = "0" _attachment_amount.placeholder_text = "0"
_attachment_amount.text = "0" _attachment_amount.text = "0"
_attachment_amount.text_changed.connect(_on_attachment_amount_changed) _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.size = Vector2(48, 46)
_amount_plus.pressed.connect(_step_attachment_amount.bind(1)) _amount_plus.pressed.connect(_step_attachment_amount.bind(1))
page.add_child(_amount_plus) page.add_child(_amount_plus)
_attachment_summary = Label.new() _attachment_summary = RichTextLabel.new()
_attachment_summary.position = Vector2(688, 246) _attachment_summary.position = Vector2(688, 246)
_attachment_summary.size = Vector2(280, 78) _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 _attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_attachment_summary) page.add_child(_attachment_summary)
var cancel := Button.new() var cancel := Button.new()
@ -281,9 +310,12 @@ func _build_letter() -> Control:
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP _letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
page.add_child(_letter_text) page.add_child(_letter_text)
_letter_gift = Label.new() _letter_gift = RichTextLabel.new()
_letter_gift.position = Vector2(730, 46) _letter_gift.position = Vector2(730, 46)
_letter_gift.size = Vector2(230, 180) _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 _letter_gift.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_letter_gift) page.add_child(_letter_gift)
_accept = Button.new() _accept = Button.new()
@ -479,13 +511,18 @@ func _refresh_attachment_choices(_index: int) -> void:
_attachment_amount.visible = amount_visible _attachment_amount.visible = amount_visible
_amount_minus.visible = amount_visible _amount_minus.visible = amount_visible
_amount_plus.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: match _attachment_kind.selected:
0: 0:
_attachment_choice.add_item("No attachment") _attachment_choice.add_item("No attachment")
1: 1:
_attachment_choice.add_item("Fish coin") _attachment_choice.add_item("Currency")
_coin_available.text = "Available: %d fish coin" % ( _coin_available.set_amount(
_reservations.get_available_fish_coin() _reservations.get_available_fish_coin()
) )
2: 2:
@ -705,7 +742,9 @@ func _attachment_text(attachment: Dictionary) -> String:
PlayerAssetReservationService.AttachmentType.NONE: PlayerAssetReservationService.AttachmentType.NONE:
return "No gift attached." return "No gift attached."
PlayerAssetReservationService.AttachmentType.FISH_COIN: 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: PlayerAssetReservationService.AttachmentType.FISH:
var fish_catch := _inventory.get_catch_by_id( var fish_catch := _inventory.get_catch_by_id(
StringName(str(attachment.get("catch_id", ""))) StringName(str(attachment.get("catch_id", "")))

View file

@ -14,6 +14,9 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const InventoryNotepadType = preload( const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd" "res://ui/components/inventory_notepad.gd"
) )
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
const FishPoolType = preload("res://fish/fish_pool.gd") const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.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_binding: Label = %BindingDecoration
@onready var _notepad_title: Label = %NotepadTitle @onready var _notepad_title: Label = %NotepadTitle
@onready var _notepad_rule: ColorRect = %NotepadRule @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 _notepad_capacity_value: Label = %NotepadCapacityValue
@onready var _cooler_detail_texture: TextureRect = %CoolerDetailTexture @onready var _cooler_detail_texture: TextureRect = %CoolerDetailTexture
@onready var _cooler_detail_name: Label = %CoolerDetailName @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_weight_unit: Label = %CoolerWeightUnit
@onready var _cooler_offer_row: HBoxContainer = %CoolerOfferRow @onready var _cooler_offer_row: HBoxContainer = %CoolerOfferRow
@onready var _cooler_offer_label: Label = %CoolerOfferLabel @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_summary: Control = %CoolerSelectionSummary
@onready var _cooler_selection_empty: Label = %CoolerSelectionEmpty @onready var _cooler_selection_empty: Label = %CoolerSelectionEmpty
@onready var _cooler_selected_count_row: HBoxContainer = %CoolerSelectedCountRow @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_selected_count_label: Label = %CoolerSelectedCountLabel
@onready var _cooler_combined_offer_row: HBoxContainer = %CoolerCombinedOfferRow @onready var _cooler_combined_offer_row: HBoxContainer = %CoolerCombinedOfferRow
@onready var _cooler_combined_offer_label: Label = %CoolerCombinedOfferLabel @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 _favorite_bubble: NotepadInkActionType = %FavoriteBubble
@onready var _sell_bubble: NotepadInkActionType = %SellBubble @onready var _sell_bubble: NotepadInkActionType = %SellBubble
@onready var _sell_all_bubble: NotepadInkActionType = %SellAllBubble @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 _content_stage: Control = %Content
@onready var _cooler_scroll: ScrollContainer = %CoolerScroll @onready var _cooler_scroll: ScrollContainer = %CoolerScroll
@onready var _cooler_host: Control = %CoolerHost @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 _header: Control = %Header
@onready var _separator: Control = %Separator @onready var _separator: Control = %Separator
@onready var _wallet_status: BubbleStatusBubbleType = %WalletStatus @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 _bag_detail_data: Label = %BagDetailData
@onready var _sort_option: OptionButton = %SortOption @onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection @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 _cooler_count: Label = %CoolerCount
@onready var _inventory_empty: Label = %InventoryEmpty @onready var _inventory_empty: Label = %InventoryEmpty
@onready var _selection_summary: Label = %SelectionSummary @onready var _selection_summary: Label = %SelectionSummary
@onready var _favorite_button: Button = %FavoriteButton @onready var _favorite_button: Button = %FavoriteButton
@onready var _sell_button: Button = %SellButton @onready var _sell_button: Button = %SellButton
@onready var _sale_unavailable: Label = %SaleUnavailable @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 _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 _confirm_sale_button: Button = %ConfirmSaleButton
@onready var _cancel_sale_button: Button = %CancelSaleButton @onready var _cancel_sale_button: Button = %CancelSaleButton
@onready var _logbook_empty: Label = %LogbookEmpty @onready var _logbook_empty: Label = %LogbookEmpty
@ -1881,11 +1884,11 @@ func _apply_inventory_styles() -> void:
UtilityPageStyle.panel_style(), UtilityPageStyle.panel_style(),
) )
_confirmation_message.add_theme_font_override( _confirmation_message.add_theme_font_override(
"font", "normal_font",
UtilityPageStyle.TuffyFont, UtilityPageStyle.TuffyFont,
) )
_confirmation_message.add_theme_color_override( _confirmation_message.add_theme_color_override(
"font_color", "default_color",
UtilityPageStyle.OCEAN_TEXT_PRIMARY, UtilityPageStyle.OCEAN_TEXT_PRIMARY,
) )
UtilityPageStyle.apply_ocean_button(_confirm_sale_button) UtilityPageStyle.apply_ocean_button(_confirm_sale_button)
@ -2405,14 +2408,13 @@ func _update_shell_layout() -> void:
_notepad_capacity_value.size = ( _notepad_capacity_value.size = (
Vector2(142.0, 17.0) if compact else Vector2(124.0, 23.0) Vector2(142.0, 17.0) if compact else Vector2(124.0, 23.0)
) )
for status_value: Label in [ _notepad_wallet_value.icon_size = 10.0 if compact else 14.0
_notepad_wallet_value, _notepad_wallet_value.get_amount_label().add_theme_font_size_override(
_notepad_capacity_value, "font_size", 10 if compact else 14
]: )
status_value.add_theme_font_size_override( _notepad_capacity_value.add_theme_font_size_override(
"font_size", "font_size", 10 if compact else 14
10 if compact else 14, )
)
_cooler_detail_texture.position = ( _cooler_detail_texture.position = (
Vector2(5.0, 84.0) if compact else Vector2(80.0, 137.0) 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", "font_size",
13 if compact else 18, 13 if compact else 18,
) )
_cooler_offer_value.add_theme_font_size_override( _cooler_offer_value.icon_size = 10.0 if compact else 15.0
"font_size", _cooler_offer_value.get_amount_label().add_theme_font_size_override(
10 if compact else 15, "font_size", 10 if compact else 15
) )
func _layout_cooler_selection_text(compact: bool) -> void: func _layout_cooler_selection_text(compact: bool) -> void:
var row_height: float = 17.5 if compact else 20.0 var row_height: float = 17.5 if compact else 20.0
for numeric_label: Label in [ _cooler_selected_count_value.add_theme_font_size_override(
_cooler_selected_count_value, "font_size", 10 if compact else 14
_cooler_combined_offer_value, )
]: _cooler_combined_offer_value.icon_size = 10.0 if compact else 14.0
numeric_label.add_theme_font_size_override( _cooler_combined_offer_value.get_amount_label().add_theme_font_size_override(
"font_size", "font_size", 10 if compact else 14
10 if compact else 14, )
)
for word_label: Label in [ for word_label: Label in [
_cooler_selection_empty, _cooler_selection_empty,
_cooler_selected_count_label, _cooler_selected_count_label,
@ -3438,9 +3439,9 @@ func _refresh_economy_summary() -> void:
if _inventory != null if _inventory != null
else 0 else 0
) )
_wallet_balance.text = "wallet: $%d" % balance _wallet_balance.set_amount(balance)
_wallet_status.set_content("wallet", "$%d" % balance) _wallet_status.set_currency_amount("wallet", balance)
_notepad_wallet_value.text = "$%d" % balance _notepad_wallet_value.set_amount(balance)
_capacity_status.set_content("cooler", "%d / %d" % [ _capacity_status.set_content("cooler", "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0, _inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != 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, _inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != 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_status.set_currency_amount("held value", held_total)
_held_value.text = "held fish base value: $%d" % held_total _held_value.set_amount(held_total)
_cooler_count.text = "%d / %d" % [ _cooler_count.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0, _inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != 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_CTRL),
Input.is_key_pressed(KEY_SHIFT) Input.is_key_pressed(KEY_SHIFT)
) )
_transaction_feedback.text = "" _set_transaction_feedback("")
_refresh_inventory() _refresh_inventory()
@ -3719,7 +3720,7 @@ func _on_fish_field_gui_input(event: InputEvent) -> void:
and not get_viewport().gui_is_dragging() and not get_viewport().gui_is_dragging()
): ):
_fish_selection.clear() _fish_selection.clear()
_transaction_feedback.text = "" _set_transaction_feedback("")
_refresh_inventory() _refresh_inventory()
@ -3751,9 +3752,9 @@ func _clear_cooler_detail_stats() -> void:
_cooler_weight_value, _cooler_weight_value,
_cooler_weight_unit, _cooler_weight_unit,
_cooler_offer_label, _cooler_offer_label,
_cooler_offer_value,
]: ]:
label.text = "" label.text = ""
_cooler_offer_value.visible = false
func _set_cooler_selection_summary_empty() -> void: 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_value,
_cooler_selected_count_label, _cooler_selected_count_label,
_cooler_combined_offer_label, _cooler_combined_offer_label,
_cooler_combined_offer_value,
]: ]:
label.visible = false label.visible = false
label.text = "" label.text = ""
_cooler_combined_offer_value.visible = false
func _set_cooler_selection_summary( func _set_cooler_selection_summary(
@ -3785,10 +3786,9 @@ func _set_cooler_selection_summary(
_cooler_selected_count_label.text = "fish selected" _cooler_selected_count_label.text = "fish selected"
if offer_value >= 0: if offer_value >= 0:
_cooler_combined_offer_label.text = "combined offer" _cooler_combined_offer_label.text = "combined offer"
_cooler_combined_offer_value.text = "$%d" % offer_value _cooler_combined_offer_value.set_amount(offer_value)
else: else:
_cooler_combined_offer_label.text = "offer unavailable" _cooler_combined_offer_label.text = "offer unavailable"
_cooler_combined_offer_value.text = ""
func _update_inventory_detail(fish_catch: FishCatchType) -> void: 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: if buyer_offer >= 0 and active_buyer != null:
_cooler_offer_label.text = _get_offer_label(active_buyer) _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: else:
_cooler_offer_label.text = "buyer unavailable" _cooler_offer_label.text = "buyer unavailable"
_cooler_offer_value.text = "" _cooler_offer_value.visible = false
_favorite_button.disabled = false _favorite_button.disabled = false
_favorite_button.text = ( _favorite_button.text = (
"unfavorite" if fish_catch.is_favorited else "favorite" "unfavorite" if fish_catch.is_favorited else "favorite"
@ -3970,12 +3971,8 @@ func _update_sale_summary() -> void:
or preview.status == FishSaleResultType.Status.FAVORITED 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) _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: else:
_set_cooler_selection_summary(selected_count, -1) _set_cooler_selection_summary(selected_count, -1)
_offer_status.set_content(offer_label, "unavailable") _offer_status.set_content(offer_label, "unavailable")
@ -4058,7 +4055,7 @@ func _on_favorite_pressed() -> void:
fish_catch.catch_id, fish_catch.catch_id,
not fish_catch.is_favorited not fish_catch.is_favorited
): ):
_transaction_feedback.text = ( _set_transaction_feedback(
"%s %s." "%s %s."
% [ % [
fish_catch.fish.display_name, fish_catch.fish.display_name,
@ -4074,7 +4071,7 @@ func _on_sell_pressed() -> void:
func _on_sell_all_pressed() -> void: func _on_sell_all_pressed() -> void:
var catch_ids: Array[StringName] = _get_sell_all_catch_ids() var catch_ids: Array[StringName] = _get_sell_all_catch_ids()
if catch_ids.is_empty(): if catch_ids.is_empty():
_transaction_feedback.text = "No sellable fish in the cooler." _set_transaction_feedback("No sellable fish in the cooler.")
return return
_begin_sale_confirmation(catch_ids) _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() and _network_sale_service.is_local_sale_pending()
) )
): ):
_transaction_feedback.text = "Selling…" _set_transaction_feedback("Selling…")
return return
if ( if (
_network_sale_service == null _network_sale_service == null
or buyer_id.is_empty() or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id) or not _network_sale_service.can_request_sale(buyer_id)
): ):
_transaction_feedback.text = ( _set_transaction_feedback(
"Selling is not supported by this server." "Selling is not supported by this server."
) )
return return
@ -4114,7 +4111,7 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
active_buyer active_buyer
) )
if not preview.is_success(): if not preview.is_success():
_transaction_feedback.text = ( _set_transaction_feedback(
"favorited fish cannot be sold. " "favorited fish cannot be sold. "
+ "remove them from the selection first." + "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED if preview.status == FishSaleResultType.Status.FAVORITED
@ -4128,15 +4125,15 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
_confirmation_generation = _menu_generation _confirmation_generation = _menu_generation
var fish_label: String = "fish" var fish_label: String = "fish"
_confirmation_message.text = ( _confirmation_message.text = (
"sell %d %s to the %s for $%d?\ncombined base value: $%d" "[center]sell %d %s to the %s for %s?\n"
% [ + "combined base value: %s[/center]"
preview.fish_count, ) % [
fish_label, preview.fish_count,
_get_buyer_display_group(active_buyer), fish_label,
preview.payout, _get_buyer_display_group(active_buyer),
preview.base_value, CurrencyPresentationType.bbcode_amount(preview.payout, 22),
] CurrencyPresentationType.bbcode_amount(preview.base_value, 22),
) ]
_sale_confirmation.visible = true _sale_confirmation.visible = true
if _shop_cooler_context_active: if _shop_cooler_context_active:
shop_cooler_modal_changed.emit(true) shop_cooler_modal_changed.emit(true)
@ -4182,7 +4179,7 @@ func _on_confirm_sale_pressed() -> void:
and transaction_generation == _menu_generation and transaction_generation == _menu_generation
and (visible or _shop_cooler_context_active) and (visible or _shop_cooler_context_active)
): ):
_transaction_feedback.text = "Selling…" _set_transaction_feedback("Selling…")
func _can_use_shared_world_actions() -> bool: 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: func _on_network_sale_pending(_request_id: String) -> void:
_sale_in_progress = true _sale_in_progress = true
if visible or _shop_cooler_context_active: if visible or _shop_cooler_context_active:
_transaction_feedback.text = "Selling…" _set_transaction_feedback("Selling…")
_update_sale_summary() _update_sale_summary()
@ -4213,11 +4210,12 @@ func _on_network_sale_finished(
return return
_refresh_all() _refresh_all()
var feedback_message: String = ( var feedback_message: String = (
"Sale complete. $%d received." % payout "Sale complete • %s received."
% CurrencyPresentationType.bbcode_amount(payout, 20)
if accepted if accepted
else message else message
) )
_transaction_feedback.text = feedback_message _set_transaction_feedback(feedback_message)
if not accepted: if not accepted:
_sale_unavailable.text = feedback_message _sale_unavailable.text = feedback_message
_sale_unavailable.visible = true _sale_unavailable.visible = true
@ -4273,7 +4271,7 @@ func _revalidate_confirmation() -> void:
or _confirmation_buyer.id != _confirmation_buyer_id or _confirmation_buyer.id != _confirmation_buyer_id
): ):
_close_sale_confirmation() _close_sale_confirmation()
_transaction_feedback.text = "sale selection is no longer available." _set_transaction_feedback("sale selection is no longer available.")
return return
var preview: FishSaleResultType = _sale_service.preview_batch( var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids, _confirmation_catch_ids,
@ -4288,7 +4286,7 @@ func _revalidate_confirmation() -> void:
) )
): ):
_close_sale_confirmation() _close_sale_confirmation()
_transaction_feedback.text = ( _set_transaction_feedback(
"favorited fish cannot be sold. " "favorited fish cannot be sold. "
+ "remove them from the selection first." + "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED if preview.status == FishSaleResultType.Status.FAVORITED
@ -4308,6 +4306,10 @@ func _get_buyer_display_group(
return "buyer" return "buyer"
func _set_transaction_feedback(message: String) -> void:
_transaction_feedback.text = "[center]%s[/center]" % message
func _get_active_sale_buyer() -> FishBuyerProfileType: func _get_active_sale_buyer() -> FishBuyerProfileType:
if _sale_buyer_override != null and _sale_buyer_override.is_valid(): if _sale_buyer_override != null and _sale_buyer_override.is_valid():
return _sale_buyer_override 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="Script" path="res://ui/player_menu.gd" id="1_menu"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [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/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/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="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"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
@ -224,7 +225,7 @@ theme_override_font_sizes/font_size = 19
text = "cooler notes" text = "cooler notes"
horizontal_alignment = 1 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 unique_name_in_owner = true
layout_mode = 0 layout_mode = 0
offset_left = 18.0 offset_left = 18.0
@ -232,9 +233,7 @@ offset_top = 61.0
offset_right = 138.0 offset_right = 138.0
offset_bottom = 84.0 offset_bottom = 84.0
mouse_filter = 2 mouse_filter = 2
theme_override_colors/font_color = Color(0.16, 0.16, 0.14, 1) icon_size = 14.0
theme_override_font_sizes/font_size = 14
text = "$0"
[node name="NotepadCapacityValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"] [node name="NotepadCapacityValue" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"]
unique_name_in_owner = true 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_colors/font_color = Color(0.16, 0.16, 0.14, 1)
theme_override_font_sizes/font_size = 18 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
theme_override_colors/font_color = Color(0.16, 0.16, 0.14, 1) icon_size = 15.0
theme_override_font_sizes/font_size = 15
[node name="CoolerSelectionSummary" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"] [node name="CoolerSelectionSummary" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation"]
unique_name_in_owner = true 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 theme_override_font_sizes/font_size = 18
text = "combined offer" 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
theme_override_colors/font_color = Color(0.2, 0.18, 0.15, 0.86) icon_size = 14.0
theme_override_font_sizes/font_size = 14
[node name="FavoriteBubble" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation" instance=ExtResource("12_ink_action")] [node name="FavoriteBubble" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/CoolerPage/DetailConstellation" instance=ExtResource("12_ink_action")]
unique_name_in_owner = true unique_name_in_owner = true
@ -965,24 +962,31 @@ unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 8 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 unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "wallet: $0" alignment = 0
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
[node name="Separator" type="HSeparator" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"] [node name="Separator" type="HSeparator" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 30)
layout_mode = 2 layout_mode = 2
text = "" text = ""
horizontal_alignment = 1 bbcode_enabled = true
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) fit_content = true
theme_override_font_sizes/font_size = 21 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"] [node name="Content" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout"]
unique_name_in_owner = true unique_name_in_owner = true
@ -1057,12 +1061,16 @@ layout_mode = 2
text = "0 / 12" text = "0 / 12"
theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) 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 unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 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"] [node name="InventoryBody" type="BoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/MenuPanel/Margin/Layout/Content/InventorySection"]
unique_name_in_owner = true unique_name_in_owner = true
@ -1366,12 +1374,15 @@ layout_mode = 2
theme_override_constants/separation = 12 theme_override_constants/separation = 12
alignment = 1 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 64)
layout_mode = 2 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 autowrap_mode = 2
horizontal_alignment = 1
[node name="ConfirmationButtons" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"] [node name="ConfirmationButtons" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"]
layout_mode = 2 layout_mode = 2

View file

@ -36,6 +36,8 @@ func _ready() -> void:
UtilityPageStyleType.apply_ocean_button(button) UtilityPageStyleType.apply_ocean_button(button)
button.custom_minimum_size = Vector2(48, 44) button.custom_minimum_size = Vector2(48, 44)
_make_button_round(button, 22) _make_button_round(button, 22)
_enlarge_action_icon(_eraser_button)
_enlarge_action_icon(_undo_button)
_mode_button.custom_minimum_size = Vector2(48, 48) _mode_button.custom_minimum_size = Vector2(48, 48)
_make_button_round(_mode_button, 24) _make_button_round(_mode_button, 24)
_mode_button.pressed.connect(_toggle_mode) _mode_button.pressed.connect(_toggle_mode)
@ -91,6 +93,25 @@ func _make_button_round(button: Button, radius: int) -> void:
button.add_theme_stylebox_override(style_name, style) 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( func setup(
service: NetworkSurfaceDrawingService, service: NetworkSurfaceDrawingService,
unlocks: PlayerArtUnlocks, unlocks: PlayerArtUnlocks,

View file

@ -1,10 +1,14 @@
[gd_scene load_steps=6 format=3] [gd_scene load_steps=8 format=3]
[ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"] [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="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/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/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://ui/icons/pictograms/x_light.png" id="5_x_light"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_eraser.png" id="6_art_kit_eraser"]
[sub_resource type="ButtonGroup" id="GuideActionGroup"]
allow_unpress = true
[node name="SurfaceDrawingToolbar" type="Control"] [node name="SurfaceDrawingToolbar" type="Control"]
visible = false visible = false
@ -51,9 +55,12 @@ tooltip_text = "grid size"
[node name="EraserButton" type="Button" parent="TopPanel/Top"] [node name="EraserButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
texture_filter = 1
tooltip_text = "toggle eraser" tooltip_text = "toggle eraser"
toggle_mode = true toggle_mode = true
text = "⌫" accessibility_name = "toggle eraser"
icon = ExtResource("6_art_kit_eraser")
expand_icon = true
[node name="UndoButton" type="Button" parent="TopPanel/Top"] [node name="UndoButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true unique_name_in_owner = true
@ -67,24 +74,27 @@ expand_icon = true
[node name="HideGuideButton" type="Button" parent="TopPanel/Top"] [node name="HideGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
tooltip_text = "hide grid on next click" tooltip_text = "toggle hide grid mode"
toggle_mode = true toggle_mode = true
button_group = SubResource("GuideActionGroup")
text = "◌" text = "◌"
[node name="RestoreGuideButton" type="Button" parent="TopPanel/Top"] [node name="RestoreGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
tooltip_text = "restore hidden grid on next click" tooltip_text = "toggle show grid mode"
toggle_mode = true toggle_mode = true
button_group = SubResource("GuideActionGroup")
text = "▤" text = "▤"
[node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"] [node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
texture_filter = 1 texture_filter = 1
tooltip_text = "finish grid on next click" tooltip_text = "toggle finalize grid mode"
toggle_mode = true toggle_mode = true
accessibility_name = "finish grid on next click" button_group = SubResource("GuideActionGroup")
accessibility_name = "toggle finalize grid mode"
icon = ExtResource("4_check_mark_light") icon = ExtResource("4_check_mark_light")
expand_icon = true expand_icon = true

View file

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

View file

@ -20,6 +20,9 @@ const TitleCreditsPageType = preload("res://ui/title_credits_page.gd")
const NetworkSessionType = preload("res://network/network_session.gd") const NetworkSessionType = preload("res://network/network_session.gd")
const SavedServerStoreType = preload("res://network/saved_server_store.gd") const SavedServerStoreType = preload("res://network/saved_server_store.gd")
const JoinGamePageType = preload("res://ui/network/join_game_page.gd") const JoinGamePageType = preload("res://ui/network/join_game_page.gd")
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [
preload("res://fish/species/bass/fish_bass_striped.png"), preload("res://fish/species/bass/fish_bass_striped.png"),
preload("res://fish/species/bluegill/fish_bluegill.png"), preload("res://fish/species/bluegill/fish_bluegill.png"),
@ -106,7 +109,7 @@ enum ConfirmationAction {
@onready var _join_game_button: BubbleButtonType = %JoinGameButton @onready var _join_game_button: BubbleButtonType = %JoinGameButton
@onready var _new_game_label: Label = %NewGameLabel @onready var _new_game_label: Label = %NewGameLabel
@onready var _delete_save_label: Label = %DeleteSaveLabel @onready var _delete_save_label: Label = %DeleteSaveLabel
@onready var _feedback_label: Label = %FeedbackLabel @onready var _feedback_label: RichTextLabel = %FeedbackLabel
@onready var _confirmation_page: TitleConfirmationBubblePageType = ( @onready var _confirmation_page: TitleConfirmationBubblePageType = (
%ConfirmationPage %ConfirmationPage
) )
@ -287,7 +290,7 @@ func open_join_game_page(endpoint: String = "") -> void:
func report_network_error(message: String) -> void: func report_network_error(message: String) -> void:
_join_game_page.set_status(message) _join_game_page.set_status(message)
if not _join_game_page.visible: if not _join_game_page.visible:
_feedback_label.text = message _feedback_label.text = _center_feedback_text(message)
_feedback_label.show() _feedback_label.show()
_feedback_label.modulate.a = 1.0 _feedback_label.modulate.a = 1.0
@ -953,16 +956,23 @@ func _get_continue_stats_text() -> String:
or _inspection.status != SaveInspectionType.Status.VALID_SUPPORTED or _inspection.status != SaveInspectionType.Status.VALID_SUPPORTED
): ):
return "" return ""
return ( return _center_feedback_text(
"%d fish • $%d%d discovered" "%d fish • %s%d discovered" % [
% [
_inspection.catch_count, _inspection.catch_count,
_inspection.wallet_balance, CurrencyPresentationType.bbcode_amount(
_inspection.wallet_balance, 18
),
_inspection.discovered_species_count, _inspection.discovered_species_count,
] ]
) )
func _center_feedback_text(message: String) -> String:
if message.begins_with("[center]"):
return message
return "[center]%s[/center]" % message
func _on_continue_pressed() -> void: func _on_continue_pressed() -> void:
if ( if (
_action_in_progress _action_in_progress
@ -991,13 +1001,15 @@ func _on_new_game_pressed() -> void:
new_game_requested.emit() new_game_requested.emit()
return return
if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION: if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION:
_feedback_label.text = ( _feedback_label.text = _center_feedback_text(
"this save is from a newer game version. " "this save is from a newer game version. "
+ "use delete save before starting over." + "use delete save before starting over."
) )
return return
if _inspection.status == SaveInspectionType.Status.IO_ERROR: if _inspection.status == SaveInspectionType.Status.IO_ERROR:
_feedback_label.text = "the existing save cannot be accessed safely." _feedback_label.text = _center_feedback_text(
"the existing save cannot be accessed safely."
)
return return
_open_confirmation( _open_confirmation(
ConfirmationAction.NEW_GAME, ConfirmationAction.NEW_GAME,
@ -1043,14 +1055,16 @@ func _on_confirmation_accepted() -> void:
_action_in_progress = false _action_in_progress = false
return return
if not _save_manager.delete_progression_save(): if not _save_manager.delete_progression_save():
_feedback_label.text = "failed to delete saved progression." _feedback_label.text = _center_feedback_text(
"failed to delete saved progression."
)
_action_in_progress = false _action_in_progress = false
_refresh_save_inspection() _refresh_save_inspection()
_begin_confirmation_return() _begin_confirmation_return()
return return
_save_manager.initialize_new_game() _save_manager.initialize_new_game()
_refresh_save_inspection() _refresh_save_inspection()
_feedback_label.text = "saved progression deleted." _feedback_label.text = _center_feedback_text("saved progression deleted.")
_begin_confirmation_return() _begin_confirmation_return()
_action_in_progress = false _action_in_progress = false
@ -1325,8 +1339,9 @@ func _begin_title_cluster_return(feedback_text: String) -> void:
_title_settings_transition_generation += 1 _title_settings_transition_generation += 1
_cancel_title_settings_tween() _cancel_title_settings_tween()
_title_settings_transition_active = true _title_settings_transition_active = true
if _feedback_label.text != feedback_text: var centered_feedback: String = _center_feedback_text(feedback_text)
_feedback_label.text = feedback_text if _feedback_label.text != centered_feedback:
_feedback_label.text = centered_feedback
_feedback_label.visible = _feedback_visible_before_settings _feedback_label.visible = _feedback_visible_before_settings
_hide_continue_stats_context() _hide_continue_stats_context()
_set_title_bubbles_interactive(false) _set_title_bubbles_interactive(false)
@ -1434,7 +1449,7 @@ func _refresh_save_inspection() -> void:
_delete_save_label.modulate.a = ( _delete_save_label.modulate.a = (
DISABLED_BUBBLE_LABEL_ALPHA if _delete_button.disabled else 1.0 DISABLED_BUBBLE_LABEL_ALPHA if _delete_button.disabled else 1.0
) )
_feedback_label.text = _inspection.message _feedback_label.text = _center_feedback_text(_inspection.message)
if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED:
_feedback_label.text = _get_continue_stats_text() _feedback_label.text = _get_continue_stats_text()
if _continue_button.disabled: if _continue_button.disabled:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=18 format=3] [gd_scene load_steps=19 format=3]
[ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
@ -22,6 +22,17 @@ shader = ExtResource("4_water_shader")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_logo"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_logo"]
shader = ExtResource("6_logo_shader") shader = ExtResource("6_logo_shader")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_continue_stats"]
content_margin_left = 14.0
content_margin_top = 6.0
content_margin_right = 14.0
content_margin_bottom = 6.0
bg_color = Color(0.031, 0.122, 0.169, 1)
corner_radius_top_left = 14
corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[node name="TitleScreen" type="Control"] [node name="TitleScreen" type="Control"]
unique_name_in_owner = true unique_name_in_owner = true
z_index = 200 z_index = 200
@ -433,15 +444,18 @@ motion_phase = 4.8
deformation_amplitude = 0.02 deformation_amplitude = 0.02
deformation_period = 4.8 deformation_period = 4.8
[node name="FeedbackLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] [node name="FeedbackLabel" type="RichTextLabel" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(430, 44)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 4
text = "" text = ""
horizontal_alignment = 1 bbcode_enabled = true
vertical_alignment = 1 scroll_active = false
autowrap_mode = 2 autowrap_mode = 2
vertical_alignment = 1
theme_override_styles/normal = SubResource("StyleBoxFlat_continue_stats")
[node name="ConfirmationPage" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("10_confirmation_page")] [node name="ConfirmationPage" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("10_confirmation_page")]
unique_name_in_owner = true unique_name_in_owner = true

View file

@ -4,6 +4,12 @@
[sub_resource type="ShaderMaterial" id="ShaderMaterial_netfishing"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_netfishing"]
shader = ExtResource("1_sky_shader") shader = ExtResource("1_sky_shader")
shader_parameter/cloud_coverage = 0.0
shader_parameter/cloud_opacity = 0.0
shader_parameter/cloud_light_color = Color(0.7, 0.76, 0.77, 1)
shader_parameter/cloud_shadow_color = Color(0.31, 0.38, 0.4, 1)
shader_parameter/cloud_wind = Vector2(0.01, -0.006)
shader_parameter/cloud_scale = 0.72
shader_parameter/sky_top_color = Color(0.204, 0.498, 0.643, 1) shader_parameter/sky_top_color = Color(0.204, 0.498, 0.643, 1)
shader_parameter/sky_horizon_color = Color(0.663, 0.843, 0.847, 1) shader_parameter/sky_horizon_color = Color(0.663, 0.843, 0.847, 1)
shader_parameter/ground_bottom_color = Color(0.157, 0.361, 0.439, 1) shader_parameter/ground_bottom_color = Color(0.157, 0.361, 0.439, 1)

File diff suppressed because one or more lines are too long

View file

@ -49,9 +49,6 @@ uniform float shoreline_base_fill : hint_range(0.0, 1.0, 0.01) = 0.32;
uniform float shoreline_opacity_boost : hint_range(0.0, 0.5, 0.01) = 0.26; uniform float shoreline_opacity_boost : hint_range(0.0, 0.5, 0.01) = 0.26;
uniform float surface_highlight_night_floor : hint_range(0.0, 1.0, 0.01) = 0.72; uniform float surface_highlight_night_floor : hint_range(0.0, 1.0, 0.01) = 0.72;
uniform float shoreline_minimum_reach : hint_range(0.1, 1.0, 0.01) = 0.48; uniform float shoreline_minimum_reach : hint_range(0.1, 1.0, 0.01) = 0.48;
uniform float shoreline_crest_width : hint_range(0.02, 0.5, 0.01) = 0.13;
uniform float shoreline_trail_spacing : hint_range(0.05, 0.8, 0.01) = 0.28;
uniform float shoreline_trail_strength : hint_range(0.0, 1.0, 0.01) = 0.55;
uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0; uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0;
uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0; uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0;
@ -486,25 +483,6 @@ void fragment() {
water_depth water_depth
); );
shoreline_mask *= shoreline_depth_visibility; shoreline_mask *= shoreline_depth_visibility;
float shoreline_lead_depth = animated_shoreline_depth * 0.82;
float shoreline_lead_crest = 1.0 - smoothstep(
shoreline_crest_width * 0.25,
shoreline_crest_width,
abs(contour_depth - shoreline_lead_depth)
);
float shoreline_trail_depth = max(
shoreline_lead_depth - shoreline_trail_spacing,
shoreline_crest_width
);
float shoreline_trail_crest = 1.0 - smoothstep(
shoreline_crest_width * 0.40,
shoreline_crest_width * 1.45,
abs(contour_depth - shoreline_trail_depth)
);
shoreline_lead_crest *= shoreline_depth_visibility;
shoreline_trail_crest *= (
shoreline_depth_visibility * shoreline_trail_strength
);
float shoreline_marks = max( float shoreline_marks = max(
blocky_mark_layer( blocky_mark_layer(
world_position.xz * shoreline_mark_density world_position.xz * shoreline_mark_density
@ -524,17 +502,14 @@ void fragment() {
1.0, 1.0,
shoreline_marks shoreline_marks
); );
float moving_crests = max(
shoreline_lead_crest,
shoreline_trail_crest
) * mix(0.68, 1.0, shoreline_marks);
shoreline_foam = max(shoreline_foam, moving_crests);
shoreline_foam *= shoreline_proximity; shoreline_foam *= shoreline_proximity;
water_alpha = min( water_alpha = min(
water_alpha + shoreline_foam * shoreline_opacity_boost, water_alpha + shoreline_foam * shoreline_opacity_boost,
0.98 0.98
); );
water_alpha *= contact_stability; // Preserve most of the water layer at exact terrain contact. Fully fading
// it exposed the sand texture as a broken tan seam along the water plane.
water_alpha *= mix(0.82, 1.0, contact_stability);
float surface_mottle = value_noise( float surface_mottle = value_noise(
world_position.xz * surface_mottle_scale world_position.xz * surface_mottle_scale

View file

@ -47,9 +47,6 @@ shader_parameter/shoreline_base_fill = 0.58
shader_parameter/shoreline_opacity_boost = 0.38 shader_parameter/shoreline_opacity_boost = 0.38
shader_parameter/surface_highlight_night_floor = 0.72 shader_parameter/surface_highlight_night_floor = 0.72
shader_parameter/shoreline_minimum_reach = 0.22 shader_parameter/shoreline_minimum_reach = 0.22
shader_parameter/shoreline_crest_width = 0.08
shader_parameter/shoreline_trail_spacing = 0.13
shader_parameter/shoreline_trail_strength = 0.55
shader_parameter/distant_alpha_start = 260.0 shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0 shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1) shader_parameter/environment_tint = Color(1, 1, 1, 1)

View file

@ -47,9 +47,6 @@ shader_parameter/shoreline_base_fill = 0.58
shader_parameter/shoreline_opacity_boost = 0.38 shader_parameter/shoreline_opacity_boost = 0.38
shader_parameter/surface_highlight_night_floor = 0.72 shader_parameter/surface_highlight_night_floor = 0.72
shader_parameter/shoreline_minimum_reach = 0.22 shader_parameter/shoreline_minimum_reach = 0.22
shader_parameter/shoreline_crest_width = 0.08
shader_parameter/shoreline_trail_spacing = 0.13
shader_parameter/shoreline_trail_strength = 0.55
shader_parameter/distant_alpha_start = 260.0 shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0 shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1) shader_parameter/environment_tint = Color(1, 1, 1, 1)

File diff suppressed because one or more lines are too long

View file

@ -6,13 +6,13 @@
[ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="4_environment"] [ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="4_environment"]
[sub_resource type="BoxShape3D" id="NorthSouthBoundsShape"] [sub_resource type="BoxShape3D" id="NorthSouthBoundsShape"]
size = Vector3(120, 14, 2) size = Vector3(168.8772, 14, 2)
[sub_resource type="BoxShape3D" id="EastWestBoundsShape"] [sub_resource type="BoxShape3D" id="EastWestBoundsShape"]
size = Vector3(2, 14, 90) size = Vector3(2, 14, 126.45079)
[sub_resource type="BoxShape3D" id="FailsafeShape"] [sub_resource type="BoxShape3D" id="FailsafeShape"]
size = Vector3(118, 4, 88) size = Vector3(166.8772, 4, 124.45079)
[node name="TestWorld" type="Node3D"] [node name="TestWorld" type="Node3D"]
script = ExtResource("1_world") script = ExtResource("1_world")
@ -49,25 +49,25 @@ shape = SubResource("FailsafeShape")
[node name="WorldBounds" type="Node3D" parent="."] [node name="WorldBounds" type="Node3D" parent="."]
[node name="North" type="StaticBody3D" parent="WorldBounds"] [node name="North" type="StaticBody3D" parent="WorldBounds"]
position = Vector3(12, 5, 45) position = Vector3(12, 5, 63.225395)
[node name="Shape" type="CollisionShape3D" parent="WorldBounds/North"] [node name="Shape" type="CollisionShape3D" parent="WorldBounds/North"]
shape = SubResource("NorthSouthBoundsShape") shape = SubResource("NorthSouthBoundsShape")
[node name="South" type="StaticBody3D" parent="WorldBounds"] [node name="South" type="StaticBody3D" parent="WorldBounds"]
position = Vector3(12, 5, -45) position = Vector3(12, 5, -63.225395)
[node name="Shape" type="CollisionShape3D" parent="WorldBounds/South"] [node name="Shape" type="CollisionShape3D" parent="WorldBounds/South"]
shape = SubResource("NorthSouthBoundsShape") shape = SubResource("NorthSouthBoundsShape")
[node name="West" type="StaticBody3D" parent="WorldBounds"] [node name="West" type="StaticBody3D" parent="WorldBounds"]
position = Vector3(-48, 5, 0) position = Vector3(-72.4386, 5, 0)
[node name="Shape" type="CollisionShape3D" parent="WorldBounds/West"] [node name="Shape" type="CollisionShape3D" parent="WorldBounds/West"]
shape = SubResource("EastWestBoundsShape") shape = SubResource("EastWestBoundsShape")
[node name="East" type="StaticBody3D" parent="WorldBounds"] [node name="East" type="StaticBody3D" parent="WorldBounds"]
position = Vector3(72, 5, 0) position = Vector3(96.4386, 5, 0)
[node name="Shape" type="CollisionShape3D" parent="WorldBounds/East"] [node name="Shape" type="CollisionShape3D" parent="WorldBounds/East"]
shape = SubResource("EastWestBoundsShape") shape = SubResource("EastWestBoundsShape")

View file

@ -51,7 +51,8 @@ func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary:
configuration.water_is_inside, configuration.water_is_inside,
configuration.simplification_tolerance, configuration.simplification_tolerance,
configuration.smoothing_iterations, configuration.smoothing_iterations,
configuration.resample_spacing configuration.resample_spacing,
configuration.corner_rounding_distance,
) )
var output_directory := configuration.output_resource_path.get_base_dir() var output_directory := configuration.output_resource_path.get_base_dir()
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory)) DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(output_directory))

View file

@ -12,3 +12,4 @@ extends Resource
@export var simplification_tolerance := -1.0 @export var simplification_tolerance := -1.0
@export_range(-1, 4, 1) var smoothing_iterations := -1 @export_range(-1, 4, 1) var smoothing_iterations := -1
@export var resample_spacing := -1.0 @export var resample_spacing := -1.0
@export var corner_rounding_distance := -1.0

View file

@ -5,9 +5,14 @@ const WATER_PLANE_EPSILON := 0.001
const ENDPOINT_MERGE_TOLERANCE := 0.03 const ENDPOINT_MERGE_TOLERANCE := 0.03
const LOOP_CLOSURE_TOLERANCE := 0.06 const LOOP_CLOSURE_TOLERANCE := 0.06
const MINIMUM_FRAGMENT_LENGTH := 2.0 const MINIMUM_FRAGMENT_LENGTH := 2.0
const SIMPLIFICATION_TOLERANCE := 0.025 const SIMPLIFICATION_TOLERANCE := 0.55
const SMOOTHING_ITERATIONS := 0 const SMOOTHING_ITERATIONS := 3
const RESAMPLE_SPACING := 0.10 const RESAMPLE_SPACING := 0.16
const CORNER_ROUNDING_DISTANCE := 2.4
const CORNER_ROUNDING_START_DEGREES := 24.0
const CORNER_ROUNDING_FULL_DEGREES := 78.0
const MAXIMUM_JOIN_SCALE := 1.0
const RIBBON_REACH_GROWTH_PER_METER := 0.52
const RIBBON_WIDTH := 1.35 const RIBBON_WIDTH := 1.35
const LAND_INSET := 0.12 const LAND_INSET := 0.12
const SURFACE_OFFSET := 0.018 const SURFACE_OFFSET := 0.018
@ -21,7 +26,8 @@ static func generate(
water_is_inside: bool, water_is_inside: bool,
simplification_override := -1.0, simplification_override := -1.0,
smoothing_iterations_override := -1, smoothing_iterations_override := -1,
resample_spacing_override := -1.0 resample_spacing_override := -1.0,
corner_rounding_override := -1.0
) -> Dictionary: ) -> Dictionary:
var simplification := ( var simplification := (
SIMPLIFICATION_TOLERANCE SIMPLIFICATION_TOLERANCE
@ -38,6 +44,11 @@ static func generate(
if resample_spacing_override < 0.0 if resample_spacing_override < 0.0
else resample_spacing_override else resample_spacing_override
) )
var corner_rounding := (
CORNER_ROUNDING_DISTANCE
if corner_rounding_override < 0.0
else corner_rounding_override
)
var segments := _extract_segments(faces, water_height, bounds) var segments := _extract_segments(faces, water_height, bounds)
var raw_paths := _stitch_segments(segments) var raw_paths := _stitch_segments(segments)
var processed_paths: Array[Dictionary] = [] var processed_paths: Array[Dictionary] = []
@ -52,7 +63,12 @@ static func generate(
continue continue
var simplified := _simplify(raw_points, closed, simplification) var simplified := _simplify(raw_points, closed, simplification)
simplified_paths.append({"points": simplified, "closed": closed}) simplified_paths.append({"points": simplified, "closed": closed})
var smoothed := _chaikin(simplified, closed, smoothing_iterations) var rounded := _round_sharp_corners(
simplified,
closed,
corner_rounding,
)
var smoothed := _chaikin(rounded, closed, smoothing_iterations)
var resampled := _resample(smoothed, closed, resample_spacing) var resampled := _resample(smoothed, closed, resample_spacing)
if resampled.size() < (3 if closed else 2): if resampled.size() < (3 if closed else 2):
continue continue
@ -296,6 +312,65 @@ static func _chaikin(
return result return result
static func _round_sharp_corners(
points: PackedVector2Array,
closed: bool,
rounding_distance: float,
) -> PackedVector2Array:
if rounding_distance <= WATER_PLANE_EPSILON or points.size() < 3:
return points
var result := PackedVector2Array()
for index: int in points.size():
if not closed and (index == 0 or index == points.size() - 1):
result.append(points[index])
continue
var previous := points[(index - 1 + points.size()) % points.size()]
var point := points[index]
var following := points[(index + 1) % points.size()]
var incoming_vector := point - previous
var outgoing_vector := following - point
var incoming_length := incoming_vector.length()
var outgoing_length := outgoing_vector.length()
if (
incoming_length <= WATER_PLANE_EPSILON
or outgoing_length <= WATER_PLANE_EPSILON
):
result.append(point)
continue
var incoming := incoming_vector / incoming_length
var outgoing := outgoing_vector / outgoing_length
var turn_degrees := rad_to_deg(
acos(clampf(incoming.dot(outgoing), -1.0, 1.0))
)
var corner_weight := smoothstep(
CORNER_ROUNDING_START_DEGREES,
CORNER_ROUNDING_FULL_DEGREES,
turn_degrees,
)
var cut_distance := minf(
rounding_distance * corner_weight,
minf(incoming_length, outgoing_length) * 0.44,
)
if cut_distance <= WATER_PLANE_EPSILON:
result.append(point)
continue
var entry := point - incoming * cut_distance
var exit := point + outgoing * cut_distance
var curve_steps := maxi(3, ceili(cut_distance / 0.22))
for step: int in curve_steps + 1:
var amount := float(step) / float(curve_steps)
var first := entry.lerp(point, amount)
var second := point.lerp(exit, amount)
var rounded_point := first.lerp(second, amount)
if (
result.is_empty()
or result[result.size() - 1].distance_to(rounded_point)
> WATER_PLANE_EPSILON
):
result.append(rounded_point)
return result
static func _resample( static func _resample(
points: PackedVector2Array, points: PackedVector2Array,
closed: bool, closed: bool,
@ -348,10 +423,10 @@ static func _build_mesh(
else 0.0 else 0.0
) )
var base_index := vertices.size() var base_index := vertices.size()
var path_distance := 0.0 var water_normals := PackedVector2Array()
var join_scales := PackedFloat32Array()
var water_reaches := PackedFloat32Array()
for index: int in points.size(): for index: int in points.size():
if index > 0:
path_distance += points[index - 1].distance_to(points[index])
var previous := points[(index - 1 + points.size()) % points.size()] var previous := points[(index - 1 + points.size()) % points.size()]
var following := points[(index + 1) % points.size()] var following := points[(index + 1) % points.size()]
if not closed: if not closed:
@ -381,14 +456,37 @@ static func _build_mesh(
water_normal = -water_normal water_normal = -water_normal
var join_scale := minf( var join_scale := minf(
1.0 / maxf(absf(water_normal.dot(outgoing_normal)), 0.55), 1.0 / maxf(absf(water_normal.dot(outgoing_normal)), 0.55),
1.65, MAXIMUM_JOIN_SCALE,
) )
var water_reach := _safe_water_reach(
incoming,
outgoing,
water_normal,
previous.distance_to(point),
point.distance_to(following),
(RIBBON_WIDTH - LAND_INSET) * join_scale,
)
water_normals.append(water_normal)
join_scales.append(join_scale)
water_reaches.append(water_reach)
water_reaches = _smooth_water_reaches(
points,
water_reaches,
closed,
)
var path_distance := 0.0
for index: int in points.size():
if index > 0:
path_distance += points[index - 1].distance_to(points[index])
var point := points[index]
var water_normal := water_normals[index]
var join_scale := join_scales[index]
var land_point := ( var land_point := (
point - water_normal * LAND_INSET * join_scale point - water_normal * LAND_INSET * join_scale
) )
var water_point := ( var water_point := (
point point
+ water_normal * (RIBBON_WIDTH - LAND_INSET) * join_scale + water_normal * water_reaches[index]
) )
vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y)) vertices.append(Vector3(land_point.x, water_height + SURFACE_OFFSET, land_point.y))
vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y)) vertices.append(Vector3(water_point.x, water_height + SURFACE_OFFSET, water_point.y))
@ -416,6 +514,58 @@ static func _build_mesh(
return mesh return mesh
static func _safe_water_reach(
incoming: Vector2,
outgoing: Vector2,
water_normal: Vector2,
incoming_length: float,
outgoing_length: float,
desired_reach: float,
) -> float:
var turn_cross := incoming.cross(outgoing)
var water_side := incoming.cross(water_normal)
if turn_cross * water_side <= 0.0:
return desired_reach
var turn_angle := acos(clampf(incoming.dot(outgoing), -1.0, 1.0))
if turn_angle <= WATER_PLANE_EPSILON:
return desired_reach
var radius := minf(incoming_length, outgoing_length) / maxf(
2.0 * sin(turn_angle * 0.5),
WATER_PLANE_EPSILON,
)
return minf(desired_reach, maxf(radius * 0.58, 0.24))
static func _smooth_water_reaches(
points: PackedVector2Array,
reaches: PackedFloat32Array,
closed: bool,
) -> PackedFloat32Array:
var result := reaches.duplicate()
if result.size() < 2:
return result
for _pass: int in 3:
var forward_start := 0 if closed else 1
for index: int in range(forward_start, result.size()):
var previous := (index - 1 + result.size()) % result.size()
var allowed := (
result[previous]
+ points[previous].distance_to(points[index])
* RIBBON_REACH_GROWTH_PER_METER
)
result[index] = minf(result[index], allowed)
var backward_start := result.size() - 1 if closed else result.size() - 2
for index: int in range(backward_start, -1, -1):
var following := (index + 1) % result.size()
var allowed := (
result[following]
+ points[index].distance_to(points[following])
* RIBBON_REACH_GROWTH_PER_METER
)
result[index] = minf(result[index], allowed)
return result
static func _closed_path_water_side( static func _closed_path_water_side(
points: PackedVector2Array, points: PackedVector2Array,
water_is_inside: bool, water_is_inside: bool,