feat: add typed pantry storage site

This commit is contained in:
2026-07-07 12:57:47 +02:00
parent 33b9c1bdfb
commit e5bd93d705
16 changed files with 304 additions and 69 deletions
+10 -6
View File
@@ -67,9 +67,10 @@ Main
└── UI └── UI
``` ```
The player and `WorldViewManager` still reference non-resource activity markers The player and `WorldViewManager` still reference non-resource activity sites
through exported `NodePath` values. `NpcVisual` relies on the navigation map to through exported `NodePath` values. `NpcVisual` relies on the navigation map to
reach both those markers and ResourceNode interaction points. reach legacy guard/study/rest markers, `ResourceNode` interaction points, and
the typed village pantry `StorageNode`.
Replacing the ground without respecting those references would break behavior Replacing the ground without respecting those references would break behavior
that already works. that already works.
@@ -91,11 +92,12 @@ Main
│ ├── FoliageRoot │ ├── FoliageRoot
│ ├── NavigationRegion3D │ ├── NavigationRegion3D
│ ├── WorldObjects │ ├── WorldObjects
│ │ ── ResourceNodes │ │ ── ResourceNodes
│ │ ├── BerryBush instances │ │ ├── BerryBush instances
│ │ └── Tree instances │ │ └── Tree instances
│ │ └── StorageSites
│ │ └── VillagePantry
│ ├── LegacyActivityMarkers temporary non-resource activities │ ├── LegacyActivityMarkers temporary non-resource activities
│ │ ├── PantryMarker
│ │ ├── GuardZone │ │ ├── GuardZone
│ │ ├── StudyZone │ │ ├── StudyZone
│ │ └── RestZone │ │ └── RestZone
@@ -110,8 +112,10 @@ Main
``` ```
`JajceWorld` should contain geography, world-object presentations, and temporary `JajceWorld` should contain geography, world-object presentations, and temporary
legacy interaction locations. It should not own `SimulationManager`, simulated legacy interaction locations. The pantry has since moved out of
NPC data, player state, or UI. `LegacyActivityMarkers` into `WorldObjects/StorageSites/VillagePantry`; guard,
study, and rest remain temporary markers. `JajceWorld` should not own
`SimulationManager`, simulated NPC data, player state, or UI.
Create a small look-development wrapper that instances the same world: Create a small look-development wrapper that instances the same world:
+5 -5
View File
@@ -24,8 +24,8 @@ world only when consumed.
transactions. transactions.
- `village.food` is a synchronized aggregate view used by the existing UI, - `village.food` is a synchronized aggregate view used by the existing UI,
priorities, and utility scoring. It is not a second mutation path. priorities, and utility scoring. It is not a second mutation path.
- `PantryMarker` supplies the active-world interaction position for deposit, - `StorageNode` supplies the active-world interaction position and presentation
withdrawal, eating, and current player pantry interaction. for deposit, withdrawal, eating, and current player pantry interaction.
The village pantry has stable ID `village_pantry`. Storage and NPC inventory The village pantry has stable ID `village_pantry`. Storage and NPC inventory
are included in versioned state and deterministic checksums. are included in versioned state and deterministic checksums.
@@ -49,13 +49,13 @@ replans on its next idle tick.
## Deliberate limits ## Deliberate limits
This slice contains one pantry and one carried item type. It does not yet add: This slice contains one authored pantry and one carried item type. It does not
yet add:
- player inventory; - player inventory;
- storage reservations or access capacity; - storage reservations or access capacity;
- multiple households or ownership; - multiple households or ownership;
- spoilage, item quality, weight, or stack definitions; - spoilage, item quality, weight, or stack definitions.
- an authored StorageNode scene beyond the current pantry marker.
Economic transfer events exist, but event retention, denial/depletion facts, Economic transfer events exist, but event retention, denial/depletion facts,
witnesses, and social interpretation are deliberately deferred. witnesses, and social interpretation are deliberately deferred.
+18 -12
View File
@@ -181,8 +181,8 @@ task choice, physical travel, work completion, resource change, and direct
player assistance. NPC `gather_food` and `gather_wood` tasks now use finite player assistance. NPC `gather_food` and `gather_wood` tasks now use finite
world `ResourceNode` instances (berry bushes and trees) rather than abstract world `ResourceNode` instances (berry bushes and trees) rather than abstract
zone markers. The obsolete farm and forest markers have been removed. Separate zone markers. The obsolete farm and forest markers have been removed. Separate
temporary activity markers remain for eating, rest, study, and patrol. The temporary activity markers remain for rest, study, and patrol; eating and food
migration is documented in transfer now target the typed village pantry. The migration is documented in
[the ResourceNode plan](RESOURCE_NODE_MIGRATION.md). [the ResourceNode plan](RESOURCE_NODE_MIGRATION.md).
## Current technology ## Current technology
@@ -293,25 +293,29 @@ stop moving, change appearance, and no longer collide.
### Current activity markers ### Current activity markers
The main scene contains placeholder activity markers for guard duty, study, The main scene contains placeholder activity markers for guard duty, study,
rest, and food. The former farm and forest zones and their decorative geometry and rest. The former farm and forest zones and their decorative geometry have
have been deleted. been deleted, and food storage now uses a typed pantry site.
**Migration status:** NPC `gather_food` and `gather_wood` target `ResourceNode` **Migration status:** NPC `gather_food` and `gather_wood` target `ResourceNode`
instances (berry bushes and trees), with no marker fallback. The remaining instances (berry bushes and trees), with no marker fallback. The remaining
markers are direct transitional targets for non-resource NPC tasks: markers are direct transitional targets for non-resource NPC tasks, while
storage actions now route to a typed pantry site:
- patrol → guard marker; - patrol → guard marker;
- study → study marker; - study → study marker;
- rest → rest marker; - rest → rest marker;
- eat → food marker. - eat/deposit/withdraw → `StorageNode` (`village_pantry`).
The player also harvests food and wood directly from nearby `ResourceNode` The player also harvests food and wood directly from nearby `ResourceNode`
instances. Extraction returns the actual amount removed, updates the village by instances. Extraction returns the actual amount removed, updates the village by
that amount, and releases an NPC reservation if the source is depleted. that amount, and releases an NPC reservation if the source is depleted.
The food bullet above is the historical transition point: active eating,
deposit, and withdrawal now resolve to `WorldObjects/StorageSites/VillagePantry`
instead of a pantry marker.
Storage, rest, study, and guard behavior should later migrate to target types Rest, study, and guard behavior should later migrate to target types that match
that match their actual semantics rather than treating every usable object as a their actual semantics rather than treating every usable object as a generic
resource source. marker.
### Jajce scaffold ### Jajce scaffold
@@ -323,7 +327,8 @@ resource source.
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke; - fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
- warm sky, fog, shadows, and wind-reactive foliage; - warm sky, fog, shadows, and wind-reactive foliage;
- four ResourceNodes preserving the flat-map stable IDs; - four ResourceNodes preserving the flat-map stable IDs;
- remaining food, guard, study, and rest activity markers; - typed village pantry storage plus remaining guard, study, and rest activity
markers;
- a temporary baked navigation loop with 24 tested routes; - a temporary baked navigation loop with 24 tested routes;
- a look-development scene and camera. - a look-development scene and camera.
@@ -503,8 +508,9 @@ These are expected prototype constraints, not necessarily isolated bugs:
- Action and profession IDs are stable and definition-backed, but activity - Action and profession IDs are stable and definition-backed, but activity
marker mapping and several action effects remain hard-coded. marker mapping and several action effects remain hard-coded.
- Temporary activity markers remain for eating, rest, study, and patrol; NPC - Temporary activity markers remain for rest, study, and patrol; NPC and player
and player food/wood gathering use `ResourceNode` instances with no fallback. food/wood gathering use `ResourceNode` instances with no fallback, while food
transfer uses the typed pantry `StorageNode`.
- Current NPC, village, resource, storage, event, clock, and RNG state serialize - Current NPC, village, resource, storage, event, clock, and RNG state serialize
through world schema v3. F5/F9 provide one validated local quicksave; a save through world schema v3. F5/F9 provide one validated local quicksave; a save
menu, metadata, and player-transform persistence remain deferred. menu, metadata, and player-transform persistence remain deferred.
+8 -9
View File
@@ -14,7 +14,7 @@
### Key divergences from the original plan ### Key divergences from the original plan
- **Zone fallback removed** (Phase 3): `gather_food`/`gather_wood` no longer fall back to `FarmZone`/`ForestZone` markers when no resource node is available. The original plan called for a logged fallback; instead, the NPC goes idle (via `navigation_failed` → wander) to guarantee resource conservation. The obsolete farm and forest scene nodes have now also been deleted. The remaining markers are temporary targets for eating, rest, study, and patrol—not resource fallbacks. - **Zone fallback removed** (Phase 3): `gather_food`/`gather_wood` no longer fall back to `FarmZone`/`ForestZone` markers when no resource node is available. The original plan called for a logged fallback; instead, the NPC goes idle (via `navigation_failed` → wander) to guarantee resource conservation. The obsolete farm and forest scene nodes have now also been deleted. The remaining markers are temporary targets for rest, study, and patrol—not resource fallbacks. Eating and food transfer now target the typed village pantry.
- **`navigation_failed` signal** (discovered during Phase 4): Added a separate signal pathway to distinguish genuine arrival from navigation failure. The original plan treated all "arrivals" uniformly. - **`navigation_failed` signal** (discovered during Phase 4): Added a separate signal pathway to distinguish genuine arrival from navigation failure. The original plan treated all "arrivals" uniformly.
- **Stale-path prevention** (discovered during Phase 4): Added `path_request_id` + `path_pending` to reject async path results that arrive after a new target was set or the NPC died. - **Stale-path prevention** (discovered during Phase 4): Added `path_request_id` + `path_pending` to reject async path results that arrive after a new target was set or the NPC died.
- **Duplicate node_id detection** (discovered during Phase 4): Added startup validation that disables nodes with duplicate IDs and pushes an error. - **Duplicate node_id detection** (discovered during Phase 4): Added startup validation that disables nodes with duplicate IDs and pushes an error.
@@ -524,15 +524,14 @@ Remove each old zone only when its replacement has:
**Progress:** the `TaskZone` container and obsolete `FarmZone`/`ForestZone` **Progress:** the `TaskZone` container and obsolete `FarmZone`/`ForestZone`
nodes have been deleted from `main.tscn`. The surviving nodes live under nodes have been deleted from `main.tscn`. The surviving nodes live under
`ActivityMarkers` to make their transitional role explicit. Task-to-marker `LegacyActivityMarkers` to make their transitional role explicit. Task-to-marker
mapping still exists for eating, rest, study, and patrol, so this phase is not mapping still exists for rest, study, and patrol, so this phase is not complete.
complete.
The former `FoodZone` is now `PantryMarker` backed by simulation-owned The former `FoodZone`/`PantryMarker` is now an authored `StorageNode` at
`StorageStateRecord` state and explicit deposit/withdraw transactions. A `JajceWorld/WorldObjects/StorageSites/VillagePantry`, backed by
dedicated authored StorageNode presentation is still pending. Successful food simulation-owned `StorageStateRecord` state and explicit deposit/withdraw/eat
transfers now emit serializable economic events, and an active NPC visibly transactions. Successful food transfers emit serializable economic events, and
carries food between source, pantry, and consumption. an active NPC visibly carries food between source, pantry, and consumption.
**Exit:** temporary activity markers and task-to-marker mapping are deleted, **Exit:** temporary activity markers and task-to-marker mapping are deleted,
not merely unused. not merely unused.
+4 -4
View File
@@ -23,12 +23,12 @@ height = 1.7
[node name="JajceWorld" parent="." instance=ExtResource("11_jajce")] [node name="JajceWorld" parent="." instance=ExtResource("11_jajce")]
[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "pantry_marker", "guard_zone", "study_zone")] [node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "pantry_storage", "guard_zone", "study_zone")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
script = ExtResource("1_h2yge") script = ExtResource("1_h2yge")
camera_rig = NodePath("../CameraRig") camera_rig = NodePath("../CameraRig")
simulation_manager = NodePath("../SimulationManager") simulation_manager = NodePath("../SimulationManager")
pantry_marker = NodePath("../JajceWorld/LegacyActivityMarkers/PantryMarker") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
guard_zone = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone") guard_zone = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone")
study_zone = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone") study_zone = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone")
stomach_capacity_for_food = 10 stomach_capacity_for_food = 10
@@ -57,12 +57,12 @@ deadzone_forward = 2.0
current = true current = true
fov = 65.0 fov = 65.0
[node name="ActiveWorldAdapter" type="Node" parent="." node_paths=PackedStringArray("guard_marker", "study_marker", "rest_marker", "pantry_marker")] [node name="ActiveWorldAdapter" type="Node" parent="." node_paths=PackedStringArray("guard_marker", "study_marker", "rest_marker", "pantry_storage")]
script = ExtResource("9_adapter") script = ExtResource("9_adapter")
guard_marker = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone") guard_marker = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone")
study_marker = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone") study_marker = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone")
rest_marker = NodePath("../JajceWorld/LegacyActivityMarkers/RestZone") rest_marker = NodePath("../JajceWorld/LegacyActivityMarkers/RestZone")
pantry_marker = NodePath("../JajceWorld/LegacyActivityMarkers/PantryMarker") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
[node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")] [node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")]
script = ExtResource("3_lquwl") script = ExtResource("3_lquwl")
+9 -2
View File
@@ -7,7 +7,7 @@ extends CharacterBody3D
@export var simulation_manager: Node @export var simulation_manager: Node
@export var pantry_marker: Marker3D @export var pantry_storage: StorageNode
@export var guard_zone: Marker3D @export var guard_zone: Marker3D
@export var study_zone: Marker3D @export var study_zone: Marker3D
@@ -65,7 +65,7 @@ func try_interact() -> void:
elif is_near(study_zone): elif is_near(study_zone):
simulation_manager.add_knowledge(2.0) simulation_manager.add_knowledge(2.0)
print("Player shared knowledge.") print("Player shared knowledge.")
elif is_near(pantry_marker): elif is_near_storage(pantry_storage):
simulation_manager.eat_food(stomach_capacity_for_food) simulation_manager.eat_food(stomach_capacity_for_food)
print("Player ate food from the village supply.") print("Player ate food from the village supply.")
@@ -105,3 +105,10 @@ func is_near(zone: Marker3D) -> bool:
return false return false
return global_position.distance_to(zone.global_position) <= interaction_range return global_position.distance_to(zone.global_position) <= interaction_range
func is_near_storage(storage_node: StorageNode) -> bool:
if storage_node == null:
return false
return global_position.distance_to(storage_node.get_interaction_position()) <= interaction_range
+19
View File
@@ -48,6 +48,7 @@ func _ready() -> void:
village.update_priorities() village.update_priorities()
generate_npcs() generate_npcs()
call_deferred("register_loaded_resource_nodes") call_deferred("register_loaded_resource_nodes")
call_deferred("register_loaded_storage_nodes")
if debug_logs: if debug_logs:
print("--- Simulation started ---") print("--- Simulation started ---")
@@ -430,6 +431,24 @@ func get_pantry() -> StorageStateRecord:
return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
func register_loaded_storage_nodes() -> void:
for node in StorageNode.get_all():
register_storage_node(node)
func register_storage_node(node: StorageNode) -> bool:
if node == null or node.storage_id.is_empty():
return false
var storage_state := storage_states.get(node.storage_id) as StorageStateRecord
if storage_state == null:
push_error(
"SimulationManager: StorageNode '%s' has no authoritative StorageStateRecord"
% node.storage_id
)
return false
return node.bind_state(storage_state)
func _sync_village_food() -> void: func _sync_village_food() -> void:
var pantry := get_pantry() var pantry := get_pantry()
if pantry != null: if pantry != null:
+18 -2
View File
@@ -47,8 +47,17 @@ func _run() -> void:
var fixed_origins := [Vector3(-6.0, 0.0, -4.0), Vector3(0.0, 0.0, 0.0), Vector3(6.0, 0.0, 4.0)] var fixed_origins := [Vector3(-6.0, 0.0, -4.0), Vector3(0.0, 0.0, 0.0), Vector3(6.0, 0.0, 4.0)]
var destinations: Array[Vector3] = [] var destinations: Array[Vector3] = []
_check(
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillagePantry"),
"Runtime should expose a typed VillagePantry StorageNode"
)
_check(
not main_scene.has_node("JajceWorld/LegacyActivityMarkers/PantryMarker"),
"Runtime should not keep the pantry as a legacy activity marker"
)
var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
destinations.append(pantry.get_interaction_position())
for marker_path in [ for marker_path in [
"JajceWorld/LegacyActivityMarkers/PantryMarker",
"JajceWorld/LegacyActivityMarkers/GuardZone", "JajceWorld/LegacyActivityMarkers/GuardZone",
"JajceWorld/LegacyActivityMarkers/StudyZone", "JajceWorld/LegacyActivityMarkers/StudyZone",
"JajceWorld/LegacyActivityMarkers/RestZone" "JajceWorld/LegacyActivityMarkers/RestZone"
@@ -81,8 +90,15 @@ func _run() -> void:
executor.advance_npc(starving, village) executor.advance_npc(starving, village)
_check(starving.is_dead, "Starvation threshold should still kill an NPC") _check(starving.is_dead, "Starvation threshold should still kill an NPC")
var adapter := main_scene.get_node("ActiveWorldAdapter") as ActiveWorldAdapter
var storage_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_FOOD)
_check(
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
"Runtime adapter should route storage actions to village_pantry"
)
if failures.is_empty(): if failures.is_empty():
print("[TEST] Jajce runtime passed: 6 NPCs, 4 resources, 24 navigation routes") print("[TEST] Jajce runtime passed: 6 NPCs, 4 resources, typed pantry route")
quit(0) quit(0)
return return
+24 -1
View File
@@ -116,6 +116,17 @@ func _run() -> void:
ids == ["berry_bush_01", "berry_bush_02", "tree_01", "tree_02"], ids == ["berry_bush_01", "berry_bush_02", "tree_01", "tree_02"],
"Jajce resources should preserve all stable IDs" "Jajce resources should preserve all stable IDs"
) )
_check(world.has_node("WorldObjects/StorageSites"), "JajceWorld should expose StorageSites")
var pantry := world.get_node("WorldObjects/StorageSites/VillagePantry") as StorageNode
_check(pantry != null, "JajceWorld should include a typed VillagePantry StorageNode")
_check(
pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY,
"VillagePantry should preserve the stable storage ID"
)
_check(
not world.has_node("LegacyActivityMarkers/PantryMarker"),
"Pantry should no longer be a legacy activity marker"
)
var navigation_region := world.get_node("NavigationRegion3D") as NavigationRegion3D var navigation_region := world.get_node("NavigationRegion3D") as NavigationRegion3D
var navigation_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid()) var navigation_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid())
@@ -131,6 +142,7 @@ func _run() -> void:
var destinations: Array[Vector3] = [] var destinations: Array[Vector3] = []
for resource in resources: for resource in resources:
destinations.append((resource as ResourceNode).interaction_point.global_position) destinations.append((resource as ResourceNode).interaction_point.global_position)
destinations.append(pantry.get_interaction_position())
for marker in world.get_node("LegacyActivityMarkers").get_children(): for marker in world.get_node("LegacyActivityMarkers").get_children():
destinations.append((marker as Marker3D).global_position) destinations.append((marker as Marker3D).global_position)
@@ -143,6 +155,7 @@ func _run() -> void:
) )
var adapter := ActiveWorldAdapter.new() var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry
root.add_child(adapter) root.add_child(adapter)
var manager: Node = load("res://simulation/SimulationManager.gd").new() var manager: Node = load("res://simulation/SimulationManager.gd").new()
manager.debug_logs = false manager.debug_logs = false
@@ -164,10 +177,20 @@ func _run() -> void:
"Selected Jajce resource should be authoritatively reserved" "Selected Jajce resource should be authoritatively reserved"
) )
manager.release_resource(selected.node_id, npc.id) manager.release_resource(selected.node_id, npc.id)
var storage_target := adapter.get_activity_target(SimulationIds.ACTION_WITHDRAW_FOOD)
_check(
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
"Storage actions should target the typed pantry's stable ID"
)
var storage_target_position: Vector3 = storage_target.get("position", Vector3(9999, 9999, 9999))
_check(
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
"Storage actions should use the pantry StorageNode interaction point"
)
if failures.is_empty(): if failures.is_empty():
print( print(
"[TEST] Jajce scaffold passed: Terrain3D, 6 textures, shader water, building blockouts, chimney smoke" "[TEST] Jajce scaffold passed: Terrain3D, storage site, shader water, building blockouts"
) )
quit(0) quit(0)
return return
+12
View File
@@ -57,6 +57,18 @@ func _run() -> void:
"Village should receive exactly the extracted wood" "Village should receive exactly the extracted wood"
) )
var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
var pantry_state: StorageStateRecord = simulation_manager.get_pantry()
pantry_state.deposit(SimulationIds.RESOURCE_FOOD, 3.0)
simulation_manager._sync_village_food()
var pantry_food_before := pantry_state.get_amount(SimulationIds.RESOURCE_FOOD)
player.global_position = pantry.get_interaction_position()
player.try_interact()
_check(
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before,
"Player should eat from the typed pantry StorageNode"
)
if failures.is_empty(): if failures.is_empty():
print("[TEST] ResourceNode player parity passed") print("[TEST] ResourceNode player parity passed")
quit(0) quit(0)
+13 -14
View File
@@ -4,7 +4,7 @@ extends Node
@export var guard_marker: Marker3D @export var guard_marker: Marker3D
@export var study_marker: Marker3D @export var study_marker: Marker3D
@export var rest_marker: Marker3D @export var rest_marker: Marker3D
@export var pantry_marker: Marker3D @export var pantry_storage: StorageNode
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
@@ -28,19 +28,18 @@ func get_activity_target(action_id: StringName) -> Dictionary:
SimulationIds.ACTION_REST: SimulationIds.ACTION_REST:
marker = rest_marker marker = rest_marker
SimulationIds.ACTION_EAT: SimulationIds.ACTION_EAT:
marker = pantry_marker return _get_storage_target(pantry_storage)
SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD: SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD:
marker = pantry_marker return _get_storage_target(pantry_storage)
if marker == null: if marker == null:
return {} return {}
var target_id := "" return {"target_id": "", "position": marker.global_position}
if (
action_id
in [ func _get_storage_target(storage_node: StorageNode) -> Dictionary:
SimulationIds.ACTION_EAT, if storage_node == null:
SimulationIds.ACTION_DEPOSIT_FOOD, return {}
SimulationIds.ACTION_WITHDRAW_FOOD return {
] "target_id": String(storage_node.storage_id),
): "position": storage_node.get_interaction_position()
target_id = String(SimulationIds.STORAGE_VILLAGE_PANTRY) }
return {"target_id": target_id, "position": marker.global_position}
+5 -14
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=26 format=3] [gd_scene load_steps=25 format=3]
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"] [ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
[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"]
@@ -13,6 +13,7 @@
[ext_resource type="PackedScene" path="res://world/jajce/WaterfallMist.tscn" id="11_mist"] [ext_resource type="PackedScene" path="res://world/jajce/WaterfallMist.tscn" id="11_mist"]
[ext_resource type="PackedScene" path="res://world/jajce/ChimneySmoke.tscn" id="12_smoke"] [ext_resource type="PackedScene" path="res://world/jajce/ChimneySmoke.tscn" id="12_smoke"]
[ext_resource type="Script" path="res://world/jajce/day_night_cycle.gd" id="13_daynight"] [ext_resource type="Script" path="res://world/jajce/day_night_cycle.gd" id="13_daynight"]
[ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = { _shader_parameters = {
@@ -86,10 +87,6 @@ adjustment_brightness = 1.04
adjustment_contrast = 1.03 adjustment_contrast = 1.03
adjustment_saturation = 1.1 adjustment_saturation = 1.1
[sub_resource type="StandardMaterial3D" id="Material_pantry"]
albedo_color = Color(0.5, 0.32, 0.2, 1)
roughness = 0.85
[sub_resource type="StandardMaterial3D" id="Material_guard"] [sub_resource type="StandardMaterial3D" id="Material_guard"]
albedo_color = Color(0.55, 0.45, 0.35, 1) albedo_color = Color(0.55, 0.45, 0.35, 1)
roughness = 0.8 roughness = 0.8
@@ -102,10 +99,6 @@ roughness = 0.82
albedo_color = Color(0.35, 0.28, 0.22, 1) albedo_color = Color(0.35, 0.28, 0.22, 1)
roughness = 0.88 roughness = 0.88
[sub_resource type="BoxMesh" id="Mesh_pantry_base"]
material = SubResource("Material_pantry")
size = Vector3(1.6, 1.1, 1.6)
[sub_resource type="CylinderMesh" id="Mesh_guard_post"] [sub_resource type="CylinderMesh" id="Mesh_guard_post"]
material = SubResource("Material_guard") material = SubResource("Material_guard")
top_radius = 0.35 top_radius = 0.35
@@ -253,14 +246,12 @@ resource_id = &"wood"
initial_amount = 12.0 initial_amount = 12.0
yield_per_action = 3.0 yield_per_action = 3.0
[node name="LegacyActivityMarkers" type="Node3D" parent="."] [node name="StorageSites" type="Node3D" parent="WorldObjects"]
[node name="PantryMarker" type="Marker3D" parent="LegacyActivityMarkers"] [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
position = Vector3(0, 0, -8) position = Vector3(0, 0, -8)
[node name="PantryVisual" type="MeshInstance3D" parent="LegacyActivityMarkers/PantryMarker"] [node name="LegacyActivityMarkers" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.55, 0)
mesh = SubResource("Mesh_pantry_base")
[node name="GuardZone" type="Marker3D" parent="LegacyActivityMarkers"] [node name="GuardZone" type="Marker3D" parent="LegacyActivityMarkers"]
position = Vector3(0, 0, 8) position = Vector3(0, 0, 8)
+1
View File
@@ -0,0 +1 @@
uid://0epmcyfxcn62
+105
View File
@@ -0,0 +1,105 @@
class_name StorageNode
extends Node3D
static var _all: Array = []
@export var storage_id: StringName = SimulationIds.STORAGE_VILLAGE_PANTRY
@export var display_name := "Village Pantry"
@export var accepted_items: Array[StringName] = []
@export var debug_label_enabled := true
@onready var interaction_point: Marker3D = $InteractionPoint
var state: StorageStateRecord
var _last_label_text := ""
func _ready() -> void:
if accepted_items.is_empty():
accepted_items = [SimulationIds.RESOURCE_FOOD]
if storage_id.is_empty():
push_error("StorageNode at %s has empty storage_id" % get_path())
_update_presentation()
return
var existing := get_by_id(storage_id)
if existing != null:
push_error(
(
"Duplicate StorageNode storage_id '%s' at %s; first registered at %s"
% [storage_id, get_path(), existing.get_path()]
)
)
_update_presentation()
return
_all.append(self)
add_to_group("storage_nodes")
_try_register_with_simulation()
_update_presentation()
func _process(_delta: float) -> void:
if debug_label_enabled:
_update_presentation()
func _exit_tree() -> void:
_all.erase(self)
state = null
func bind_state(storage_state: StorageStateRecord) -> bool:
if storage_state == null or storage_state.get_storage_id() != storage_id:
return false
state = storage_state
_update_presentation()
return true
func get_interaction_position() -> Vector3:
return interaction_point.global_position if interaction_point != null else global_position
func accepts_item(item_id: StringName) -> bool:
return accepted_items.is_empty() or item_id in accepted_items
func _try_register_with_simulation() -> void:
var managers := get_tree().get_nodes_in_group("simulation_manager")
if managers.size() != 1:
return
var manager := managers[0]
if manager.has_method("register_storage_node"):
manager.register_storage_node(self)
func _update_presentation() -> void:
if not has_node("DebugLabel"):
return
var label := $DebugLabel as Label3D
label.visible = debug_label_enabled
if not debug_label_enabled:
return
var text := "%s\n%s" % [display_name, storage_id]
if state == null:
text += "\nUNBOUND"
else:
for item_id in accepted_items:
text += "\n%s: %.1f" % [item_id, state.get_amount(item_id)]
if text == _last_label_text:
return
_last_label_text = text
label.text = text
static func get_by_id(search_id: StringName) -> StorageNode:
for node in _all:
if node.storage_id == search_id:
return node
return null
static func get_all() -> Array[StorageNode]:
var nodes: Array[StorageNode] = []
for node in _all:
nodes.append(node as StorageNode)
return nodes
+52
View File
@@ -0,0 +1,52 @@
[gd_scene load_steps=7 format=3]
[ext_resource type="Script" path="res://world/storage/StorageNode.gd" id="1_storage"]
[sub_resource type="StandardMaterial3D" id="Material_crate"]
albedo_color = Color(0.5, 0.32, 0.2, 1)
roughness = 0.85
[sub_resource type="StandardMaterial3D" id="Material_cloth"]
albedo_color = Color(0.72, 0.56, 0.34, 1)
roughness = 0.9
[sub_resource type="BoxMesh" id="Mesh_crate"]
material = SubResource("Material_crate")
size = Vector3(1.6, 1.1, 1.6)
[sub_resource type="BoxMesh" id="Mesh_lid"]
material = SubResource("Material_cloth")
size = Vector3(1.75, 0.12, 1.75)
[sub_resource type="BoxMesh" id="Mesh_sack"]
material = SubResource("Material_cloth")
size = Vector3(0.45, 0.65, 0.45)
[node name="StorageNode" type="Node3D"]
script = ExtResource("1_storage")
[node name="Visual" type="Node3D" parent="."]
[node name="Crate" type="MeshInstance3D" parent="Visual"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.55, 0)
mesh = SubResource("Mesh_crate")
[node name="LidCloth" type="MeshInstance3D" parent="Visual"]
transform = Transform3D(1, 0, 0, 0, 0.7, 0.7173561, 0, -0.7173561, 0.7, 0, 1.15, -0.06)
mesh = SubResource("Mesh_lid")
[node name="FoodSack" type="MeshInstance3D" parent="Visual"]
transform = Transform3D(0.85, 0, 0, 0, 1, 0, 0, 0, 0.85, 0.8, 0.325, 0.55)
mesh = SubResource("Mesh_sack")
[node name="InteractionPoint" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2.0)
[node name="DebugLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.1, 0)
billboard = 1
no_depth_test = true
font_size = 20
outline_modulate = Color(0, 0, 0, 1)
outline_size = 4
text = "Storage"
+1
View File
@@ -0,0 +1 @@
uid://cascjhf8lsvay