feat: derive resource visuals from state
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user