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
|
||||
scenario prove three-batch visual cost, nested discovery, and a forced
|
||||
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:
|
||||
|
||||
1. Give the authored berry/tree resources state-driven low/depleted visuals
|
||||
sourced from their real `ResourceStateRecord` amount. Prove NPC and player
|
||||
extraction plus save/restore rebuild the same presentation without adding
|
||||
visual fields to simulation state.
|
||||
2. Apply the workflow to a second forest-edge consumer only after that contract
|
||||
is honest and reusable. Add livestock only with a real identity and
|
||||
interaction contract.
|
||||
1. Move the existing `tree_forest_grove_01` and `tree_forest_grove_02` anchors
|
||||
into one bounded forest-edge authoring cluster. Reuse the proven tree amount
|
||||
presentation, preserve both stable IDs, and prove reachable expanding
|
||||
discovery plus depletion/restore without serializing decorative foliage.
|
||||
2. 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
|
||||
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
|
||||
initial 24 m range and selects the nested berry exactly by ID.
|
||||
|
||||
The immediate next slice should make those two resource-specific visuals
|
||||
reflect authoritative amount/depletion: thin the berry patch and leave a stump
|
||||
or equivalent readable tree state after real NPC/player extraction, then prove
|
||||
save/restore rebuilds the same presentation without persisting visual-only
|
||||
state. Only after both the berry and tree consume that contract should shared
|
||||
cluster/depletion tooling be extracted or the workflow copied to a forest
|
||||
edge. Do not generalize the resource grid to people/buildings/events or
|
||||
introduce active/abstract LOD before those consumers need it.
|
||||
The first amount-derived resource presentation slice is complete. A shared
|
||||
`ResourceAmountVisual` contract reads the loaded `ResourceNode`'s authoritative
|
||||
amount and derives full, low, or depleted presentation without entering saved
|
||||
state. Real player berry harvests remove fruit and leaf mass; real NPC wood
|
||||
gathers thin the crown and ultimately leave a warm-cut stump. Restoring both
|
||||
low and depleted snapshots rebinds the same nodes and reconstructs the same
|
||||
visuals from `ResourceStateRecord.amount_remaining` alone.
|
||||
|
||||
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:
|
||||
|
||||
- `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
|
||||
stable-ID anchors inside a restrained decorative bank cluster. Twenty-eight
|
||||
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
|
||||
root.add_child(lookdev)
|
||||
await process_frame
|
||||
if not _apply_resource_capture_overrides():
|
||||
quit(1)
|
||||
return
|
||||
_stabilize_lookdev(lookdev)
|
||||
for _frame in WARMUP_FRAMES:
|
||||
await process_frame
|
||||
@@ -73,6 +76,32 @@ func _get_argument_value(prefix: String, default_value: String) -> String:
|
||||
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:
|
||||
var min_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="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/StylizedBerryPatch.tscn" id="4_berry"]
|
||||
[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"]
|
||||
script = ExtResource("1_script")
|
||||
@@ -66,7 +67,7 @@ discovery_priority = 0.1
|
||||
[node name="MeshInstance3D" parent="ResourceAnchors/Tree_River_Resource_01/Visual"]
|
||||
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)
|
||||
|
||||
[node name="InteractionPoint" parent="ResourceAnchors/Tree_River_Resource_01"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name StylizedBerryPatch
|
||||
extends Node3D
|
||||
extends ResourceAmountVisual
|
||||
|
||||
const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader")
|
||||
const LEAF_POSITIONS := [
|
||||
@@ -31,12 +31,37 @@ const BERRY_POSITIONS := [
|
||||
func _ready() -> void:
|
||||
_build_leaves()
|
||||
_build_berries()
|
||||
super()
|
||||
|
||||
|
||||
func get_presentation_instance_count() -> int:
|
||||
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:
|
||||
var mesh := SphereMesh.new()
|
||||
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):
|
||||
state.depleted.connect(_on_state_depleted)
|
||||
_update_presentation()
|
||||
amount_changed.emit(node_id, state.get_amount_remaining())
|
||||
_notify_world_adapters()
|
||||
return true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user