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/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -53,8 +53,7 @@ func get_sale_message(
var buyer_name: String = animal_name_plural
if buyer_name.is_empty():
buyer_name = display_name
return "you sold your %s to the %s for $%d." % [
return "you sold your %s to the %s." % [
fish_name,
buyer_name,
payout,
]

View file

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

View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b20kab6kxh7ap"
path="res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit_marker.png"
dest_files=["res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 388 B

View file

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

Before

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bf3jlmqhmmuge"
path="res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/shop/32_currency.png"
dest_files=["res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

@ -344,7 +344,7 @@ func arm_guide_action(action: int) -> bool:
_selected_canvas_id = ""
_update_aim()
_refresh_stencil_visibility()
_emit_hud_state("click a grid to %s it" % _guide_action_verb(action))
_emit_hud_state(_guide_action_status(action))
return true
@ -1874,8 +1874,10 @@ func _execute_armed_guide_action() -> void:
_restore_selected_guide()
GuideAction.FINALIZE:
_finalize_selected_guide()
_clear_armed_guide_action(true)
_emit_hud_state("")
# Guide actions are persistent modes. Keep the selected action armed so
# multiple grids can be handled without returning to the toolbar after
# every click; selecting the active toggle again explicitly exits it.
_emit_hud_state(_guide_action_status(action))
func _cancel_armed_guide_action() -> void:
@ -1904,12 +1906,18 @@ func _guide_action_verb(action: int) -> String:
GuideAction.HIDE:
return "hide"
GuideAction.RESTORE:
return "restore"
return "show"
GuideAction.FINALIZE:
return "finish"
return "use"
func _guide_action_status(action: int) -> String:
return "%s grid mode • click grids • toggle again to exit" % (
_guide_action_verb(action).capitalize()
)
func _remove_selected_guide() -> void:
if _selected_canvas_id.is_empty():
_emit_hud_state("aim at a placed grid before removing its guide")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -325,6 +325,25 @@ func _run() -> void:
and finalize_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(grid_option.item_count == 4)
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.pressed = true
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.HIDE
)
assert(service.is_placement_mode())
assert(not service.is_eraser_mode())
assert(hide_button.button_pressed)
hide_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode())
assert(service.is_eraser_mode())
assert(not service.is_placement_mode() and service.is_eraser_mode())
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.RESTORE
)
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
assert(not restore_button.button_pressed)
assert(finalize_button.button_pressed)
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
(color_buttons[&"ocean_teal"] as Button).pressed.emit()
assert(not service.is_eraser_mode())
@ -410,6 +441,32 @@ func _run() -> void:
player.hotbar.select_slot(0)
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
await process_frame
var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
var pocket_animation_player := player.get(
"_character_animation_player"
) as AnimationPlayer
var pocket_animation_name: StringName = player.get(
"_pocket_visual_animation"
)
if (
not held_art_kit_display.visible
and pocket_animation_player != null
and not pocket_animation_name.is_empty()
and pocket_animation_player.has_animation(pocket_animation_name)
):
var pocket_animation: Animation = (
pocket_animation_player.get_animation(pocket_animation_name)
)
await create_timer(pocket_animation.length * 0.55).timeout
assert(held_art_kit_display.visible)
assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
assert(is_zero_approx(angle_difference(
held_art_kit_sprite.rotation.z,
-PI * 0.5,
)))
assert(held_art_kit_sprite.flip_h)
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
assert(service.is_active() and toolbar.visible)
assert(service.get_grid_size() == 128)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -36,6 +36,8 @@ func _ready() -> void:
UtilityPageStyleType.apply_ocean_button(button)
button.custom_minimum_size = Vector2(48, 44)
_make_button_round(button, 22)
_enlarge_action_icon(_eraser_button)
_enlarge_action_icon(_undo_button)
_mode_button.custom_minimum_size = Vector2(48, 48)
_make_button_round(_mode_button, 24)
_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)
func _enlarge_action_icon(button: Button) -> void:
# The shared ocean button uses generous text padding, leaving only a
# 20-pixel icon area in these compact bubbles. Keep the 48x44 control and
# hitbox intact while giving icon-only actions a full 40x40 presentation.
button.add_theme_constant_override("icon_max_width", 40)
for style_name: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var existing: StyleBox = button.get_theme_stylebox(style_name)
var style := existing.duplicate() as StyleBoxFlat
if style == null:
continue
style.content_margin_left = 4.0
style.content_margin_right = 4.0
style.content_margin_top = 2.0
style.content_margin_bottom = 2.0
button.add_theme_stylebox_override(style_name, style)
func setup(
service: NetworkSurfaceDrawingService,
unlocks: PlayerArtUnlocks,

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

View file

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

View file

@ -20,6 +20,9 @@ const TitleCreditsPageType = preload("res://ui/title_credits_page.gd")
const NetworkSessionType = preload("res://network/network_session.gd")
const SavedServerStoreType = preload("res://network/saved_server_store.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] = [
preload("res://fish/species/bass/fish_bass_striped.png"),
preload("res://fish/species/bluegill/fish_bluegill.png"),
@ -106,7 +109,7 @@ enum ConfirmationAction {
@onready var _join_game_button: BubbleButtonType = %JoinGameButton
@onready var _new_game_label: Label = %NewGameLabel
@onready var _delete_save_label: Label = %DeleteSaveLabel
@onready var _feedback_label: Label = %FeedbackLabel
@onready var _feedback_label: RichTextLabel = %FeedbackLabel
@onready var _confirmation_page: TitleConfirmationBubblePageType = (
%ConfirmationPage
)
@ -287,7 +290,7 @@ func open_join_game_page(endpoint: String = "") -> void:
func report_network_error(message: String) -> void:
_join_game_page.set_status(message)
if not _join_game_page.visible:
_feedback_label.text = message
_feedback_label.text = _center_feedback_text(message)
_feedback_label.show()
_feedback_label.modulate.a = 1.0
@ -953,16 +956,23 @@ func _get_continue_stats_text() -> String:
or _inspection.status != SaveInspectionType.Status.VALID_SUPPORTED
):
return ""
return (
"%d fish • $%d%d discovered"
% [
return _center_feedback_text(
"%d fish • %s%d discovered" % [
_inspection.catch_count,
_inspection.wallet_balance,
CurrencyPresentationType.bbcode_amount(
_inspection.wallet_balance, 18
),
_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:
if (
_action_in_progress
@ -991,13 +1001,15 @@ func _on_new_game_pressed() -> void:
new_game_requested.emit()
return
if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION:
_feedback_label.text = (
_feedback_label.text = _center_feedback_text(
"this save is from a newer game version. "
+ "use delete save before starting over."
)
return
if _inspection.status == SaveInspectionType.Status.IO_ERROR:
_feedback_label.text = "the existing save cannot be accessed safely."
_feedback_label.text = _center_feedback_text(
"the existing save cannot be accessed safely."
)
return
_open_confirmation(
ConfirmationAction.NEW_GAME,
@ -1043,14 +1055,16 @@ func _on_confirmation_accepted() -> void:
_action_in_progress = false
return
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
_refresh_save_inspection()
_begin_confirmation_return()
return
_save_manager.initialize_new_game()
_refresh_save_inspection()
_feedback_label.text = "saved progression deleted."
_feedback_label.text = _center_feedback_text("saved progression deleted.")
_begin_confirmation_return()
_action_in_progress = false
@ -1325,8 +1339,9 @@ func _begin_title_cluster_return(feedback_text: String) -> void:
_title_settings_transition_generation += 1
_cancel_title_settings_tween()
_title_settings_transition_active = true
if _feedback_label.text != feedback_text:
_feedback_label.text = feedback_text
var centered_feedback: String = _center_feedback_text(feedback_text)
if _feedback_label.text != centered_feedback:
_feedback_label.text = centered_feedback
_feedback_label.visible = _feedback_visible_before_settings
_hide_continue_stats_context()
_set_title_bubbles_interactive(false)
@ -1434,7 +1449,7 @@ func _refresh_save_inspection() -> void:
_delete_save_label.modulate.a = (
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:
_feedback_label.text = _get_continue_stats_text()
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="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"]
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"]
unique_name_in_owner = true
z_index = 200
@ -433,15 +444,18 @@ motion_phase = 4.8
deformation_amplitude = 0.02
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
visible = false
custom_minimum_size = Vector2(0, 44)
custom_minimum_size = Vector2(430, 44)
layout_mode = 2
size_flags_horizontal = 4
text = ""
horizontal_alignment = 1
vertical_alignment = 1
bbcode_enabled = true
scroll_active = false
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")]
unique_name_in_owner = true

View file

@ -4,6 +4,12 @@
[sub_resource type="ShaderMaterial" id="ShaderMaterial_netfishing"]
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_horizon_color = Color(0.663, 0.843, 0.847, 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 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_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_end : hint_range(20.0, 5000.0, 1.0) = 1400.0;
@ -486,25 +483,6 @@ void fragment() {
water_depth
);
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(
blocky_mark_layer(
world_position.xz * shoreline_mark_density
@ -524,17 +502,14 @@ void fragment() {
1.0,
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;
water_alpha = min(
water_alpha + shoreline_foam * shoreline_opacity_boost,
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(
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/surface_highlight_night_floor = 0.72
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_end = 1400.0
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/surface_highlight_night_floor = 0.72
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_end = 1400.0
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"]
[sub_resource type="BoxShape3D" id="NorthSouthBoundsShape"]
size = Vector3(120, 14, 2)
size = Vector3(168.8772, 14, 2)
[sub_resource type="BoxShape3D" id="EastWestBoundsShape"]
size = Vector3(2, 14, 90)
size = Vector3(2, 14, 126.45079)
[sub_resource type="BoxShape3D" id="FailsafeShape"]
size = Vector3(118, 4, 88)
size = Vector3(166.8772, 4, 124.45079)
[node name="TestWorld" type="Node3D"]
script = ExtResource("1_world")
@ -49,25 +49,25 @@ shape = SubResource("FailsafeShape")
[node name="WorldBounds" type="Node3D" parent="."]
[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"]
shape = SubResource("NorthSouthBoundsShape")
[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"]
shape = SubResource("NorthSouthBoundsShape")
[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"]
shape = SubResource("EastWestBoundsShape")
[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"]
shape = SubResource("EastWestBoundsShape")

View file

@ -51,7 +51,8 @@ func _rebuild(configuration: ShorelineRibbonConfig) -> Dictionary:
configuration.water_is_inside,
configuration.simplification_tolerance,
configuration.smoothing_iterations,
configuration.resample_spacing
configuration.resample_spacing,
configuration.corner_rounding_distance,
)
var output_directory := configuration.output_resource_path.get_base_dir()
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_range(-1, 4, 1) var smoothing_iterations := -1
@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 LOOP_CLOSURE_TOLERANCE := 0.06
const MINIMUM_FRAGMENT_LENGTH := 2.0
const SIMPLIFICATION_TOLERANCE := 0.025
const SMOOTHING_ITERATIONS := 0
const RESAMPLE_SPACING := 0.10
const SIMPLIFICATION_TOLERANCE := 0.55
const SMOOTHING_ITERATIONS := 3
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 LAND_INSET := 0.12
const SURFACE_OFFSET := 0.018
@ -21,7 +26,8 @@ static func generate(
water_is_inside: bool,
simplification_override := -1.0,
smoothing_iterations_override := -1,
resample_spacing_override := -1.0
resample_spacing_override := -1.0,
corner_rounding_override := -1.0
) -> Dictionary:
var simplification := (
SIMPLIFICATION_TOLERANCE
@ -38,6 +44,11 @@ static func generate(
if resample_spacing_override < 0.0
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 raw_paths := _stitch_segments(segments)
var processed_paths: Array[Dictionary] = []
@ -52,7 +63,12 @@ static func generate(
continue
var simplified := _simplify(raw_points, closed, simplification)
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)
if resampled.size() < (3 if closed else 2):
continue
@ -296,6 +312,65 @@ static func _chaikin(
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(
points: PackedVector2Array,
closed: bool,
@ -348,10 +423,10 @@ static func _build_mesh(
else 0.0
)
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():
if index > 0:
path_distance += points[index - 1].distance_to(points[index])
var previous := points[(index - 1 + points.size()) % points.size()]
var following := points[(index + 1) % points.size()]
if not closed:
@ -381,14 +456,37 @@ static func _build_mesh(
water_normal = -water_normal
var join_scale := minf(
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 := (
point - water_normal * LAND_INSET * join_scale
)
var water_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(water_point.x, water_height + SURFACE_OFFSET, water_point.y))
@ -416,6 +514,58 @@ static func _build_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(
points: PackedVector2Array,
water_is_inside: bool,