feat: derive resource visuals from state

This commit is contained in:
Rijad Zuzo
2026-07-17 18:39:14 +02:00
parent 04c5371b6b
commit 2719b0398b
21 changed files with 686 additions and 18 deletions
+10 -7
View File
@@ -787,16 +787,19 @@ Completed:
separate terrain-snapped approach points. Matched captures and a focused separate terrain-snapped approach points. Matched captures and a focused
scenario prove three-batch visual cost, nested discovery, and a forced scenario prove three-batch visual cost, nested discovery, and a forced
query beyond the first 24 m range without changing the 18-resource state. query beyond the first 24 m range without changing the 18-resource state.
38. `Jajce Resource States 05`: a shared amount-derived presentation contract
now gives that berry and tree honest full, low, and depleted states. Real
player berry harvests thin fruit and leaves; real NPC wood gathers thin the
crown and leave a stump. Low and depleted saves restore the same visuals
using only `amount_remaining`, with matched Metal comparison frames.
Next: Next:
1. Give the authored berry/tree resources state-driven low/depleted visuals 1. Move the existing `tree_forest_grove_01` and `tree_forest_grove_02` anchors
sourced from their real `ResourceStateRecord` amount. Prove NPC and player into one bounded forest-edge authoring cluster. Reuse the proven tree amount
extraction plus save/restore rebuild the same presentation without adding presentation, preserve both stable IDs, and prove reachable expanding
visual fields to simulation state. discovery plus depletion/restore without serializing decorative foliage.
2. Apply the workflow to a second forest-edge consumer only after that contract 2. Add livestock only with a real identity and interaction contract.
is honest and reusable. Add livestock only with a real identity and
interaction contract.
Do not start with GIS data, a full city, a large asset pack, or more NPC Do not start with GIS data, a full city, a large asset pack, or more NPC
mechanics. The next proof is a beautiful stage for the systems that already mechanics. The next proof is a beautiful stage for the systems that already
+20 -8
View File
@@ -904,17 +904,29 @@ of eighteen resources, sit above the carved channel, and expose separate
terrain-snapped approach points. A forced village query expands beyond the terrain-snapped approach points. A forced village query expands beyond the
initial 24 m range and selects the nested berry exactly by ID. initial 24 m range and selects the nested berry exactly by ID.
The immediate next slice should make those two resource-specific visuals The first amount-derived resource presentation slice is complete. A shared
reflect authoritative amount/depletion: thin the berry patch and leave a stump `ResourceAmountVisual` contract reads the loaded `ResourceNode`'s authoritative
or equivalent readable tree state after real NPC/player extraction, then prove amount and derives full, low, or depleted presentation without entering saved
save/restore rebuilds the same presentation without persisting visual-only state. Real player berry harvests remove fruit and leaf mass; real NPC wood
state. Only after both the berry and tree consume that contract should shared gathers thin the crown and ultimately leave a warm-cut stump. Restoring both
cluster/depletion tooling be extracted or the workflow copied to a forest low and depleted snapshots rebinds the same nodes and reconstructs the same
edge. Do not generalize the resource grid to people/buildings/events or visuals from `ResourceStateRecord.amount_remaining` alone.
introduce active/abstract LOD before those consumers need it.
The immediate next slice should apply that proven contract to the existing
`tree_forest_grove_01` and `tree_forest_grove_02` anchors as one bounded
forest-edge authoring consumer. Preserve both stable IDs, keep decorative
canopy/understory outside simulation state, and prove reachable expanding
discovery plus depletion/restore with the shared tree presentation. Do not
generalize the resource grid to people/buildings/events or introduce
active/abstract LOD before those consumers need it.
Recently completed: Recently completed:
- `Jajce Resource States 05`: the authored river berry/tree now derive full,
low, and depleted visuals from authoritative amounts. Player and NPC
extraction drive the changes, save/restore reconstructs low and depleted
states without visual save fields, and matched Metal frames record the berry
thinning, sparse crown, fruitless remnant, and stump.
- `Jajce Resource Grove 04`: two existing river resources now live as nested - `Jajce Resource Grove 04`: two existing river resources now live as nested
stable-ID anchors inside a restrained decorative bank cluster. Twenty-eight stable-ID anchors inside a restrained decorative bank cluster. Twenty-eight
understory instances use three MultiMesh batches, bespoke berry/tree visuals understory instances use three MultiMesh batches, bespoke berry/tree visuals
@@ -0,0 +1,80 @@
# Jajce Resource States 05
Captured: 2026-07-17
## Files
- [full resources](jajce_resource_states_05_full.png)
- [low resources](jajce_resource_states_05_low.png)
- [depleted resources](jajce_resource_states_05_depleted.png)
- `world/resource_nodes/ResourceAmountVisual.gd`
- `world/jajce/HarvestableTreePresentation.tscn`
- `tests/jajce_resource_presentation_state_test.gd`
The three images use the same 1280×720 cross-river camera, daylight value,
warmup, and Metal renderer. Debug labels are hidden. The lower-left berry patch
and right-hand harvestable tree are shown at their authored full amounts, at
40%/50% respectively, and at zero.
## Amount-derived visual language
`ResourceAmountVisual` finds its owning `ResourceNode` and derives one of three
tiers from `amount_remaining / initial_amount`:
- full keeps all five berry leaf clumps, eight berries, and four tree canopies;
- low keeps three leaf clumps, four berries, and two tree canopies; and
- depleted keeps a two-leaf fruitless remnant and replaces the tree with a
low-poly stump and warm cut face.
The threshold is deliberately simple and readable: a positive source at or
below half amount is low. The berry keeps using two bounded MultiMeshes, so
thinning changes only `visible_instance_count`; it does not create or destroy
nodes during play. The tree wrapper reuses the existing wind-reactive standing
tree and toggles authored crown/stump parts.
## Authority and restore proof
`ResourceStateRecord` remains the only amount authority. `ResourceNode` now
emits its existing `amount_changed` presentation signal when authoritative
state is rebound, including restore. The visual contract listens to that signal
and also refreshes once after scene readiness, covering either bind order.
The focused scenario performs two real player harvests on
`berry_bush_river_02` and two real NPC gather completions on
`tree_river_resource_01`, checks the low presentation and NPC wood inventory,
then fully depletes both through those same production paths. It restores low
and depleted snapshots in turn and verifies the presentation is reconstructed.
The resource records contain no visual, visibility, tier, or stump fields.
The capture tool's optional amount overrides also construct and bind ordinary
`ResourceStateRecord`s. They do not directly manipulate visual children, so
the review images exercise the same runtime contract as gameplay.
## Next slice
Apply this contract to the existing `tree_forest_grove_01` and
`tree_forest_grove_02` stable IDs in one bounded forest-edge cluster. Decorative
canopy and understory should stay presentation-only while the two finite trees
remain reachable, discoverable, depletable, and restore correctly.
## Capture commands
```bash
HOME="$PWD/logs/quality/godot_profile" \
APPDATA="$PWD/logs/quality/godot_profile" \
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
/Applications/Godot.app/Contents/MacOS/Godot \
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
--scene=res://world/jajce/JajceResourceStateLookdev.tscn \
--output=res://docs/baselines/jajce_resource_states_05_full.png \
--hide-resource-labels
```
For the low frame, append:
```text
--resource-amount=berry_bush_river_02:2
--resource-amount=tree_river_resource_01:4
```
For the depleted frame, use the same arguments with both amounts set to `0`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

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

