Polish world, shop, and artwork presentation
7
addons/netfishing_shoreline_baker/plugin.cfg
Normal 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"
|
||||
66
addons/netfishing_shoreline_baker/shoreline_auto_baker.gd
Normal 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()
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cbk4xpalei5v3
|
||||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -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
|
||||
BIN
items/icons/art/art_kit_eraser.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -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
|
||||
BIN
items/icons/art/art_kit_marker.png
Normal file
|
After Width: | Height: | Size: 945 B |
40
items/icons/art/art_kit_marker.png.import
Normal 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
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.2 KiB |
|
|
@ -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 |
|
|
@ -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 |
BIN
items/icons/shop/32_currency.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
40
items/icons/shop/32_currency.png.import
Normal 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
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
74
main/texture_sampling_policy.gd
Normal 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
|
||||
)
|
||||
1
main/texture_sampling_policy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dwd4wfn8bwk5i
|
||||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
97
tests/shoreline_ribbon_validation.gd
Normal 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
|
||||
)
|
||||
1
tests/shoreline_ribbon_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c73frygpfhred
|
||||
148
tests/texture_sampling_validation.gd
Normal 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)
|
||||
1
tests/texture_sampling_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dqm0j1xnkeox1
|
||||
15
ui/art_kit_marker_icon.gdshader
Normal 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
|
||||
);
|
||||
}
|
||||
1
ui/art_kit_marker_icon.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bsg3cwrnjq6ev
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
48
ui/components/currency_amount.gd
Normal 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
|
||||
1
ui/components/currency_amount.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b7jx040ujcol
|
||||
29
ui/components/currency_amount.tscn
Normal 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
|
||||
28
ui/currency_presentation.gd
Normal 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,
|
||||
]
|
||||
1
ui/currency_presentation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bcvy1l2jqqbmt
|
||||
|
|
@ -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$%d • owned %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$%d • owned %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),
|
||||
],
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,10 +759,9 @@ 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,
|
||||
|
|
@ -768,7 +770,6 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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", "")))
|
||||
|
|
|
|||
|
|
@ -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,13 +2408,12 @@ 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,21 +2596,20 @@ 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,
|
||||
|
|
@ -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"
|
||||
% [
|
||||
"[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),
|
||||
preview.payout,
|
||||
preview.base_value,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||