Compare commits
4 commits
c239ca6bd5
...
0076996d39
| Author | SHA1 | Date | |
|---|---|---|---|
| 0076996d39 | |||
| c06d5714c0 | |||
| 117fdbfdaa | |||
| 7d38bc962b |
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=""
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -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=""
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -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=""
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 46 KiB |
|
|
@ -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: 2.5 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_grid_finish_light.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
40
items/icons/art/art_kit_grid_finish_light.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dm24dy3ebvqmv"
|
||||
path="res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://items/icons/art/art_kit_grid_finish_light.png"
|
||||
dest_files=["res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
items/icons/art/art_kit_grid_hidden_light.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
40
items/icons/art/art_kit_grid_hidden_light.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cv6yicrysb0vs"
|
||||
path="res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://items/icons/art/art_kit_grid_hidden_light.png"
|
||||
dest_files=["res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
items/icons/art/art_kit_grid_light.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
40
items/icons/art/art_kit_grid_light.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dh2tu3ptqe6f7"
|
||||
path="res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://items/icons/art/art_kit_grid_light.png"
|
||||
dest_files=["res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
items/icons/art/art_kit_grid_restore_light.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
40
items/icons/art/art_kit_grid_restore_light.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ckxdpi5sqerao"
|
||||
path="res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://items/icons/art/art_kit_grid_restore_light.png"
|
||||
dest_files=["res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_player,
|
||||
_surface_drawings_root,
|
||||
_player.bag,
|
||||
_player.hotbar,
|
||||
_player.art_unlocks,
|
||||
)
|
||||
_network_player_list.set_surface_drawing_service(
|
||||
|
|
|
|||
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 = (
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ var _spawn_service: PlayerSpawnService
|
|||
var _relationships: PlayerRelationshipStore
|
||||
var _local_player: Player
|
||||
var _bag: PlayerBag
|
||||
var _hotbar: PlayerHotbar
|
||||
var _art_unlocks: PlayerArtUnlocks
|
||||
var _drawing_root: Node3D
|
||||
var _canvas_states: Dictionary[String, Dictionary] = {}
|
||||
|
|
@ -87,6 +88,7 @@ func setup(
|
|||
local_player: Player,
|
||||
drawing_root: Node3D,
|
||||
bag: PlayerBag,
|
||||
hotbar: PlayerHotbar,
|
||||
art_unlocks: PlayerArtUnlocks,
|
||||
) -> void:
|
||||
_session = session
|
||||
|
|
@ -95,6 +97,7 @@ func setup(
|
|||
_local_player = local_player
|
||||
_drawing_root = drawing_root
|
||||
_bag = bag
|
||||
_hotbar = hotbar
|
||||
_art_unlocks = art_unlocks
|
||||
_refresh_local_unlocks()
|
||||
if _session != null:
|
||||
|
|
@ -123,20 +126,6 @@ func handle_input(
|
|||
can_open: bool,
|
||||
pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION,
|
||||
) -> bool:
|
||||
if event is InputEventKey:
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
key_event.pressed
|
||||
and not key_event.echo
|
||||
and key_event.physical_keycode == KEY_P
|
||||
):
|
||||
if _active:
|
||||
deactivate()
|
||||
return true
|
||||
if can_open and can_activate():
|
||||
activate(pointer_screen_position)
|
||||
return true
|
||||
return false
|
||||
if not _active:
|
||||
return false
|
||||
if not can_open:
|
||||
|
|
@ -149,8 +138,7 @@ func handle_input(
|
|||
if _placing_grid:
|
||||
_set_placement_mode(false)
|
||||
return true
|
||||
deactivate()
|
||||
return true
|
||||
return false
|
||||
if event is InputEventKey:
|
||||
var key_event := event as InputEventKey
|
||||
if not key_event.pressed or key_event.echo:
|
||||
|
|
@ -172,28 +160,19 @@ func handle_input(
|
|||
return true
|
||||
if event is InputEventMouseButton:
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
# Ordinary wheel input always belongs to Hotbar selection. Brush size is
|
||||
# selected explicitly in the Art Kit toolbar.
|
||||
if mouse_event.button_index in [
|
||||
MOUSE_BUTTON_WHEEL_UP,
|
||||
MOUSE_BUTTON_WHEEL_DOWN,
|
||||
]:
|
||||
return false
|
||||
_set_pointer_from_input(
|
||||
mouse_event.position,
|
||||
pointer_screen_position,
|
||||
)
|
||||
_update_aim()
|
||||
if (
|
||||
mouse_event.shift_pressed
|
||||
and mouse_event.button_index in [
|
||||
MOUSE_BUTTON_WHEEL_UP,
|
||||
MOUSE_BUTTON_WHEEL_DOWN,
|
||||
]
|
||||
):
|
||||
return false
|
||||
match mouse_event.button_index:
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
if mouse_event.pressed and not _placing_grid:
|
||||
_set_brush_size(_brush_size + 1)
|
||||
return true
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
if mouse_event.pressed and not _placing_grid:
|
||||
_set_brush_size(_brush_size - 1)
|
||||
return true
|
||||
MOUSE_BUTTON_LEFT:
|
||||
if _placing_grid:
|
||||
if mouse_event.pressed:
|
||||
|
|
@ -241,7 +220,7 @@ func activate(
|
|||
if _active or not can_activate():
|
||||
return
|
||||
_active = true
|
||||
_placing_grid = true
|
||||
_placing_grid = false
|
||||
_eraser_mode = false
|
||||
_clear_armed_guide_action(false)
|
||||
_refresh_local_unlocks()
|
||||
|
|
@ -257,9 +236,7 @@ func activate(
|
|||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_update_aim()
|
||||
_refresh_stencil_visibility()
|
||||
_emit_hud_state(
|
||||
"click to place a shared grid"
|
||||
)
|
||||
_emit_hud_state("marker selected")
|
||||
|
||||
|
||||
func deactivate() -> void:
|
||||
|
|
@ -288,7 +265,11 @@ func is_placement_mode() -> bool:
|
|||
|
||||
|
||||
func can_activate() -> bool:
|
||||
return _drawing_available() and _owns_art_kit()
|
||||
return (
|
||||
_drawing_available()
|
||||
and _owns_art_kit()
|
||||
and _art_kit_is_selected()
|
||||
)
|
||||
|
||||
|
||||
func set_placement_mode(enabled: bool) -> void:
|
||||
|
|
@ -344,7 +325,7 @@ func arm_guide_action(action: int) -> bool:
|
|||
_selected_canvas_id = ""
|
||||
_update_aim()
|
||||
_refresh_stencil_visibility()
|
||||
_emit_hud_state("click a grid to %s it" % _guide_action_verb(action))
|
||||
_emit_hud_state(_guide_action_status(action))
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -415,6 +396,15 @@ func _owns_art_kit() -> bool:
|
|||
return _bag != null and _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
|
||||
|
||||
|
||||
func _art_kit_is_selected() -> bool:
|
||||
return (
|
||||
_hotbar != null
|
||||
and _hotbar.get_selected_assignment_kind()
|
||||
== PlayerHotbar.AssignmentKind.ITEM
|
||||
and _hotbar.get_selected_item_id() == ArtShopStockType.ART_KIT_ITEM_ID
|
||||
)
|
||||
|
||||
|
||||
func _local_entitlement() -> Dictionary:
|
||||
return {
|
||||
"has_kit": _owns_art_kit(),
|
||||
|
|
@ -550,8 +540,7 @@ func request_canvas_at_surface(
|
|||
tangent: Vector3,
|
||||
) -> bool:
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
not can_activate()
|
||||
or _art_unlocks == null
|
||||
or not _art_unlocks.is_grid_size_unlocked(_grid_size)
|
||||
or normal.is_zero_approx()
|
||||
|
|
@ -581,8 +570,7 @@ func request_cell_edits(
|
|||
stroke_id: String = "",
|
||||
) -> bool:
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
not can_activate()
|
||||
or canvas_id.is_empty()
|
||||
or edits.is_empty()
|
||||
):
|
||||
|
|
@ -613,7 +601,7 @@ func request_guide_visibility(
|
|||
should_be_visible: bool,
|
||||
should_finalize: bool = false,
|
||||
) -> bool:
|
||||
if not _drawing_available() or not _owns_art_kit() or canvas_id.is_empty():
|
||||
if not can_activate() or canvas_id.is_empty():
|
||||
return false
|
||||
var data: Dictionary = {
|
||||
"request_id": _new_request_id("guide"),
|
||||
|
|
@ -633,8 +621,7 @@ func request_guide_visibility(
|
|||
|
||||
func request_undo_last_stroke() -> bool:
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
not can_activate()
|
||||
or _last_local_stroke_id.is_empty()
|
||||
):
|
||||
_emit_hud_state("nothing to undo")
|
||||
|
|
@ -698,6 +685,9 @@ func get_canvas_state(canvas_id: String) -> Dictionary:
|
|||
func _process(_delta: float) -> void:
|
||||
if not _active:
|
||||
return
|
||||
if not can_activate():
|
||||
deactivate()
|
||||
return
|
||||
_update_aim()
|
||||
if _placing_grid:
|
||||
return
|
||||
|
|
@ -1874,8 +1864,10 @@ func _execute_armed_guide_action() -> void:
|
|||
_restore_selected_guide()
|
||||
GuideAction.FINALIZE:
|
||||
_finalize_selected_guide()
|
||||
_clear_armed_guide_action(true)
|
||||
_emit_hud_state("")
|
||||
# Guide actions are persistent modes. Keep the selected action armed so
|
||||
# multiple grids can be handled without returning to the toolbar after
|
||||
# every click; selecting the active toggle again explicitly exits it.
|
||||
_emit_hud_state(_guide_action_status(action))
|
||||
|
||||
|
||||
func _cancel_armed_guide_action() -> void:
|
||||
|
|
@ -1904,12 +1896,18 @@ func _guide_action_verb(action: int) -> String:
|
|||
GuideAction.HIDE:
|
||||
return "hide"
|
||||
GuideAction.RESTORE:
|
||||
return "restore"
|
||||
return "show"
|
||||
GuideAction.FINALIZE:
|
||||
return "finish"
|
||||
return "use"
|
||||
|
||||
|
||||
func _guide_action_status(action: int) -> String:
|
||||
return "%s grid mode • click grids • toggle again to exit" % (
|
||||
_guide_action_verb(action).capitalize()
|
||||
)
|
||||
|
||||
|
||||
func _remove_selected_guide() -> void:
|
||||
if _selected_canvas_id.is_empty():
|
||||
_emit_hud_state("aim at a placed grid before removing its guide")
|
||||
|
|
@ -2163,7 +2161,7 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
|
||||
|
||||
func _on_local_art_entitlement_changed() -> void:
|
||||
if _active and not _owns_art_kit():
|
||||
if _active and not can_activate():
|
||||
deactivate()
|
||||
_publish_local_entitlement()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const FishingRodAttachmentScene = preload(
|
|||
const HeldItemAttachmentScene = preload(
|
||||
"res://player/held_item_attachment.tscn"
|
||||
)
|
||||
const SprintDustTrailType = preload("res://player/sprint_dust_trail.gd")
|
||||
|
||||
signal retract_visual_finished
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ const BLINK_DURATION_SECONDS: float = 0.11
|
|||
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
||||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||
const BASE_REEL_SPEED: float = 0.16
|
||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||
# The target Android handheld exposes its physical right trigger through
|
||||
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
||||
# remains isolated from camera behavior.
|
||||
|
|
@ -348,6 +350,7 @@ var _held_item_attachment: BoneAttachment3D
|
|||
var _held_fish_display: Node3D
|
||||
var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
|
||||
var _held_fish_sprite: Sprite3D
|
||||
var _held_art_kit_display: Node3D
|
||||
var _held_art_kit_sprite: Sprite3D
|
||||
var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
|
||||
|
||||
|
|
@ -424,8 +427,11 @@ var _fishing_rod: Node3D
|
|||
var _fishing_rod_tip: Marker3D
|
||||
var _active_item_is_rod: bool = false
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _sprint_dust_landing_ready: bool = false
|
||||
var _sprint_dust_airborne: bool = false
|
||||
var _sprint_dust_fall_speed: float = 0.0
|
||||
|
||||
@onready var _sprint_dust: CPUParticles3D = %SprintDust
|
||||
@onready var _sprint_dust: SprintDustTrailType = %SprintDust
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -505,6 +511,7 @@ func _initialize_held_item_attachment() -> void:
|
|||
_held_fish_sprite = attachment.get_node(
|
||||
"HeldFishDisplay/HeldFishSprite"
|
||||
) as Sprite3D
|
||||
_held_art_kit_display = attachment.get_node("HeldArtKitDisplay") as Node3D
|
||||
_held_art_kit_sprite = attachment.get_node(
|
||||
"HeldArtKitDisplay/HeldArtKitSprite"
|
||||
) as Sprite3D
|
||||
|
|
@ -608,7 +615,7 @@ func _physics_process(delta: float) -> void:
|
|||
func _process(delta: float) -> void:
|
||||
_update_blink(delta)
|
||||
_update_character_animation()
|
||||
_update_sprint_dust()
|
||||
_update_sprint_dust(delta)
|
||||
# The hand bone supplies the attachment position, but its animated wrist
|
||||
# rotation should not turn the flat fish/catch artwork edge-on. Keep each
|
||||
# display aligned with the character's facing while it follows the hand.
|
||||
|
|
@ -694,24 +701,54 @@ func _schedule_next_blink() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _update_sprint_dust() -> void:
|
||||
func _update_sprint_dust(delta: float) -> void:
|
||||
if _sprint_dust == null:
|
||||
return
|
||||
var horizontal_speed_squared: float = (
|
||||
velocity.x * velocity.x + velocity.z * velocity.z
|
||||
)
|
||||
var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
|
||||
var grounded: bool = is_on_floor()
|
||||
if _water_recovery_active:
|
||||
_sprint_dust_airborne = false
|
||||
_sprint_dust_fall_speed = 0.0
|
||||
elif not _sprint_dust_landing_ready:
|
||||
# Establish a real ground contact before arming landing effects. This
|
||||
# prevents the player's initial placement from looking like a fall.
|
||||
_sprint_dust_landing_ready = grounded
|
||||
elif not grounded:
|
||||
_sprint_dust_airborne = true
|
||||
_sprint_dust_fall_speed = maxf(
|
||||
_sprint_dust_fall_speed,
|
||||
maxf(-velocity.y, 0.0),
|
||||
)
|
||||
elif _sprint_dust_airborne:
|
||||
if _sprint_dust_fall_speed >= LANDING_DUST_MIN_FALL_SPEED:
|
||||
var landing_facing: Vector3 = horizontal_velocity.normalized()
|
||||
if landing_facing.is_zero_approx() and _visuals != null:
|
||||
landing_facing = -_visuals.global_basis.z
|
||||
_sprint_dust.emit_landing_burst(
|
||||
global_position,
|
||||
landing_facing,
|
||||
)
|
||||
_sprint_dust_airborne = false
|
||||
_sprint_dust_fall_speed = 0.0
|
||||
var horizontal_speed_squared: float = horizontal_velocity.length_squared()
|
||||
var fastest_non_sprint_speed := maxf(
|
||||
walk_speed,
|
||||
maxf(sneak_speed, slow_walk_speed)
|
||||
)
|
||||
var running_threshold := fastest_non_sprint_speed + 0.5
|
||||
_sprint_dust.emitting = (
|
||||
is_on_floor()
|
||||
var should_emit: bool = (
|
||||
grounded
|
||||
and not _sitting
|
||||
and not _water_recovery_active
|
||||
and horizontal_speed_squared
|
||||
> running_threshold * running_threshold
|
||||
)
|
||||
_sprint_dust.update_trail(
|
||||
delta,
|
||||
global_position,
|
||||
horizontal_velocity,
|
||||
should_emit,
|
||||
)
|
||||
|
||||
|
||||
func _get_controller_camera_stick() -> Vector2:
|
||||
|
|
@ -1675,6 +1712,8 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
|||
_held_art_kit_visible = true
|
||||
_held_art_kit_sprite.texture = icon
|
||||
_held_art_kit_sprite.visible = true
|
||||
if not previous_show_pose:
|
||||
_held_art_kit_display.visible = false
|
||||
_fishing_rod.visible = false
|
||||
if item_changed:
|
||||
_begin_pocket_visual(
|
||||
|
|
@ -1682,8 +1721,11 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
|||
previous_show_pose,
|
||||
true,
|
||||
Callable(),
|
||||
false,
|
||||
_apply_active_art_kit_visual,
|
||||
)
|
||||
else:
|
||||
_apply_active_art_kit_visual()
|
||||
_update_character_animation()
|
||||
|
||||
|
||||
|
|
@ -1763,6 +1805,7 @@ func _apply_pending_held_fish() -> void:
|
|||
func _clear_active_art_kit() -> void:
|
||||
var was_visible: bool = _held_art_kit_visible
|
||||
_held_art_kit_visible = false
|
||||
_held_art_kit_display.visible = false
|
||||
_held_art_kit_sprite.texture = null
|
||||
_held_art_kit_sprite.visible = false
|
||||
if was_visible:
|
||||
|
|
@ -1772,6 +1815,12 @@ func _clear_active_art_kit() -> void:
|
|||
_update_character_animation()
|
||||
|
||||
|
||||
func _apply_active_art_kit_visual() -> void:
|
||||
if not _held_art_kit_visible or _held_art_kit_sprite.texture == null:
|
||||
return
|
||||
_held_art_kit_display.visible = true
|
||||
|
||||
|
||||
func _begin_pocket_visual(
|
||||
target: PocketVisualTarget,
|
||||
starts_in_show_pose: bool,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=21 format=3]
|
||||
[gd_scene load_steps=22 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
||||
[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||
radius = 0.45
|
||||
|
|
@ -25,7 +26,7 @@ material = ExtResource("12_blob_shadow")
|
|||
size = Vector2(1.15, 0.72)
|
||||
|
||||
[sub_resource type="Shader" id="SprintDustShader"]
|
||||
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec4 dust_color : source_color = vec4(0.47, 0.39, 0.27, 0.42);\nvarying float particle_phase;\n\nvoid vertex() {\n\tparticle_phase = INSTANCE_CUSTOM.y;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble = sin(angle * 5.0 + particle_phase * 8.0) * 0.07;\n\tfloat radius = length(centered_uv);\n\tfloat soft_shape = 1.0 - smoothstep(0.48 + edge_wobble, 1.0 + edge_wobble, radius);\n\tfloat fade_in = smoothstep(0.0, 0.12, particle_phase);\n\tfloat fade_out = 1.0 - smoothstep(0.38, 1.0, particle_phase);\n\tALBEDO = dust_color.rgb;\n\tALPHA = soft_shape * dust_color.a * fade_in * fade_out;\n}\n"
|
||||
code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec3 dust_color : source_color = vec3(0.61, 0.57, 0.53);\nvarying flat float puff_phase;\nvarying flat float puff_alpha;\n\nvoid vertex() {\n\tpuff_phase = INSTANCE_CUSTOM.x;\n\tpuff_alpha = INSTANCE_CUSTOM.w;\n\tVERTEX.xy *= INSTANCE_CUSTOM.yz;\n\tfloat puff_rotation = puff_phase * 6.28318530718;\n\tfloat rotation_sine = sin(puff_rotation);\n\tfloat rotation_cosine = cos(puff_rotation);\n\tVERTEX.xy = mat2(\n\t\tvec2(rotation_cosine, -rotation_sine),\n\t\tvec2(rotation_sine, rotation_cosine)\n\t) * VERTEX.xy;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble =\n\t\tsin(angle * 5.0 + puff_phase * 8.0) * 0.07\n\t\t+ sin(angle * 3.0 - puff_phase * 5.0) * 0.025;\n\tfloat radius = length(centered_uv);\n\tif (radius > 0.82 + edge_wobble) {\n\t\tdiscard;\n\t}\n\tALBEDO = dust_color;\n\tALPHA = puff_alpha;\n}\n"
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="SprintDustMaterial"]
|
||||
render_priority = 1
|
||||
|
|
@ -33,7 +34,7 @@ shader = SubResource("SprintDustShader")
|
|||
|
||||
[sub_resource type="QuadMesh" id="SprintDustMesh"]
|
||||
material = SubResource("SprintDustMaterial")
|
||||
size = Vector2(0.38, 0.28)
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
collision_layer = 2
|
||||
|
|
@ -66,27 +67,11 @@ rotation_degrees = Vector3(-90, 0, 0)
|
|||
cast_shadow = 0
|
||||
mesh = SubResource("GroundShadowMesh")
|
||||
|
||||
[node name="SprintDust" type="CPUParticles3D" parent="."]
|
||||
[node name="SprintDust" type="MultiMeshInstance3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.08, 0)
|
||||
emitting = false
|
||||
amount = 16
|
||||
lifetime = 0.65
|
||||
randomness = 0.55
|
||||
visibility_aabb = AABB(-4, -2, -4, 8, 5, 8)
|
||||
local_coords = false
|
||||
emission_shape = 3
|
||||
emission_box_extents = Vector3(0.24, 0.03, 0.16)
|
||||
direction = Vector3(0, 1, 0)
|
||||
spread = 65.0
|
||||
gravity = Vector3(0, -0.8, 0)
|
||||
initial_velocity_min = 0.35
|
||||
initial_velocity_max = 0.8
|
||||
angular_velocity_min = -90.0
|
||||
angular_velocity_max = 90.0
|
||||
scale_amount_min = 0.35
|
||||
scale_amount_max = 0.72
|
||||
mesh = SubResource("SprintDustMesh")
|
||||
cast_shadow = 0
|
||||
script = ExtResource("16_sprint_dust")
|
||||
puff_mesh = SubResource("SprintDustMesh")
|
||||
|
||||
[node name="CharacterRig" parent="Visuals" instance=ExtResource("13_character")]
|
||||
rotation_degrees = Vector3(0, 180, 0)
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ static func _runtime_fur_pattern_material(
|
|||
static func _configure_matte_material(material: StandardMaterial3D) -> void:
|
||||
if material == null:
|
||||
return
|
||||
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||
# Character artwork is deliberately graphic and flat. Unshaded rendering
|
||||
# keeps the authored fur and facial colors invariant across world weather,
|
||||
# time of day, and the isolated Profile preview.
|
||||
|
|
|
|||
249
player/sprint_dust_trail.gd
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
class_name SprintDustTrail
|
||||
extends MultiMeshInstance3D
|
||||
|
||||
const PUFF_COUNT: int = 12
|
||||
const PUFF_SPACING: float = 0.46
|
||||
const PUFF_LIFETIME: float = 0.62
|
||||
const PUFF_GROW_TIME: float = 0.16
|
||||
const PUFF_FADE_TIME: float = 0.22
|
||||
const PUFF_BACK_OFFSET: float = 0.14
|
||||
const PUFF_SIDE_OFFSET: float = 0.11
|
||||
const PUFF_HEIGHT: float = 0.08
|
||||
const PUFF_MIN_SCALE: float = 0.22
|
||||
|
||||
@export var puff_mesh: Mesh
|
||||
|
||||
var _puff_positions: Array[Vector3] = []
|
||||
var _puff_ages := PackedFloat32Array()
|
||||
var _puff_widths := PackedFloat32Array()
|
||||
var _puff_heights := PackedFloat32Array()
|
||||
var _puff_phases := PackedFloat32Array()
|
||||
var _next_puff: int = 0
|
||||
var _spawn_serial: int = 0
|
||||
var _foot_side: float = -1.0
|
||||
var _tracking_run: bool = false
|
||||
var _previous_source_position: Vector3
|
||||
var _distance_since_puff: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
top_level = true
|
||||
global_transform = Transform3D.IDENTITY
|
||||
_initialize_pool()
|
||||
|
||||
|
||||
func update_trail(
|
||||
delta: float,
|
||||
source_position: Vector3,
|
||||
movement_velocity: Vector3,
|
||||
is_running: bool,
|
||||
) -> void:
|
||||
_update_existing_puffs(delta)
|
||||
custom_aabb = AABB(
|
||||
source_position - Vector3(5.0, 2.0, 5.0),
|
||||
Vector3(10.0, 5.0, 10.0),
|
||||
)
|
||||
if not is_running:
|
||||
_tracking_run = false
|
||||
_distance_since_puff = 0.0
|
||||
return
|
||||
|
||||
var flat_source := Vector3(
|
||||
source_position.x,
|
||||
source_position.y,
|
||||
source_position.z,
|
||||
)
|
||||
if not _tracking_run:
|
||||
_tracking_run = true
|
||||
_previous_source_position = flat_source
|
||||
_distance_since_puff = 0.0
|
||||
return
|
||||
|
||||
var segment := flat_source - _previous_source_position
|
||||
segment.y = 0.0
|
||||
var remaining_distance: float = segment.length()
|
||||
if remaining_distance <= 0.0001:
|
||||
_previous_source_position = flat_source
|
||||
return
|
||||
|
||||
var segment_direction: Vector3 = segment / remaining_distance
|
||||
var movement_direction := Vector3(
|
||||
movement_velocity.x,
|
||||
0.0,
|
||||
movement_velocity.z,
|
||||
).normalized()
|
||||
if movement_direction.is_zero_approx():
|
||||
movement_direction = segment_direction
|
||||
var cursor: Vector3 = _previous_source_position
|
||||
var distance_to_next: float = PUFF_SPACING - _distance_since_puff
|
||||
while remaining_distance >= distance_to_next:
|
||||
cursor += segment_direction * distance_to_next
|
||||
_spawn_puff(cursor, movement_direction)
|
||||
remaining_distance -= distance_to_next
|
||||
_distance_since_puff = 0.0
|
||||
distance_to_next = PUFF_SPACING
|
||||
_distance_since_puff += remaining_distance
|
||||
_previous_source_position = flat_source
|
||||
|
||||
|
||||
func clear_trail() -> void:
|
||||
_tracking_run = false
|
||||
_distance_since_puff = 0.0
|
||||
for index: int in PUFF_COUNT:
|
||||
_puff_ages[index] = PUFF_LIFETIME
|
||||
_write_inactive_puff(index)
|
||||
|
||||
|
||||
func emit_landing_burst(
|
||||
source_position: Vector3,
|
||||
facing_direction: Vector3,
|
||||
) -> void:
|
||||
var direction := Vector3(
|
||||
facing_direction.x,
|
||||
0.0,
|
||||
facing_direction.z,
|
||||
).normalized()
|
||||
if direction.is_zero_approx():
|
||||
direction = Vector3.FORWARD
|
||||
var first_direction: Vector3 = -direction
|
||||
for puff_index: int in 5:
|
||||
var spread_direction := first_direction.rotated(
|
||||
Vector3.UP,
|
||||
TAU * float(puff_index) / 5.0,
|
||||
)
|
||||
var radius: float = lerpf(
|
||||
0.38,
|
||||
0.46,
|
||||
_stable_variation(_spawn_serial + puff_index + 81),
|
||||
)
|
||||
_spawn_puff_at(
|
||||
source_position
|
||||
+ spread_direction * radius
|
||||
+ Vector3.UP * PUFF_HEIGHT,
|
||||
0.95,
|
||||
)
|
||||
|
||||
|
||||
func get_active_puff_count() -> int:
|
||||
var active_count: int = 0
|
||||
for age: float in _puff_ages:
|
||||
if age < PUFF_LIFETIME:
|
||||
active_count += 1
|
||||
return active_count
|
||||
|
||||
|
||||
func get_first_active_puff_position() -> Vector3:
|
||||
for index: int in PUFF_COUNT:
|
||||
if _puff_ages[index] < PUFF_LIFETIME:
|
||||
return _puff_positions[index]
|
||||
return Vector3.INF
|
||||
|
||||
|
||||
func _initialize_pool() -> void:
|
||||
var pool := MultiMesh.new()
|
||||
pool.transform_format = MultiMesh.TRANSFORM_3D
|
||||
pool.use_custom_data = true
|
||||
pool.instance_count = PUFF_COUNT
|
||||
pool.mesh = puff_mesh
|
||||
multimesh = pool
|
||||
_puff_positions.resize(PUFF_COUNT)
|
||||
_puff_ages.resize(PUFF_COUNT)
|
||||
_puff_widths.resize(PUFF_COUNT)
|
||||
_puff_heights.resize(PUFF_COUNT)
|
||||
_puff_phases.resize(PUFF_COUNT)
|
||||
clear_trail()
|
||||
|
||||
|
||||
func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
|
||||
var lateral := Vector3(-direction.z, 0.0, direction.x)
|
||||
var position := (
|
||||
source_position
|
||||
- direction * PUFF_BACK_OFFSET
|
||||
+ lateral * PUFF_SIDE_OFFSET * _foot_side
|
||||
+ Vector3.UP * PUFF_HEIGHT
|
||||
)
|
||||
_spawn_puff_at(position)
|
||||
_foot_side *= -1.0
|
||||
|
||||
|
||||
func _spawn_puff_at(
|
||||
position: Vector3,
|
||||
size_multiplier: float = 1.0,
|
||||
) -> void:
|
||||
var variation: float = _stable_variation(_spawn_serial)
|
||||
var second_variation: float = _stable_variation(_spawn_serial + 37)
|
||||
var index: int = _next_puff
|
||||
_puff_positions[index] = position
|
||||
_puff_ages[index] = 0.0
|
||||
_puff_widths[index] = (
|
||||
lerpf(0.34, 0.43, variation) * size_multiplier
|
||||
)
|
||||
_puff_heights[index] = (
|
||||
lerpf(0.23, 0.30, second_variation) * size_multiplier
|
||||
)
|
||||
_puff_phases[index] = variation
|
||||
_write_puff(index, PUFF_MIN_SCALE, 1.0)
|
||||
_next_puff = (_next_puff + 1) % PUFF_COUNT
|
||||
_spawn_serial += 1
|
||||
|
||||
|
||||
func _update_existing_puffs(delta: float) -> void:
|
||||
if multimesh == null:
|
||||
return
|
||||
for index: int in PUFF_COUNT:
|
||||
var age: float = _puff_ages[index]
|
||||
if age >= PUFF_LIFETIME:
|
||||
continue
|
||||
age += delta
|
||||
_puff_ages[index] = age
|
||||
if age >= PUFF_LIFETIME:
|
||||
_write_inactive_puff(index)
|
||||
continue
|
||||
var visual_scale: float = 1.0
|
||||
if age < PUFF_GROW_TIME:
|
||||
var grow_weight: float = smoothstep(
|
||||
0.0,
|
||||
1.0,
|
||||
age / PUFF_GROW_TIME,
|
||||
)
|
||||
visual_scale = lerpf(PUFF_MIN_SCALE, 1.0, grow_weight)
|
||||
var visual_alpha: float = 1.0
|
||||
if age > PUFF_LIFETIME - PUFF_FADE_TIME:
|
||||
var fade_weight: float = smoothstep(
|
||||
0.0,
|
||||
1.0,
|
||||
(age - (PUFF_LIFETIME - PUFF_FADE_TIME))
|
||||
/ PUFF_FADE_TIME,
|
||||
)
|
||||
visual_alpha = 1.0 - fade_weight
|
||||
_write_puff(index, visual_scale, visual_alpha)
|
||||
|
||||
|
||||
func _write_puff(
|
||||
index: int,
|
||||
visual_scale: float,
|
||||
visual_alpha: float,
|
||||
) -> void:
|
||||
var puff_transform := Transform3D.IDENTITY
|
||||
puff_transform.origin = _puff_positions[index]
|
||||
multimesh.set_instance_transform(index, puff_transform)
|
||||
multimesh.set_instance_custom_data(
|
||||
index,
|
||||
Color(
|
||||
_puff_phases[index],
|
||||
_puff_widths[index] * visual_scale,
|
||||
_puff_heights[index] * visual_scale,
|
||||
visual_alpha,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
func _write_inactive_puff(index: int) -> void:
|
||||
var inactive_transform := Transform3D.IDENTITY
|
||||
inactive_transform.origin = _puff_positions[index]
|
||||
multimesh.set_instance_transform(index, inactive_transform)
|
||||
multimesh.set_instance_custom_data(index, Color(0.0, 0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
func _stable_variation(serial: int) -> float:
|
||||
return fposmod(sin(float(serial + 1) * 12.9898) * 43758.5453, 1.0)
|
||||
1
player/sprint_dust_trail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://o2gi5oqunsp8
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,55 @@ func _run() -> void:
|
|||
await physics_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
||||
assert(sprint_dust != null)
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.update_trail(
|
||||
0.016,
|
||||
Vector3.ZERO,
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.update_trail(
|
||||
0.016,
|
||||
Vector3(0.5, 0.0, 0.0),
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 1)
|
||||
var planted_puff_position: Vector3 = (
|
||||
sprint_dust.get_first_active_puff_position()
|
||||
)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3(0.75, 0.0, 0.0),
|
||||
Vector3(4.5, 0.0, 0.0),
|
||||
true,
|
||||
)
|
||||
assert(
|
||||
sprint_dust.get_first_active_puff_position()
|
||||
== planted_puff_position
|
||||
)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3(0.75, 0.0, 0.0),
|
||||
Vector3.ZERO,
|
||||
false,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 1)
|
||||
sprint_dust.clear_trail()
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
sprint_dust.emit_landing_burst(Vector3.ZERO, Vector3.FORWARD)
|
||||
assert(sprint_dust.get_active_puff_count() == 5)
|
||||
sprint_dust.update_trail(
|
||||
0.08,
|
||||
Vector3.ZERO,
|
||||
Vector3.ZERO,
|
||||
false,
|
||||
)
|
||||
assert(sprint_dust.get_active_puff_count() == 5)
|
||||
sprint_dust.clear_trail()
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
) as NetworkSurfaceDrawingService
|
||||
|
|
@ -210,9 +259,8 @@ func _run() -> void:
|
|||
assert(game_ui.is_gameplay_hud_hidden())
|
||||
assert(player_menu.visible)
|
||||
assert(root.gui_get_focus_owner() is not LineEdit)
|
||||
player_menu.close_menu()
|
||||
for _frame: int in 12:
|
||||
await process_frame
|
||||
player_menu.close_menu(PlayerMenu.CloseReason.SESSION_END)
|
||||
await process_frame
|
||||
game_ui.call("_on_quick_action_selected", &"hud")
|
||||
await process_frame
|
||||
assert(not game_ui.is_gameplay_hud_hidden())
|
||||
|
|
@ -220,14 +268,17 @@ func _run() -> void:
|
|||
assert(not service.can_activate())
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
|
||||
assert(service.can_activate())
|
||||
|
||||
player.hotbar.select_slot(1)
|
||||
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
||||
assert(not service.can_activate())
|
||||
var paint_key := InputEventKey.new()
|
||||
paint_key.physical_keycode = KEY_P
|
||||
paint_key.pressed = true
|
||||
assert(service.handle_input(paint_key, true))
|
||||
assert(not service.handle_input(paint_key, true))
|
||||
assert(not service.is_active())
|
||||
player.hotbar.select_slot(0)
|
||||
await process_frame
|
||||
assert(service.is_active() and service.is_placement_mode())
|
||||
assert(service.is_active() and not service.is_placement_mode())
|
||||
assert(toolbar.visible)
|
||||
var ui_root := game_ui.get_node("%UIRoot") as Control
|
||||
assert(toolbar.get_parent() == ui_root)
|
||||
|
|
@ -242,6 +293,8 @@ func _run() -> void:
|
|||
assert(toolbar.get_global_rect().end.y <= 720.0)
|
||||
var top_panel := toolbar.get_node("%TopPanel") as PanelContainer
|
||||
var color_panel := toolbar.get_node("%ColorPanel") as PanelContainer
|
||||
assert(is_equal_approx(toolbar.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
|
||||
assert(is_equal_approx(top_panel.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
|
||||
assert(top_panel.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(color_panel.position.x > 0.0)
|
||||
assert(is_equal_approx(color_panel.get_rect().end.x, top_panel.get_rect().end.x))
|
||||
|
|
@ -266,7 +319,6 @@ func _run() -> void:
|
|||
var hide_button := toolbar.get_node("%HideGuideButton") as Button
|
||||
var restore_button := toolbar.get_node("%RestoreGuideButton") as Button
|
||||
var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button
|
||||
var close_button := toolbar.get_node("%CloseButton") as Button
|
||||
assert(
|
||||
mode_button != null
|
||||
and eraser_button != null
|
||||
|
|
@ -274,8 +326,44 @@ func _run() -> void:
|
|||
and hide_button != null
|
||||
and restore_button != null
|
||||
and finalize_button != null
|
||||
and close_button != null
|
||||
)
|
||||
assert(toolbar.get_node_or_null("%CloseButton") == null)
|
||||
var expected_toolbar_icons: Dictionary[Button, String] = {
|
||||
mode_button: "/art/art_kit_marker.png",
|
||||
eraser_button: "/art/art_kit_eraser.png",
|
||||
hide_button: "/art/art_kit_grid_hidden_light.png",
|
||||
restore_button: "/art/art_kit_grid_restore_light.png",
|
||||
finalize_button: "/art/art_kit_grid_finish_light.png",
|
||||
}
|
||||
for icon_button: Button in expected_toolbar_icons:
|
||||
assert(icon_button.text.is_empty())
|
||||
assert(icon_button.icon != null)
|
||||
assert(
|
||||
icon_button.icon.resource_path.ends_with(
|
||||
expected_toolbar_icons[icon_button]
|
||||
)
|
||||
)
|
||||
for icon_button: Button in [
|
||||
eraser_button,
|
||||
undo_button,
|
||||
hide_button,
|
||||
restore_button,
|
||||
finalize_button,
|
||||
]:
|
||||
assert(icon_button.custom_minimum_size == Vector2(48, 44))
|
||||
assert(icon_button.get_theme_constant("icon_max_width") == 40)
|
||||
var icon_style := (
|
||||
icon_button.get_theme_stylebox("normal") as StyleBoxFlat
|
||||
)
|
||||
assert(is_equal_approx(icon_style.content_margin_left, 4.0))
|
||||
assert(is_equal_approx(icon_style.content_margin_top, 2.0))
|
||||
assert(mode_button.custom_minimum_size == Vector2(48, 48))
|
||||
assert(mode_button.get_theme_constant("icon_max_width") == 40)
|
||||
assert(mode_button.get_index() > finalize_button.get_index())
|
||||
assert(hide_button.button_group != null)
|
||||
assert(hide_button.button_group == restore_button.button_group)
|
||||
assert(hide_button.button_group == finalize_button.button_group)
|
||||
assert(hide_button.button_group.allow_unpress)
|
||||
assert(brush_option.item_count == 4)
|
||||
assert(grid_option.item_count == 4)
|
||||
assert(not brush_option.get_popup().is_item_disabled(0))
|
||||
|
|
@ -303,7 +391,12 @@ func _run() -> void:
|
|||
assert(service.get_grid_size() == 128)
|
||||
|
||||
mode_button.pressed.emit()
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.is_placement_mode())
|
||||
assert(
|
||||
mode_button.icon.resource_path.ends_with(
|
||||
"/art/art_kit_grid_light.png"
|
||||
)
|
||||
)
|
||||
eraser_button.pressed.emit()
|
||||
assert(service.is_eraser_mode())
|
||||
assert(eraser_button.button_pressed)
|
||||
|
|
@ -318,60 +411,112 @@ func _run() -> void:
|
|||
world_click.button_index = MOUSE_BUTTON_LEFT
|
||||
world_click.pressed = true
|
||||
assert(service.handle_input(world_click, true))
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.HIDE
|
||||
)
|
||||
assert(service.is_placement_mode())
|
||||
assert(not service.is_eraser_mode())
|
||||
assert(hide_button.button_pressed)
|
||||
hide_button.pressed.emit()
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.NONE
|
||||
)
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.is_eraser_mode())
|
||||
assert(not service.is_placement_mode() and service.is_eraser_mode())
|
||||
|
||||
restore_button.pressed.emit()
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.RESTORE
|
||||
)
|
||||
restore_button.pressed.emit()
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.NONE
|
||||
)
|
||||
assert(not service.is_placement_mode() and service.is_eraser_mode())
|
||||
finalize_button.pressed.emit()
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
|
||||
)
|
||||
assert(not restore_button.button_pressed)
|
||||
assert(finalize_button.button_pressed)
|
||||
assert(service.handle_input(world_click, true))
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
|
||||
)
|
||||
finalize_button.pressed.emit()
|
||||
assert(
|
||||
service.get_armed_guide_action()
|
||||
== NetworkSurfaceDrawingService.GuideAction.NONE
|
||||
)
|
||||
assert(not service.is_placement_mode() and service.is_eraser_mode())
|
||||
(color_buttons[&"ocean_teal"] as Button).pressed.emit()
|
||||
assert(not service.is_eraser_mode())
|
||||
undo_button.pressed.emit()
|
||||
assert(service.is_active())
|
||||
close_button.pressed.emit()
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
|
||||
assert(service.handle_input(paint_key, true))
|
||||
await process_frame
|
||||
assert(not service.handle_input(paint_key, true))
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
|
||||
assert(service.handle_input(paint_key, true))
|
||||
await process_frame
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
||||
player.hotbar.select_slot(0)
|
||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||
await process_frame
|
||||
var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
|
||||
var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
|
||||
var pocket_animation_player := player.get(
|
||||
"_character_animation_player"
|
||||
) as AnimationPlayer
|
||||
var pocket_animation_name: StringName = player.get(
|
||||
"_pocket_visual_animation"
|
||||
)
|
||||
if (
|
||||
not held_art_kit_display.visible
|
||||
and pocket_animation_player != null
|
||||
and not pocket_animation_name.is_empty()
|
||||
and pocket_animation_player.has_animation(pocket_animation_name)
|
||||
):
|
||||
var pocket_animation: Animation = (
|
||||
pocket_animation_player.get_animation(pocket_animation_name)
|
||||
)
|
||||
await create_timer(pocket_animation.length * 0.55).timeout
|
||||
assert(held_art_kit_display.visible)
|
||||
assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
|
||||
assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
|
||||
assert(is_zero_approx(angle_difference(
|
||||
held_art_kit_sprite.rotation.z,
|
||||
-PI * 0.5,
|
||||
)))
|
||||
assert(held_art_kit_sprite.flip_h)
|
||||
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
assert(service.get_grid_size() == 128)
|
||||
assert(service.handle_input(world_click, true))
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
player.hotbar.select_slot(1)
|
||||
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
||||
var wheel_down := InputEventMouseButton.new()
|
||||
wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||||
wheel_down.pressed = true
|
||||
assert(not service.handle_input(wheel_down, true))
|
||||
hotbar_ui._unhandled_input(wheel_down)
|
||||
await process_frame
|
||||
assert(player.hotbar.get_selected_slot() == 1)
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
assert(service.handle_input(paint_key, true))
|
||||
|
||||
var number_one := InputEventKey.new()
|
||||
number_one.physical_keycode = KEY_1
|
||||
number_one.pressed = true
|
||||
assert(not service.handle_input(number_one, true))
|
||||
hotbar_ui._unhandled_input(number_one)
|
||||
await process_frame
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
assert(player.hotbar.get_selected_slot() == 0)
|
||||
var number_two := InputEventKey.new()
|
||||
number_two.physical_keycode = KEY_2
|
||||
number_two.pressed = true
|
||||
assert(not service.handle_input(number_two, true))
|
||||
hotbar_ui._unhandled_input(number_two)
|
||||
await process_frame
|
||||
assert(player.hotbar.get_selected_slot() == 1)
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
game_ui.call("_on_quick_action_selected", &"paint")
|
||||
await process_frame
|
||||
assert(player.hotbar.get_selected_slot() == 0)
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
|
||||
print("Art tools validation: PASS")
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,6 +32,8 @@ func _run_host() -> void:
|
|||
await physics_frame
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.hotbar.assign_item(0, &"art_kit"))
|
||||
assert(player.hotbar.select_slot(0))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
|
||||
var service := main.get_node(
|
||||
|
|
@ -140,6 +142,8 @@ func _run_client() -> void:
|
|||
))
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.hotbar.assign_item(0, &"art_kit"))
|
||||
assert(player.hotbar.select_slot(0))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
|
||||
var service := main.get_node(
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ func _run() -> void:
|
|||
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.hotbar.assign_item(0, &"art_kit"))
|
||||
assert(player.hotbar.select_slot(0))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
) as NetworkSurfaceDrawingService
|
||||
assert(service != null)
|
||||
assert(session.supports_server_capability(SurfaceDrawingProtocol.CAPABILITY))
|
||||
service.deactivate()
|
||||
_validate_marker_controls(service, player)
|
||||
var query := PhysicsRayQueryParameters3D.create(
|
||||
player.global_position + Vector3.UP * 3.0,
|
||||
|
|
@ -124,7 +127,7 @@ func _validate_marker_controls(
|
|||
var activation_pointer := Vector2(824.0, 318.0)
|
||||
service.activate(activation_pointer)
|
||||
assert(service.is_active())
|
||||
assert(service.is_placement_mode())
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.get_pointer_screen_position() == activation_pointer)
|
||||
if DisplayServer.get_name() != "headless":
|
||||
assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
|
||||
|
|
@ -133,7 +136,7 @@ func _validate_marker_controls(
|
|||
placement_key.physical_keycode = KEY_R
|
||||
placement_key.pressed = true
|
||||
assert(service.handle_input(placement_key, true))
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.is_placement_mode())
|
||||
|
||||
var pointer_before: Vector2 = service.get_pointer_screen_position()
|
||||
var pointer_motion := InputEventMouseMotion.new()
|
||||
|
|
@ -171,6 +174,6 @@ func _validate_marker_controls(
|
|||
assert(not service.handle_input(camera_release, true))
|
||||
|
||||
assert(service.handle_input(placement_key, true))
|
||||
assert(service.is_placement_mode())
|
||||
assert(not service.is_placement_mode())
|
||||
service.deactivate()
|
||||
assert(Input.mouse_mode == prior_mouse_mode)
|
||||
|
|
|
|||
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),
|
||||
],
|
||||
)
|
||||
_configure_supply_icon_tile(button, FALLBACK_SUPPLY_ICON)
|
||||
var marker_color_id: StringName = (
|
||||
PlayerArtUnlocksType.color_id_for_product(product_id)
|
||||
)
|
||||
if marker_color_id.is_empty():
|
||||
_configure_supply_icon_tile(button, FALLBACK_SUPPLY_ICON)
|
||||
else:
|
||||
_configure_marker_icon_tile(
|
||||
button,
|
||||
SurfaceDrawingPalette.get_color(marker_color_id),
|
||||
)
|
||||
_add_supply_icon_tile(
|
||||
parent,
|
||||
button,
|
||||
|
|
@ -863,6 +918,29 @@ func _configure_supply_icon_tile(
|
|||
button.text = ""
|
||||
|
||||
|
||||
func _configure_marker_icon_tile(
|
||||
button: Button,
|
||||
marker_color: Color,
|
||||
) -> void:
|
||||
button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE
|
||||
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
button.icon = null
|
||||
button.text = ""
|
||||
var icon := TextureRect.new()
|
||||
icon.name = "MarkerIcon"
|
||||
icon.position = Vector2(4.0, 4.0)
|
||||
icon.size = Vector2(64.0, 64.0)
|
||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var marker_material := ShaderMaterial.new()
|
||||
marker_material.shader = ART_KIT_MARKER_SHADER
|
||||
marker_material.set_shader_parameter("marker_color", marker_color)
|
||||
icon.material = marker_material
|
||||
icon.texture = ART_KIT_MARKER_ICON
|
||||
button.add_child(icon)
|
||||
|
||||
|
||||
func _add_supply_icon_tile(
|
||||
parent: GridContainer,
|
||||
button: Button,
|
||||
|
|
@ -875,7 +953,7 @@ func _add_supply_icon_tile(
|
|||
button.position = Vector2.ZERO
|
||||
button.size = SUPPLY_ICON_TILE_SIZE
|
||||
tile_host.add_child(button)
|
||||
var price_text := "$%d" % price
|
||||
var price_text := str(price)
|
||||
var price_text_width: float = UtilityPageStyleType.TuffyFont.get_string_size(
|
||||
price_text,
|
||||
HORIZONTAL_ALIGNMENT_LEFT,
|
||||
|
|
@ -884,7 +962,12 @@ func _add_supply_icon_tile(
|
|||
).x
|
||||
var price_width: float = minf(
|
||||
SUPPLY_ICON_TILE_SIZE.x,
|
||||
ceilf(price_text_width + SUPPLY_PRICE_HORIZONTAL_PADDING),
|
||||
ceilf(
|
||||
price_text_width
|
||||
+ SUPPLY_PRICE_ICON_SIZE
|
||||
+ SUPPLY_PRICE_ICON_GAP
|
||||
+ SUPPLY_PRICE_HORIZONTAL_PADDING
|
||||
),
|
||||
)
|
||||
var price_bubble := PanelContainer.new()
|
||||
price_bubble.name = "PriceBubble"
|
||||
|
|
@ -905,6 +988,23 @@ func _add_supply_icon_tile(
|
|||
price_style.content_margin_bottom = 0.0
|
||||
price_bubble.add_theme_stylebox_override("panel", price_style)
|
||||
tile_host.add_child(price_bubble)
|
||||
var price_row := HBoxContainer.new()
|
||||
price_row.name = "CurrencyAmount"
|
||||
price_row.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
price_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
price_row.add_theme_constant_override(
|
||||
"separation", int(SUPPLY_PRICE_ICON_GAP)
|
||||
)
|
||||
price_bubble.add_child(price_row)
|
||||
var price_icon := TextureRect.new()
|
||||
price_icon.name = "CurrencyIcon"
|
||||
price_icon.custom_minimum_size = Vector2.ONE * SUPPLY_PRICE_ICON_SIZE
|
||||
price_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
price_icon.texture = CURRENCY_ICON
|
||||
price_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
price_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
price_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
price_row.add_child(price_icon)
|
||||
var price_label := Label.new()
|
||||
price_label.name = "Price"
|
||||
price_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
|
@ -921,7 +1021,7 @@ func _add_supply_icon_tile(
|
|||
"font_color", SUPPLY_PRICE_COLOR
|
||||
)
|
||||
price_label.add_theme_constant_override("outline_size", 0)
|
||||
price_bubble.add_child(price_label)
|
||||
price_row.add_child(price_label)
|
||||
|
||||
|
||||
func _refresh_cooler_capacity() -> void:
|
||||
|
|
@ -930,7 +1030,6 @@ func _refresh_cooler_capacity() -> void:
|
|||
var level: int = _cooler_capacity.get_level()
|
||||
var cost: int = _cooler_capacity.get_next_cost()
|
||||
var next_capacity: int = _cooler_capacity.get_next_capacity()
|
||||
_cooler_level.text = "level %d" % level
|
||||
_cooler_purchase.disabled = (
|
||||
cost < 0
|
||||
or _transaction_in_progress
|
||||
|
|
@ -939,18 +1038,25 @@ func _refresh_cooler_capacity() -> void:
|
|||
or not _network_shop.can_request_purchase()
|
||||
or not _cooler_capacity.can_purchase(_wallet)
|
||||
)
|
||||
_cooler_purchase.tooltip_text = _reel_purchase.tooltip_text
|
||||
var cooler_effect: String
|
||||
if cost < 0:
|
||||
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity()
|
||||
_cooler_cost.text = "max"
|
||||
_cooler_purchase.text = "max"
|
||||
cooler_effect = "%d fish · maximum level" % (
|
||||
_cooler_capacity.get_capacity()
|
||||
)
|
||||
_cooler_price_bubble.hide()
|
||||
else:
|
||||
_cooler_effect.text = "%d → %d fish" % [
|
||||
_cooler_price_bubble.show()
|
||||
cooler_effect = "%d → %d fish" % [
|
||||
_cooler_capacity.get_capacity(),
|
||||
next_capacity,
|
||||
]
|
||||
_cooler_cost.text = "$%d" % cost
|
||||
_cooler_purchase.text = "purchase"
|
||||
_cooler_cost.set_amount(cost)
|
||||
_cooler_purchase.tooltip_text = _upgrade_tooltip(
|
||||
"cooler capacity",
|
||||
level,
|
||||
cooler_effect,
|
||||
)
|
||||
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
|
||||
|
||||
|
||||
func _purchase_reel_speed() -> void:
|
||||
|
|
@ -963,65 +1069,65 @@ func _purchase_barrier_power() -> void:
|
|||
|
||||
func _purchase_supply(item_id: StringName) -> void:
|
||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||
_feedback.text = "unable to complete purchase."
|
||||
_set_feedback("unable to complete purchase.")
|
||||
return
|
||||
var owned: int = _bag.get_quantity(item_id)
|
||||
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||
if item == null:
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
var quantity: int = FishingShopStockType.get_purchase_quantity(
|
||||
item_id, owned, item.max_stack
|
||||
)
|
||||
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
|
||||
_feedback.text = "Your Bag is full."
|
||||
_set_feedback("Your Bag is full.")
|
||||
return
|
||||
var total_cost: int = FishingShopStockType.get_purchase_cost(
|
||||
item_id, quantity, _bag.is_bait_unlocked(item_id)
|
||||
)
|
||||
if total_cost < 0 or not _wallet.can_afford(total_cost):
|
||||
_feedback.text = "Not enough fish coin."
|
||||
_set_feedback("Insufficient funds.")
|
||||
return
|
||||
if _network_shop == null:
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
_network_shop.request_supply(item_id)
|
||||
|
||||
|
||||
func _purchase_art_kit() -> void:
|
||||
if _network_shop == null or not _is_transaction_context_valid():
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
_network_shop.request_art_kit()
|
||||
|
||||
|
||||
func _purchase_art_upgrade(product_id: StringName) -> void:
|
||||
if _network_shop == null or not _is_transaction_context_valid():
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
_network_shop.request_art_upgrade(product_id)
|
||||
|
||||
|
||||
func _purchase_cooler_capacity() -> void:
|
||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||
_feedback.text = "unable to complete purchase."
|
||||
_set_feedback("unable to complete purchase.")
|
||||
return
|
||||
var cost: int = _cooler_capacity.get_next_cost()
|
||||
if cost < 0:
|
||||
_feedback.text = "Upgrade is already at maximum."
|
||||
_set_feedback("Upgrade is already at maximum.")
|
||||
return
|
||||
if not _wallet.can_afford(cost):
|
||||
_feedback.text = "Not enough fish coin."
|
||||
_set_feedback("Insufficient funds.")
|
||||
return
|
||||
if _network_shop == null:
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
_network_shop.request_cooler_capacity_upgrade()
|
||||
|
||||
|
||||
func _purchase_upgrade(is_reel_speed: bool) -> void:
|
||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||
_feedback.text = "unable to complete purchase."
|
||||
_set_feedback("unable to complete purchase.")
|
||||
return
|
||||
var cost: int = (
|
||||
_upgrades.get_next_reel_speed_cost()
|
||||
|
|
@ -1029,13 +1135,13 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
|
|||
else _upgrades.get_next_barrier_power_cost()
|
||||
)
|
||||
if cost < 0:
|
||||
_feedback.text = "Upgrade is already at maximum."
|
||||
_set_feedback("Upgrade is already at maximum.")
|
||||
return
|
||||
if not _wallet.can_afford(cost):
|
||||
_feedback.text = "Not enough fish coin."
|
||||
_set_feedback("Insufficient funds.")
|
||||
return
|
||||
if _network_shop == null:
|
||||
_feedback.text = "Purchase could not be completed."
|
||||
_set_feedback("Purchase could not be completed.")
|
||||
return
|
||||
if is_reel_speed:
|
||||
_network_shop.request_reel_speed_upgrade()
|
||||
|
|
@ -1046,7 +1152,7 @@ func _purchase_upgrade(is_reel_speed: bool) -> void:
|
|||
func _on_network_purchase_pending(_request_id: String) -> void:
|
||||
_transaction_in_progress = true
|
||||
if visible:
|
||||
_feedback.text = "Purchasing…"
|
||||
_set_feedback("Purchasing…")
|
||||
_refresh_all()
|
||||
|
||||
|
||||
|
|
@ -1062,14 +1168,21 @@ func _on_network_purchase_finished(
|
|||
_transaction_in_progress = false
|
||||
if not visible:
|
||||
return
|
||||
_feedback.text = (
|
||||
"Purchase complete. $%d spent." % total_cost
|
||||
_set_feedback(
|
||||
(
|
||||
"Purchase complete • %s spent."
|
||||
% CurrencyPresentation.bbcode_amount(total_cost, 18)
|
||||
)
|
||||
if accepted
|
||||
else message
|
||||
)
|
||||
_refresh_all()
|
||||
|
||||
|
||||
func _set_feedback(message: String) -> void:
|
||||
_feedback.text = "[center]%s[/center]" % message
|
||||
|
||||
|
||||
func _is_transaction_context_valid() -> bool:
|
||||
return (
|
||||
visible
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural"
|
|||
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
|
||||
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
|
||||
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
||||
const ART_KIT_ITEM_ID: StringName = &"art_kit"
|
||||
|
||||
|
||||
@onready var _canonical_stage: Control = %CanonicalStage
|
||||
|
|
@ -164,6 +165,7 @@ var _network_chat_service: NetworkChatService
|
|||
var _network_profile: NetworkProfilePreferences
|
||||
var _spawn_service: PlayerSpawnService
|
||||
var _bag: PlayerBagType
|
||||
var _hotbar: PlayerHotbarType
|
||||
var _item_catalog: ItemCatalogType
|
||||
var _player_menu_open: bool = false
|
||||
var _gameplay_ui_enabled: bool = false
|
||||
|
|
@ -177,6 +179,7 @@ var _item_effects: PlayerItemEffectsType
|
|||
var _main_shop_buyer: FishBuyerProfileType
|
||||
var _shop_interaction: ShopInteractionType
|
||||
var _surface_drawing: NetworkSurfaceDrawingService
|
||||
var _surface_drawing_hotbar_selected: bool = false
|
||||
var _experience: PlayerExperienceType
|
||||
var _experience_award_queue: Array[Dictionary] = []
|
||||
var _experience_animation_active: bool = false
|
||||
|
|
@ -293,6 +296,7 @@ func setup(
|
|||
_network_profile = network_profile
|
||||
_spawn_service = spawn_service
|
||||
_bag = bag
|
||||
_hotbar = hotbar
|
||||
_item_catalog = item_catalog
|
||||
_settings_manager = settings_manager
|
||||
_fishing_spot = fishing_spot
|
||||
|
|
@ -448,16 +452,7 @@ func _input(event: InputEvent) -> void:
|
|||
)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var drawing_can_open: bool = (
|
||||
_gameplay_ui_enabled
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
and not _chat_input_open
|
||||
and not _showcase_active
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_use_surface_drawing()
|
||||
)
|
||||
var drawing_can_open: bool = _can_surface_drawing_be_active()
|
||||
if (
|
||||
_surface_drawing != null
|
||||
and _surface_drawing.handle_input(
|
||||
|
|
@ -630,7 +625,7 @@ func _on_quick_action_selected(action_id: StringName) -> void:
|
|||
&"online":
|
||||
_player_menu.open_section(PlayerMenuType.Section.PLAYERS)
|
||||
&"paint":
|
||||
_toggle_surface_drawing()
|
||||
_select_art_kit_hotbar_slot()
|
||||
&"chat":
|
||||
_chat_ui.open_chat()
|
||||
&"freecam":
|
||||
|
|
@ -1121,25 +1116,46 @@ static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
|
|||
return Rect2(cursor_margin, bounds_size)
|
||||
|
||||
|
||||
func _toggle_surface_drawing() -> void:
|
||||
if _surface_drawing == null:
|
||||
func _select_art_kit_hotbar_slot() -> void:
|
||||
if _hotbar == null:
|
||||
return
|
||||
if _surface_drawing.is_active():
|
||||
_surface_drawing.deactivate()
|
||||
elif _surface_drawing.can_activate():
|
||||
_surface_drawing.activate(_drawing_pointer_window_position())
|
||||
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
|
||||
if _hotbar.get_item_id(slot_index) == ART_KIT_ITEM_ID:
|
||||
_hotbar.select_slot(slot_index)
|
||||
return
|
||||
|
||||
|
||||
func set_surface_drawing_hotbar_selected(is_selected: bool) -> void:
|
||||
_surface_drawing_hotbar_selected = is_selected
|
||||
_refresh_surface_drawing_activation()
|
||||
|
||||
|
||||
func _refresh_surface_drawing_activation() -> void:
|
||||
if _surface_drawing == null:
|
||||
return
|
||||
if is_selected:
|
||||
if not _surface_drawing.is_active() and _surface_drawing.can_activate():
|
||||
_surface_drawing.activate(_drawing_pointer_window_position())
|
||||
elif _surface_drawing.is_active():
|
||||
var should_be_active: bool = _can_surface_drawing_be_active()
|
||||
if should_be_active and not _surface_drawing.is_active():
|
||||
_surface_drawing.activate(_drawing_pointer_window_position())
|
||||
elif not should_be_active and _surface_drawing.is_active():
|
||||
_surface_drawing.deactivate()
|
||||
|
||||
|
||||
func _can_surface_drawing_be_active() -> bool:
|
||||
return (
|
||||
_surface_drawing_hotbar_selected
|
||||
and _gameplay_ui_enabled
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
and not _chat_input_open
|
||||
and not _showcase_active
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_use_surface_drawing()
|
||||
and _surface_drawing != null
|
||||
and _surface_drawing.can_activate()
|
||||
)
|
||||
|
||||
|
||||
func _drawing_pointer_window_position() -> Vector2:
|
||||
if _virtual_mouse_active:
|
||||
return _virtual_mouse_window_position
|
||||
|
|
@ -1462,6 +1478,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
|||
_hotbar_ui.set_gameplay_input_enabled(true)
|
||||
_refresh_fishing_panel_visibility()
|
||||
call_deferred("_start_next_experience_animation")
|
||||
_refresh_surface_drawing_activation()
|
||||
|
||||
|
||||
func set_gameplay_hud_hidden(hidden: bool) -> void:
|
||||
|
|
@ -1505,8 +1522,7 @@ func _refresh_gameplay_hud_visibility() -> void:
|
|||
|
||||
func set_system_menu_open(is_open: bool) -> void:
|
||||
_system_menu_open = is_open
|
||||
if is_open and _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_refresh_surface_drawing_activation()
|
||||
_refresh_gameplay_hud_visibility()
|
||||
_refresh_chat_availability()
|
||||
_refresh_hotbar_visibility()
|
||||
|
|
@ -1862,6 +1878,7 @@ func _on_showcase_changed(
|
|||
showcase_visible: bool,
|
||||
) -> void:
|
||||
_showcase_active = showcase_visible
|
||||
_refresh_surface_drawing_activation()
|
||||
if not showcase_visible:
|
||||
_set_fishing_panel_showcase_position(false)
|
||||
_showcase_details.text = ""
|
||||
|
|
@ -2103,8 +2120,7 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
|||
_player_menu_open = is_open
|
||||
if is_open:
|
||||
_end_virtual_mouse()
|
||||
if is_open and _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_refresh_surface_drawing_activation()
|
||||
_refresh_gameplay_hud_visibility()
|
||||
if is_open:
|
||||
_hotbar_ui.set_drag_enabled(false)
|
||||
|
|
@ -2156,8 +2172,7 @@ func _on_shop_exit_started() -> void:
|
|||
|
||||
func _on_shop_visibility_changed(is_open: bool) -> void:
|
||||
_shop_open = is_open
|
||||
if is_open and _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_refresh_surface_drawing_activation()
|
||||
if is_open:
|
||||
shop_backdrop_visibility_changed.emit(true)
|
||||
if not is_open and _player_menu.is_shop_cooler_mounted():
|
||||
|
|
@ -2309,8 +2324,7 @@ func _emit_interactive_pointer_ui_changed() -> void:
|
|||
|
||||
func _on_chat_text_entry_ownership_changed(active: bool) -> void:
|
||||
_chat_input_open = active
|
||||
if active and _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_refresh_surface_drawing_activation()
|
||||
_refresh_chat_availability()
|
||||
_emit_interactive_pointer_ui_changed()
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ const InventoryNotepadType = preload(
|
|||
const LogbookPortraitType = preload(
|
||||
"res://ui/components/logbook_portrait.gd"
|
||||
)
|
||||
const CurrencyPresentationType = preload(
|
||||
"res://ui/currency_presentation.gd"
|
||||
)
|
||||
|
||||
const DETAIL_FADE_DURATION: float = 0.12
|
||||
const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION
|
||||
|
|
@ -756,19 +759,17 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
_add_detail_row(left_stats, "number caught", "unknown")
|
||||
_add_detail_row(right_stats, "rarity", fish.get_rarity_name().to_lower())
|
||||
_add_detail_row(right_stats, "time of day", _availability_text(fish))
|
||||
_add_detail_row(
|
||||
_add_currency_detail_row(
|
||||
right_stats,
|
||||
"value range",
|
||||
"%d–%d fish coin" % [
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_min,
|
||||
FishQualityType.Tier.BORING,
|
||||
),
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_max,
|
||||
FishQualityType.Tier.SHINY,
|
||||
),
|
||||
],
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_min,
|
||||
FishQualityType.Tier.BORING,
|
||||
),
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_max,
|
||||
FishQualityType.Tier.SHINY,
|
||||
),
|
||||
)
|
||||
_add_detail_row(
|
||||
right_stats,
|
||||
|
|
@ -878,6 +879,31 @@ func _add_detail_row(
|
|||
parent.add_child(row)
|
||||
|
||||
|
||||
func _add_currency_detail_row(
|
||||
parent: VBoxContainer,
|
||||
row_name: String,
|
||||
minimum_value: int,
|
||||
maximum_value: int,
|
||||
) -> void:
|
||||
var row := VBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 2)
|
||||
var heading := _field_label(row_name, 14)
|
||||
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
heading.add_theme_color_override("font_color", MUTED_INK)
|
||||
row.add_child(heading)
|
||||
var value: CurrencyAmount = (
|
||||
CurrencyPresentationType.instantiate_amount(minimum_value, 16.0)
|
||||
)
|
||||
value.set_amount_text("%d–%d" % [minimum_value, maximum_value])
|
||||
value.get_amount_label().add_theme_font_override(
|
||||
"font", InventoryNotepadType.HANDWRITTEN_FONT
|
||||
)
|
||||
value.get_amount_label().add_theme_font_size_override("font_size", 16)
|
||||
value.get_amount_label().add_theme_color_override("font_color", INK)
|
||||
row.add_child(value)
|
||||
parent.add_child(row)
|
||||
|
||||
|
||||
func _make_stats_column() -> VBoxContainer:
|
||||
var column := VBoxContainer.new()
|
||||
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
|
|
|||
|
|
@ -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,14 +2408,13 @@ func _update_shell_layout() -> void:
|
|||
_notepad_capacity_value.size = (
|
||||
Vector2(142.0, 17.0) if compact else Vector2(124.0, 23.0)
|
||||
)
|
||||
for status_value: Label in [
|
||||
_notepad_wallet_value,
|
||||
_notepad_capacity_value,
|
||||
]:
|
||||
status_value.add_theme_font_size_override(
|
||||
"font_size",
|
||||
10 if compact else 14,
|
||||
)
|
||||
_notepad_wallet_value.icon_size = 10.0 if compact else 14.0
|
||||
_notepad_wallet_value.get_amount_label().add_theme_font_size_override(
|
||||
"font_size", 10 if compact else 14
|
||||
)
|
||||
_notepad_capacity_value.add_theme_font_size_override(
|
||||
"font_size", 10 if compact else 14
|
||||
)
|
||||
_cooler_detail_texture.position = (
|
||||
Vector2(5.0, 84.0) if compact else Vector2(80.0, 137.0)
|
||||
)
|
||||
|
|
@ -2594,22 +2596,21 @@ func _layout_cooler_detail_text(compact: bool) -> void:
|
|||
"font_size",
|
||||
13 if compact else 18,
|
||||
)
|
||||
_cooler_offer_value.add_theme_font_size_override(
|
||||
"font_size",
|
||||
10 if compact else 15,
|
||||
_cooler_offer_value.icon_size = 10.0 if compact else 15.0
|
||||
_cooler_offer_value.get_amount_label().add_theme_font_size_override(
|
||||
"font_size", 10 if compact else 15
|
||||
)
|
||||
|
||||
|
||||
func _layout_cooler_selection_text(compact: bool) -> void:
|
||||
var row_height: float = 17.5 if compact else 20.0
|
||||
for numeric_label: Label in [
|
||||
_cooler_selected_count_value,
|
||||
_cooler_combined_offer_value,
|
||||
]:
|
||||
numeric_label.add_theme_font_size_override(
|
||||
"font_size",
|
||||
10 if compact else 14,
|
||||
)
|
||||
_cooler_selected_count_value.add_theme_font_size_override(
|
||||
"font_size", 10 if compact else 14
|
||||
)
|
||||
_cooler_combined_offer_value.icon_size = 10.0 if compact else 14.0
|
||||
_cooler_combined_offer_value.get_amount_label().add_theme_font_size_override(
|
||||
"font_size", 10 if compact else 14
|
||||
)
|
||||
for word_label: Label in [
|
||||
_cooler_selection_empty,
|
||||
_cooler_selected_count_label,
|
||||
|
|
@ -3438,9 +3439,9 @@ func _refresh_economy_summary() -> void:
|
|||
if _inventory != null
|
||||
else 0
|
||||
)
|
||||
_wallet_balance.text = "wallet: $%d" % balance
|
||||
_wallet_status.set_content("wallet", "$%d" % balance)
|
||||
_notepad_wallet_value.text = "$%d" % balance
|
||||
_wallet_balance.set_amount(balance)
|
||||
_wallet_status.set_currency_amount("wallet", balance)
|
||||
_notepad_wallet_value.set_amount(balance)
|
||||
_capacity_status.set_content("cooler", "%d / %d" % [
|
||||
_inventory.get_all_catches().size() if _inventory != null else 0,
|
||||
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
|
||||
|
|
@ -3449,8 +3450,8 @@ func _refresh_economy_summary() -> void:
|
|||
_inventory.get_all_catches().size() if _inventory != null else 0,
|
||||
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
|
||||
]
|
||||
_held_value_status.set_content("held value", "$%d" % held_total)
|
||||
_held_value.text = "held fish base value: $%d" % held_total
|
||||
_held_value_status.set_currency_amount("held value", held_total)
|
||||
_held_value.set_amount(held_total)
|
||||
_cooler_count.text = "%d / %d" % [
|
||||
_inventory.get_all_catches().size() if _inventory != null else 0,
|
||||
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
|
||||
|
|
@ -3707,7 +3708,7 @@ func _on_catch_card_pressed(catch_id: StringName) -> void:
|
|||
Input.is_key_pressed(KEY_CTRL),
|
||||
Input.is_key_pressed(KEY_SHIFT)
|
||||
)
|
||||
_transaction_feedback.text = ""
|
||||
_set_transaction_feedback("")
|
||||
_refresh_inventory()
|
||||
|
||||
|
||||
|
|
@ -3719,7 +3720,7 @@ func _on_fish_field_gui_input(event: InputEvent) -> void:
|
|||
and not get_viewport().gui_is_dragging()
|
||||
):
|
||||
_fish_selection.clear()
|
||||
_transaction_feedback.text = ""
|
||||
_set_transaction_feedback("")
|
||||
_refresh_inventory()
|
||||
|
||||
|
||||
|
|
@ -3751,9 +3752,9 @@ func _clear_cooler_detail_stats() -> void:
|
|||
_cooler_weight_value,
|
||||
_cooler_weight_unit,
|
||||
_cooler_offer_label,
|
||||
_cooler_offer_value,
|
||||
]:
|
||||
label.text = ""
|
||||
_cooler_offer_value.visible = false
|
||||
|
||||
|
||||
func _set_cooler_selection_summary_empty() -> void:
|
||||
|
|
@ -3764,10 +3765,10 @@ func _set_cooler_selection_summary_empty() -> void:
|
|||
_cooler_selected_count_value,
|
||||
_cooler_selected_count_label,
|
||||
_cooler_combined_offer_label,
|
||||
_cooler_combined_offer_value,
|
||||
]:
|
||||
label.visible = false
|
||||
label.text = ""
|
||||
_cooler_combined_offer_value.visible = false
|
||||
|
||||
|
||||
func _set_cooler_selection_summary(
|
||||
|
|
@ -3785,10 +3786,9 @@ func _set_cooler_selection_summary(
|
|||
_cooler_selected_count_label.text = "fish selected"
|
||||
if offer_value >= 0:
|
||||
_cooler_combined_offer_label.text = "combined offer"
|
||||
_cooler_combined_offer_value.text = "$%d" % offer_value
|
||||
_cooler_combined_offer_value.set_amount(offer_value)
|
||||
else:
|
||||
_cooler_combined_offer_label.text = "offer unavailable"
|
||||
_cooler_combined_offer_value.text = ""
|
||||
|
||||
|
||||
func _update_inventory_detail(fish_catch: FishCatchType) -> void:
|
||||
|
|
@ -3838,10 +3838,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
|
|||
]
|
||||
if buyer_offer >= 0 and active_buyer != null:
|
||||
_cooler_offer_label.text = _get_offer_label(active_buyer)
|
||||
_cooler_offer_value.text = "$%d" % buyer_offer
|
||||
_cooler_offer_value.visible = true
|
||||
_cooler_offer_value.set_amount(buyer_offer)
|
||||
else:
|
||||
_cooler_offer_label.text = "buyer unavailable"
|
||||
_cooler_offer_value.text = ""
|
||||
_cooler_offer_value.visible = false
|
||||
_favorite_button.disabled = false
|
||||
_favorite_button.text = (
|
||||
"unfavorite" if fish_catch.is_favorited else "favorite"
|
||||
|
|
@ -3970,12 +3971,8 @@ func _update_sale_summary() -> void:
|
|||
or preview.status == FishSaleResultType.Status.FAVORITED
|
||||
)
|
||||
):
|
||||
_selection_summary.text += "\n%s total offer: $%d" % [
|
||||
active_buyer.display_name,
|
||||
preview.payout,
|
||||
]
|
||||
_set_cooler_selection_summary(selected_count, preview.payout)
|
||||
_offer_status.set_content(offer_label, "$%d" % preview.payout)
|
||||
_offer_status.set_currency_amount(offer_label, preview.payout)
|
||||
else:
|
||||
_set_cooler_selection_summary(selected_count, -1)
|
||||
_offer_status.set_content(offer_label, "unavailable")
|
||||
|
|
@ -4058,7 +4055,7 @@ func _on_favorite_pressed() -> void:
|
|||
fish_catch.catch_id,
|
||||
not fish_catch.is_favorited
|
||||
):
|
||||
_transaction_feedback.text = (
|
||||
_set_transaction_feedback(
|
||||
"%s %s."
|
||||
% [
|
||||
fish_catch.fish.display_name,
|
||||
|
|
@ -4074,7 +4071,7 @@ func _on_sell_pressed() -> void:
|
|||
func _on_sell_all_pressed() -> void:
|
||||
var catch_ids: Array[StringName] = _get_sell_all_catch_ids()
|
||||
if catch_ids.is_empty():
|
||||
_transaction_feedback.text = "No sellable fish in the cooler."
|
||||
_set_transaction_feedback("No sellable fish in the cooler.")
|
||||
return
|
||||
_begin_sale_confirmation(catch_ids)
|
||||
|
||||
|
|
@ -4091,14 +4088,14 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
|
|||
and _network_sale_service.is_local_sale_pending()
|
||||
)
|
||||
):
|
||||
_transaction_feedback.text = "Selling…"
|
||||
_set_transaction_feedback("Selling…")
|
||||
return
|
||||
if (
|
||||
_network_sale_service == null
|
||||
or buyer_id.is_empty()
|
||||
or not _network_sale_service.can_request_sale(buyer_id)
|
||||
):
|
||||
_transaction_feedback.text = (
|
||||
_set_transaction_feedback(
|
||||
"Selling is not supported by this server."
|
||||
)
|
||||
return
|
||||
|
|
@ -4114,7 +4111,7 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
|
|||
active_buyer
|
||||
)
|
||||
if not preview.is_success():
|
||||
_transaction_feedback.text = (
|
||||
_set_transaction_feedback(
|
||||
"favorited fish cannot be sold. "
|
||||
+ "remove them from the selection first."
|
||||
if preview.status == FishSaleResultType.Status.FAVORITED
|
||||
|
|
@ -4128,15 +4125,15 @@ func _begin_sale_confirmation(catch_ids: Array[StringName]) -> void:
|
|||
_confirmation_generation = _menu_generation
|
||||
var fish_label: String = "fish"
|
||||
_confirmation_message.text = (
|
||||
"sell %d %s to the %s for $%d?\ncombined base value: $%d"
|
||||
% [
|
||||
preview.fish_count,
|
||||
fish_label,
|
||||
_get_buyer_display_group(active_buyer),
|
||||
preview.payout,
|
||||
preview.base_value,
|
||||
]
|
||||
)
|
||||
"[center]sell %d %s to the %s for %s?\n"
|
||||
+ "combined base value: %s[/center]"
|
||||
) % [
|
||||
preview.fish_count,
|
||||
fish_label,
|
||||
_get_buyer_display_group(active_buyer),
|
||||
CurrencyPresentationType.bbcode_amount(preview.payout, 22),
|
||||
CurrencyPresentationType.bbcode_amount(preview.base_value, 22),
|
||||
]
|
||||
_sale_confirmation.visible = true
|
||||
if _shop_cooler_context_active:
|
||||
shop_cooler_modal_changed.emit(true)
|
||||
|
|
@ -4182,7 +4179,7 @@ func _on_confirm_sale_pressed() -> void:
|
|||
and transaction_generation == _menu_generation
|
||||
and (visible or _shop_cooler_context_active)
|
||||
):
|
||||
_transaction_feedback.text = "Selling…"
|
||||
_set_transaction_feedback("Selling…")
|
||||
|
||||
|
||||
func _can_use_shared_world_actions() -> bool:
|
||||
|
|
@ -4195,7 +4192,7 @@ func _can_use_shared_world_actions() -> bool:
|
|||
func _on_network_sale_pending(_request_id: String) -> void:
|
||||
_sale_in_progress = true
|
||||
if visible or _shop_cooler_context_active:
|
||||
_transaction_feedback.text = "Selling…"
|
||||
_set_transaction_feedback("Selling…")
|
||||
_update_sale_summary()
|
||||
|
||||
|
||||
|
|
@ -4213,11 +4210,12 @@ func _on_network_sale_finished(
|
|||
return
|
||||
_refresh_all()
|
||||
var feedback_message: String = (
|
||||
"Sale complete. $%d received." % payout
|
||||
"Sale complete • %s received."
|
||||
% CurrencyPresentationType.bbcode_amount(payout, 20)
|
||||
if accepted
|
||||
else message
|
||||
)
|
||||
_transaction_feedback.text = feedback_message
|
||||
_set_transaction_feedback(feedback_message)
|
||||
if not accepted:
|
||||
_sale_unavailable.text = feedback_message
|
||||
_sale_unavailable.visible = true
|
||||
|
|
@ -4273,7 +4271,7 @@ func _revalidate_confirmation() -> void:
|
|||
or _confirmation_buyer.id != _confirmation_buyer_id
|
||||
):
|
||||
_close_sale_confirmation()
|
||||
_transaction_feedback.text = "sale selection is no longer available."
|
||||
_set_transaction_feedback("sale selection is no longer available.")
|
||||
return
|
||||
var preview: FishSaleResultType = _sale_service.preview_batch(
|
||||
_confirmation_catch_ids,
|
||||
|
|
@ -4288,7 +4286,7 @@ func _revalidate_confirmation() -> void:
|
|||
)
|
||||
):
|
||||
_close_sale_confirmation()
|
||||
_transaction_feedback.text = (
|
||||
_set_transaction_feedback(
|
||||
"favorited fish cannot be sold. "
|
||||
+ "remove them from the selection first."
|
||||
if preview.status == FishSaleResultType.Status.FAVORITED
|
||||
|
|
@ -4308,6 +4306,10 @@ func _get_buyer_display_group(
|
|||
return "buyer"
|
||||
|
||||
|
||||
func _set_transaction_feedback(message: String) -> void:
|
||||
_transaction_feedback.text = "[center]%s[/center]" % message
|
||||
|
||||
|
||||
func _get_active_sale_buyer() -> FishBuyerProfileType:
|
||||
if _sale_buyer_override != null and _sale_buyer_override.is_valid():
|
||||
return _sale_buyer_override
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ class_name SurfaceDrawingToolbar
|
|||
extends Control
|
||||
|
||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
const TOOLBAR_WIDTH: float = 580.0
|
||||
const MARKER_MODE_ICON: Texture2D = preload(
|
||||
"res://items/icons/art/art_kit_marker.png"
|
||||
)
|
||||
const GRID_MODE_ICON: Texture2D = preload(
|
||||
"res://items/icons/art/art_kit_grid_light.png"
|
||||
)
|
||||
const TOOLBAR_WIDTH: float = 510.0
|
||||
const TOOLBAR_HEIGHT: float = 373.0
|
||||
const COLOR_RAIL_WIDTH: float = 46.0
|
||||
|
||||
|
|
@ -17,7 +23,6 @@ const COLOR_RAIL_WIDTH: float = 46.0
|
|||
@onready var _hide_guide_button: Button = %HideGuideButton
|
||||
@onready var _restore_guide_button: Button = %RestoreGuideButton
|
||||
@onready var _finalize_guide_button: Button = %FinalizeGuideButton
|
||||
@onready var _close_button: Button = %CloseButton
|
||||
|
||||
var _service: NetworkSurfaceDrawingService
|
||||
var _unlocks: PlayerArtUnlocks
|
||||
|
|
@ -38,6 +43,9 @@ func _ready() -> void:
|
|||
_make_button_round(button, 22)
|
||||
_mode_button.custom_minimum_size = Vector2(48, 48)
|
||||
_make_button_round(_mode_button, 24)
|
||||
_enlarge_action_icon(_mode_button)
|
||||
for button: Button in _action_buttons():
|
||||
_enlarge_action_icon(button)
|
||||
_mode_button.pressed.connect(_toggle_mode)
|
||||
_eraser_button.pressed.connect(_toggle_eraser)
|
||||
_undo_button.pressed.connect(_undo_last_stroke)
|
||||
|
|
@ -50,7 +58,6 @@ func _ready() -> void:
|
|||
_finalize_guide_button.pressed.connect(
|
||||
_arm_guide_action.bind(NetworkSurfaceDrawingService.GuideAction.FINALIZE)
|
||||
)
|
||||
_close_button.pressed.connect(_close_toolbar)
|
||||
_brush_option.item_selected.connect(_select_brush)
|
||||
_grid_option.item_selected.connect(_select_grid)
|
||||
_build_options()
|
||||
|
|
@ -75,7 +82,6 @@ func _action_buttons() -> Array[Button]:
|
|||
_hide_guide_button,
|
||||
_restore_guide_button,
|
||||
_finalize_guide_button,
|
||||
_close_button,
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -91,6 +97,25 @@ func _make_button_round(button: Button, radius: int) -> void:
|
|||
button.add_theme_stylebox_override(style_name, style)
|
||||
|
||||
|
||||
func _enlarge_action_icon(button: Button) -> void:
|
||||
# The shared ocean button uses generous text padding, leaving only a
|
||||
# 20-pixel icon area in these compact bubbles. Keep the 48x44 control and
|
||||
# hitbox intact while giving icon-only actions a full 40x40 presentation.
|
||||
button.add_theme_constant_override("icon_max_width", 40)
|
||||
for style_name: StringName in [
|
||||
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
||||
]:
|
||||
var existing: StyleBox = button.get_theme_stylebox(style_name)
|
||||
var style := existing.duplicate() as StyleBoxFlat
|
||||
if style == null:
|
||||
continue
|
||||
style.content_margin_left = 4.0
|
||||
style.content_margin_right = 4.0
|
||||
style.content_margin_top = 2.0
|
||||
style.content_margin_bottom = 2.0
|
||||
button.add_theme_stylebox_override(style_name, style)
|
||||
|
||||
|
||||
func setup(
|
||||
service: NetworkSurfaceDrawingService,
|
||||
unlocks: PlayerArtUnlocks,
|
||||
|
|
@ -125,6 +150,8 @@ func _apply_dock_side() -> void:
|
|||
offset_right = 0.0 if _dock_right else TOOLBAR_WIDTH
|
||||
offset_top = 0.0
|
||||
offset_bottom = TOOLBAR_HEIGHT
|
||||
_top_panel.offset_left = 0.0
|
||||
_top_panel.offset_right = TOOLBAR_WIDTH
|
||||
_color_panel.offset_left = (
|
||||
TOOLBAR_WIDTH - COLOR_RAIL_WIDTH if _dock_right else 0.0
|
||||
)
|
||||
|
|
@ -255,11 +282,6 @@ func _arm_guide_action(action: int) -> void:
|
|||
_service.arm_guide_action(action)
|
||||
|
||||
|
||||
func _close_toolbar() -> void:
|
||||
if _service != null:
|
||||
_service.deactivate()
|
||||
|
||||
|
||||
func _on_unlocks_changed(_unlock_mask: int) -> void:
|
||||
_refresh_unlocks()
|
||||
|
||||
|
|
@ -276,7 +298,14 @@ func _on_service_state_changed(
|
|||
visible = is_active
|
||||
if not is_active:
|
||||
return
|
||||
_mode_button.text = "▦" if mode_name == "place grid" else "●"
|
||||
_mode_button.icon = (
|
||||
GRID_MODE_ICON if mode_name == "place grid" else MARKER_MODE_ICON
|
||||
)
|
||||
_mode_button.accessibility_name = (
|
||||
"switch to marker mode"
|
||||
if mode_name == "place grid"
|
||||
else "switch to grid mode"
|
||||
)
|
||||
_mode_button.tooltip_text = (
|
||||
"Switch to marker mode"
|
||||
if mode_name == "place grid"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
[gd_scene load_steps=6 format=3]
|
||||
[gd_scene load_steps=11 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/undo_light.png" id="3_undo_light"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/check_mark_light.png" id="4_check_mark_light"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="5_x_light"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_eraser.png" id="4_art_kit_eraser"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_marker.png" id="5_art_kit_marker"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_light.png" id="6_grid_light"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_hidden_light.png" id="7_grid_hidden"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_restore_light.png" id="8_grid_restore"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit_grid_finish_light.png" id="9_grid_finish"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="GuideActionGroup"]
|
||||
allow_unpress = true
|
||||
|
||||
[node name="SurfaceDrawingToolbar" type="Control"]
|
||||
visible = false
|
||||
|
|
@ -13,7 +20,7 @@ layout_mode = 0
|
|||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -580.0
|
||||
offset_left = -510.0
|
||||
offset_bottom = 373.0
|
||||
grow_horizontal = 0
|
||||
mouse_filter = 2
|
||||
|
|
@ -23,19 +30,13 @@ script = ExtResource("1_script")
|
|||
[node name="TopPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_right = 580.0
|
||||
offset_right = 510.0
|
||||
offset_bottom = 60.0
|
||||
|
||||
[node name="Top" type="HBoxContainer" parent="TopPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="ModeButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "switch between grid and marker mode"
|
||||
text = "▦"
|
||||
|
||||
[node name="BrushOption" type="OptionButton" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(80, 48)
|
||||
|
|
@ -51,9 +52,12 @@ tooltip_text = "grid size"
|
|||
[node name="EraserButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
texture_filter = 1
|
||||
tooltip_text = "toggle eraser"
|
||||
toggle_mode = true
|
||||
text = "⌫"
|
||||
accessibility_name = "toggle eraser"
|
||||
icon = ExtResource("4_art_kit_eraser")
|
||||
expand_icon = true
|
||||
|
||||
[node name="UndoButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
|
|
@ -67,42 +71,51 @@ expand_icon = true
|
|||
[node name="HideGuideButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "hide grid on next click"
|
||||
texture_filter = 1
|
||||
tooltip_text = "toggle hide grid mode"
|
||||
toggle_mode = true
|
||||
text = "◌"
|
||||
button_group = SubResource("GuideActionGroup")
|
||||
accessibility_name = "toggle hide grid mode"
|
||||
icon = ExtResource("7_grid_hidden")
|
||||
expand_icon = true
|
||||
|
||||
[node name="RestoreGuideButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
tooltip_text = "restore hidden grid on next click"
|
||||
texture_filter = 1
|
||||
tooltip_text = "toggle show grid mode"
|
||||
toggle_mode = true
|
||||
text = "▤"
|
||||
button_group = SubResource("GuideActionGroup")
|
||||
accessibility_name = "toggle show grid mode"
|
||||
icon = ExtResource("8_grid_restore")
|
||||
expand_icon = true
|
||||
|
||||
[node name="FinalizeGuideButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
texture_filter = 1
|
||||
tooltip_text = "finish grid on next click"
|
||||
tooltip_text = "toggle finalize grid mode"
|
||||
toggle_mode = true
|
||||
accessibility_name = "finish grid on next click"
|
||||
icon = ExtResource("4_check_mark_light")
|
||||
button_group = SubResource("GuideActionGroup")
|
||||
accessibility_name = "toggle finalize grid mode"
|
||||
icon = ExtResource("9_grid_finish")
|
||||
expand_icon = true
|
||||
|
||||
[node name="CloseButton" type="Button" parent="TopPanel/Top"]
|
||||
[node name="ModeButton" type="Button" parent="TopPanel/Top"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
texture_filter = 1
|
||||
tooltip_text = "close art tools"
|
||||
accessibility_name = "close art tools"
|
||||
icon = ExtResource("5_x_light")
|
||||
tooltip_text = "switch between grid and marker mode"
|
||||
accessibility_name = "switch to grid mode"
|
||||
icon = ExtResource("5_art_kit_marker")
|
||||
expand_icon = true
|
||||
|
||||
[node name="ColorPanel" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
offset_left = 534.0
|
||||
offset_left = 464.0
|
||||
offset_top = 54.0
|
||||
offset_right = 580.0
|
||||
offset_right = 510.0
|
||||
offset_bottom = 373.0
|
||||
|
||||
[node name="ColorList" type="VBoxContainer" parent="ColorPanel"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||