After

Width:  |  Height:  |  Size: 430 KiB

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

After

Width:  |  Height:  |  Size: 428 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://lerebtc2de5v"
path="res://.godot/imported/jajce_resource_states_05_low.png-ca9e13189ad1a8dc22e32380ddf38b2f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_resource_states_05_low.png"
dest_files=["res://.godot/imported/jajce_resource_states_05_low.png-ca9e13189ad1a8dc22e32380ddf38b2f.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
@@ -0,0 +1,211 @@
extends SceneTree
const BERRY_ID := &"berry_bush_river_02"
const TREE_ID := &"tree_river_resource_01"
const CLUSTER_PATH := "JajceWorld/WorldObjects/ResourceClusters/RiverbankResourceCluster"
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
for _frame in 3:
await physics_frame
var manager = main_scene.get_node("SimulationManager")
manager.set_process(false)
var cluster := main_scene.get_node(CLUSTER_PATH) as RiverbankResourceCluster
var berry := cluster.get_node("ResourceAnchors/BerryBush_River_02") as ResourceNode
var tree := cluster.get_node("ResourceAnchors/Tree_River_Resource_01") as ResourceNode
var berry_visual := berry.get_node("Visual/BerryPatchPresentation") as StylizedBerryPatch
var tree_visual := tree.get_node("Visual/TreePresentation") as HarvestableTreePresentation
_check_initial_presentation(berry_visual, tree_visual)
var berry_state: ResourceStateRecord = manager.get_resource_state(BERRY_ID)
var tree_state: ResourceStateRecord = manager.get_resource_state(TREE_ID)
_check(berry_state != null and tree_state != null, "Authored resources should bind state")
if berry_state == null or tree_state == null:
_finish()
return
for _action in 2:
_check(
is_equal_approx(manager.harvest_resource_node(berry), berry.yield_per_action),
"Player harvesting should extract the configured berry yield"
)
var npc: SimNPC = manager.npcs[0]
for _action in 2:
_complete_npc_gather(manager, npc, tree)
_check(
is_equal_approx(berry_state.get_amount_remaining(), 2.0),
"Two player harvests should leave the authored berry at 40 percent"
)
_check(
berry_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.LOW,
"The berry should derive its low tier from authoritative amount"
)
_check(
berry_visual.get_visible_leaf_count() == 3 and berry_visual.get_visible_berry_count() == 4,
"The low berry should visibly thin to three leaves and four berries"
)
_check(
is_equal_approx(tree_state.get_amount_remaining(), 4.0),
"Two NPC completions should leave the authored tree at half amount"
)
_check(
tree_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.LOW,
"The tree should derive its low tier from authoritative amount"
)
_check(
(
tree_visual.is_standing_visible()
and not tree_visual.is_stump_visible()
and tree_visual.get_visible_canopy_count() == 2
),
"The low tree should remain standing with a visibly thinned crown"
)
_check(
is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD), 4.0),
"The low tree state should come from real NPC inventory-producing extraction"
)
var low_snapshot: String = manager.serialize_state()
_check_no_visual_state(low_snapshot)
while berry_state.can_extract():
manager.harvest_resource_node(berry)
while tree_state.can_extract():
_complete_npc_gather(manager, npc, tree)
_check_depleted_presentation(berry_visual, tree_visual)
var depleted_snapshot: String = manager.serialize_state()
_check_no_visual_state(depleted_snapshot)
_check(
manager.restore_state_from_json(low_snapshot),
"Low authoritative resource state should restore"
)
berry_state = manager.get_resource_state(BERRY_ID)
tree_state = manager.get_resource_state(TREE_ID)
_check(
(
is_equal_approx(berry_state.get_amount_remaining(), 2.0)
and berry_visual.get_visible_berry_count() == 4
and berry_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.LOW
),
"Restore should rebuild the low berry visual from amount alone"
)
_check(
(
is_equal_approx(tree_state.get_amount_remaining(), 4.0)
and tree_visual.get_visible_canopy_count() == 2
and not tree_visual.is_stump_visible()
),
"Restore should rebuild the low standing tree from amount alone"
)
_check(
manager.restore_state_from_json(depleted_snapshot),
"Depleted authoritative resource state should restore"
)
_check_depleted_presentation(berry_visual, tree_visual)
_finish()
func _check_initial_presentation(
berry_visual: StylizedBerryPatch, tree_visual: HarvestableTreePresentation
) -> void:
_check(
(
berry_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.FULL
and berry_visual.get_visible_leaf_count() == 5
and berry_visual.get_visible_berry_count() == 8
),
"A full berry source should show all authored leaves and fruit"
)
_check(
(
tree_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.FULL
and tree_visual.is_standing_visible()
and not tree_visual.is_stump_visible()
and tree_visual.get_visible_canopy_count() == 4
),
"A full tree source should show its complete standing crown"
)
func _complete_npc_gather(manager, npc: SimNPC, resource: ResourceNode) -> void:
npc.target_id = resource.node_id
var reserved: bool = manager.reserve_resource(resource.node_id, npc.id)
_check(reserved, "NPC extraction should reserve the authored tree")
if reserved:
manager.call("_complete_resource_gather", npc, SimulationIds.ACTION_GATHER_WOOD)
func _check_depleted_presentation(
berry_visual: StylizedBerryPatch, tree_visual: HarvestableTreePresentation
) -> void:
_check(
(
berry_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.DEPLETED
and berry_visual.get_visible_leaf_count() == 2
and berry_visual.get_visible_berry_count() == 0
),
"Real player depletion should leave a small fruitless berry remnant"
)
_check(
(
tree_visual.get_amount_tier() == ResourceAmountVisual.AmountTier.DEPLETED
and not tree_visual.is_standing_visible()
and tree_visual.is_stump_visible()
and tree_visual.get_visible_canopy_count() == 0
),
"Real NPC depletion should replace the standing tree with its stump"
)
func _check_no_visual_state(snapshot: String) -> void:
var parsed = JSON.parse_string(snapshot)
_check(parsed is Dictionary, "Serialized simulation state should remain valid JSON")
if not parsed is Dictionary:
return
var matched_records := 0
for resource_data in parsed.get("resources", []):
if StringName(resource_data.get("node_id", "")) not in [BERRY_ID, TREE_ID]:
continue
matched_records += 1
for field in resource_data.keys():
var field_name := String(field)
_check(
(
not field_name.contains("visual")
and not field_name.contains("visible")
and not field_name.contains("tier")
and not field_name.contains("stump")
),
"Resource save fields should contain authority, never visual state"
)
_check(matched_records == 2, "Both authored resources should remain in simulation saves")
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
func _finish() -> void:
if failures.is_empty():
print("[TEST] Jajce resource presentation passed: amount -> low/depleted -> restore")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
@@ -0,0 +1 @@
uid://c7yc6j4js8phj
+29
View File
@@ -23,6 +23,9 @@ func _run() -> void:
var lookdev := scene.instantiate() as Node3D var lookdev := scene.instantiate() as Node3D
root.add_child(lookdev) root.add_child(lookdev)
await process_frame await process_frame
if not _apply_resource_capture_overrides():
quit(1)
return
_stabilize_lookdev(lookdev) _stabilize_lookdev(lookdev)
for _frame in WARMUP_FRAMES: for _frame in WARMUP_FRAMES:
await process_frame await process_frame
@@ -73,6 +76,32 @@ func _get_argument_value(prefix: String, default_value: String) -> String:
return default_value return default_value
func _apply_resource_capture_overrides() -> bool:
var hide_labels := OS.get_cmdline_user_args().has("--hide-resource-labels")
for resource in ResourceNode.get_all():
if hide_labels:
resource.set_debug_label_enabled(false)
for argument in OS.get_cmdline_user_args():
if not argument.begins_with("--resource-amount="):
continue
var override_text := argument.trim_prefix("--resource-amount=")
var parts := override_text.split(":", false, 1)
if parts.size() != 2 or not parts[1].is_valid_float():
push_error("Invalid resource amount override: %s" % override_text)
return false
var resource := ResourceNode.get_by_id(StringName(parts[0]))
if resource == null:
push_error("Unknown resource amount override ID: %s" % parts[0])
return false
var state := ResourceStateRecord.create_from_node(resource)
state.set_amount_remaining(parts[1].to_float())
if not resource.bind_state(state):
push_error("Could not bind capture state for resource: %s" % parts[0])
return false
return true
func _analyze_image(image: Image) -> Dictionary: func _analyze_image(image: Image) -> Dictionary:
var min_luma := INF var min_luma := INF
var max_luma := -INF var max_luma := -INF
@@ -0,0 +1,41 @@
class_name HarvestableTreePresentation
extends ResourceAmountVisual
const THINNED_CANOPIES := [&"CanopyLeft", &"CanopyRight"]
@onready var standing_tree: Node3D = $StandingTree
@onready var stump: Node3D = $Stump
func _ready() -> void:
super()
func is_standing_visible() -> bool:
return standing_tree.visible
func is_stump_visible() -> bool:
return stump.visible
func get_visible_canopy_count() -> int:
if not standing_tree.visible:
return 0
var visible_count := 0
for child in standing_tree.get_children():
if String(child.name).begins_with("Canopy") and child.visible:
visible_count += 1
return visible_count
func _apply_resource_amount_visual(
_amount_remaining: float, _remaining_ratio: float, tier: AmountTier
) -> void:
var depleted := tier == AmountTier.DEPLETED
standing_tree.visible = not depleted
stump.visible = depleted
for canopy_name in THINNED_CANOPIES:
var canopy := standing_tree.get_node_or_null(NodePath(canopy_name)) as Node3D
if canopy != null:
canopy.visible = tier == AmountTier.FULL
@@ -0,0 +1 @@
uid://cpue05wbg6roj
@@ -0,0 +1,42 @@
[gd_scene load_steps=7 format=3]
[ext_resource type="Script" path="res://world/jajce/HarvestableTreePresentation.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="2_tree"]
[sub_resource type="StandardMaterial3D" id="Material_stump_bark"]
albedo_color = Color(0.29, 0.17, 0.09, 1)
roughness = 0.96
[sub_resource type="CylinderMesh" id="Mesh_stump"]
material = SubResource("Material_stump_bark")
top_radius = 0.38
bottom_radius = 0.48
height = 0.82
radial_segments = 10
[sub_resource type="StandardMaterial3D" id="Material_stump_cut"]
albedo_color = Color(0.69, 0.49, 0.27, 1)
roughness = 0.9
[sub_resource type="CylinderMesh" id="Mesh_stump_cut"]
material = SubResource("Material_stump_cut")
top_radius = 0.36
bottom_radius = 0.37
height = 0.055
radial_segments = 10
[node name="HarvestableTreePresentation" type="Node3D"]
script = ExtResource("1_script")
[node name="StandingTree" parent="." instance=ExtResource("2_tree")]
[node name="Stump" type="Node3D" parent="."]
visible = false
[node name="Bark" type="MeshInstance3D" parent="Stump"]
position = Vector3(0, 0.41, 0)
mesh = SubResource("Mesh_stump")
[node name="CutFace" type="MeshInstance3D" parent="Stump"]
position = Vector3(0, 0.835, 0)
mesh = SubResource("Mesh_stump_cut")
@@ -0,0 +1,16 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
[node name="JajceResourceStateLookdev" type="Node3D"]
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
[node name="ResourceCamera" type="Camera3D" parent="."]
position = Vector3(14, 9, 7)
current = true
fov = 40.0
script = ExtResource("2_camera")
focus_point = Vector3(34, 3.3, 7)
animate_orbit = false
+3 -2
View File
@@ -1,10 +1,11 @@
[gd_scene load_steps=6 format=3] [gd_scene load_steps=7 format=3]
[ext_resource type="Script" path="res://world/jajce/RiverbankResourceCluster.gd" id="1_script"] [ext_resource type="Script" path="res://world/jajce/RiverbankResourceCluster.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"] [ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"] [ext_resource type="PackedScene" path="res://world/jajce/StylizedTree.tscn" id="3_tree"]
[ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"] [ext_resource type="PackedScene" path="res://world/jajce/StylizedBerryPatch.tscn" id="4_berry"]
[ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"] [ext_resource type="PackedScene" path="res://assets/foliage/tree.glb" id="5_conifer"]
[ext_resource type="PackedScene" path="res://world/jajce/HarvestableTreePresentation.tscn" id="6_harvestable_tree"]
[node name="RiverbankResourceCluster" type="Node3D"] [node name="RiverbankResourceCluster" type="Node3D"]
script = ExtResource("1_script") script = ExtResource("1_script")
@@ -66,7 +67,7 @@ discovery_priority = 0.1
[node name="MeshInstance3D" parent="ResourceAnchors/Tree_River_Resource_01/Visual"] [node name="MeshInstance3D" parent="ResourceAnchors/Tree_River_Resource_01/Visual"]
visible = false visible = false
[node name="TreePresentation" parent="ResourceAnchors/Tree_River_Resource_01/Visual" instance=ExtResource("3_tree")] [node name="TreePresentation" parent="ResourceAnchors/Tree_River_Resource_01/Visual" instance=ExtResource("6_harvestable_tree")]
scale = Vector3(1.08, 1.08, 1.08) scale = Vector3(1.08, 1.08, 1.08)
[node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"] [node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"]
+26 -1
View File
@@ -1,5 +1,5 @@
class_name StylizedBerryPatch class_name StylizedBerryPatch
extends Node3D extends ResourceAmountVisual
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader") const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
const LEAF_POSITIONS := [ const LEAF_POSITIONS := [
@@ -31,12 +31,37 @@ const BERRY_POSITIONS := [
func _ready() -> void: func _ready() -> void:
_build_leaves() _build_leaves()
_build_berries() _build_berries()
super()
func get_presentation_instance_count() -> int: func get_presentation_instance_count() -> int:
return LEAF_POSITIONS.size() + BERRY_POSITIONS.size() return LEAF_POSITIONS.size() + BERRY_POSITIONS.size()
func get_visible_leaf_count() -> int:
return $Leaves.multimesh.visible_instance_count
func get_visible_berry_count() -> int:
return $Berries.multimesh.visible_instance_count
func _apply_resource_amount_visual(
_amount_remaining: float, remaining_ratio: float, tier: AmountTier
) -> void:
var visible_leaves := LEAF_POSITIONS.size()
if tier == AmountTier.LOW:
visible_leaves = 3
elif tier == AmountTier.DEPLETED:
visible_leaves = 2
$Leaves.multimesh.visible_instance_count = visible_leaves
var visible_berries := ceili(remaining_ratio * BERRY_POSITIONS.size())
if tier == AmountTier.DEPLETED:
visible_berries = 0
$Berries.multimesh.visible_instance_count = visible_berries
func _build_leaves() -> void: func _build_leaves() -> void:
var mesh := SphereMesh.new() var mesh := SphereMesh.new()
mesh.radius = 1.0 mesh.radius = 1.0
@@ -0,0 +1,84 @@
class_name ResourceAmountVisual
extends Node3D
enum AmountTier {
FULL,
LOW,
DEPLETED,
}
@export_range(0.0, 1.0, 0.05) var low_amount_ratio := 0.5
var resource_node: ResourceNode
var amount_ratio := 1.0
var amount_tier := AmountTier.FULL
func _ready() -> void:
call_deferred("_bind_resource_node")
func _exit_tree() -> void:
if (
resource_node != null
and resource_node.amount_changed.is_connected(_on_resource_amount_changed)
):
resource_node.amount_changed.disconnect(_on_resource_amount_changed)
resource_node = null
func refresh_from_resource() -> void:
if resource_node == null:
return
_apply_amount(resource_node.get_amount_remaining())
func get_amount_tier() -> AmountTier:
return amount_tier
func get_amount_ratio() -> float:
return amount_ratio
func _bind_resource_node() -> void:
resource_node = _find_resource_node()
if resource_node == null:
return
if not resource_node.amount_changed.is_connected(_on_resource_amount_changed):
resource_node.amount_changed.connect(_on_resource_amount_changed)
refresh_from_resource()
func _find_resource_node() -> ResourceNode:
var candidate := get_parent()
while candidate != null:
if candidate is ResourceNode:
return candidate as ResourceNode
candidate = candidate.get_parent()
return null
func _on_resource_amount_changed(changed_id: StringName, amount_remaining: float) -> void:
if resource_node != null and changed_id == resource_node.node_id:
_apply_amount(amount_remaining)
func _apply_amount(amount_remaining: float) -> void:
var initial_amount := maxf(resource_node.initial_amount, 0.0)
amount_ratio = (
clampf(amount_remaining / initial_amount, 0.0, 1.0) if initial_amount > 0.0 else 0.0
)
if amount_remaining <= 0.0:
amount_tier = AmountTier.DEPLETED
elif amount_ratio <= low_amount_ratio:
amount_tier = AmountTier.LOW
else:
amount_tier = AmountTier.FULL
_apply_resource_amount_visual(amount_remaining, amount_ratio, amount_tier)
func _apply_resource_amount_visual(
_amount_remaining: float, _remaining_ratio: float, _tier: AmountTier
) -> void:
pass
@@ -0,0 +1 @@
uid://bls5qs3s3omgh
+1
View File
@@ -74,6 +74,7 @@ func bind_state(resource_state: ResourceStateRecord) -> bool:
if not state.depleted.is_connected(_on_state_depleted): if not state.depleted.is_connected(_on_state_depleted):
state.depleted.connect(_on_state_depleted) state.depleted.connect(_on_state_depleted)
_update_presentation() _update_presentation()
amount_changed.emit(node_id, state.get_amount_remaining())
_notify_world_adapters() _notify_world_adapters()
return true return true