feat: replace activity markers with typed sites

This commit is contained in:
2026-07-07 13:27:02 +02:00
parent e5bd93d705
commit 4c090f1635
19 changed files with 308 additions and 126 deletions
+3 -2
View File
@@ -39,12 +39,13 @@ SimulationManager tick
- consumes active-world facts through ActiveWorldAdapter; - consumes active-world facts through ActiveWorldAdapter;
- checks simulation-owned ResourceStateRecord availability; - checks simulation-owned ResourceStateRecord availability;
- reserves and returns stable resource target IDs; - reserves and returns stable resource target IDs;
- returns activity or wander positions without owning presentation. - returns typed activity-site, storage, or wander positions without owning
presentation.
### ActiveWorldAdapter ### ActiveWorldAdapter
- exposes loaded resource interaction positions; - exposes loaded resource interaction positions;
- maps current temporary activity markers to action IDs; - exposes loaded storage and activity-site interaction positions;
- contains active-world query facts, not persistent mutable authority; - contains active-world query facts, not persistent mutable authority;
- does not choose actions or reserve resources. - does not choose actions or reserve resources.
+23 -25
View File
@@ -41,8 +41,8 @@ Do not redo these tasks:
- hunger, energy, starvation, death, task scoring, and task duration; - hunger, energy, starvation, death, task scoring, and task duration;
- task-change and village-change signals; - task-change and village-change signals;
- visual NPC navigation and arrival reporting; - visual NPC navigation and arrival reporting;
- finite berry/tree ResourceNodes plus food, guard, study, and rest activity - finite berry/tree ResourceNodes plus typed food, guard, study, and rest
markers; world sites;
- aggregate village UI; - aggregate village UI;
- initial tree assets; - initial tree assets;
- a baked navigation region for the current flat test area. - a baked navigation region for the current flat test area.
@@ -56,21 +56,20 @@ plugin content. They are references, not the game.
```text ```text
Main Main
├── current flat World and NavigationRegion3D ├── JajceWorld
├── Player ├── Player
├── CameraRig ├── CameraRig
├── SimulationManager ├── SimulationManager
├── WorldViewManager ├── WorldViewManager
├── ActiveNPCs ├── ActiveNPCs
├── ActivityMarkers ├── typed world sites inside JajceWorld/WorldObjects
├── WorldEnvironment ├── WorldEnvironment
└── UI └── UI
``` ```
The player and `WorldViewManager` still reference non-resource activity sites The player still references interactable world sites through exported `NodePath`
through exported `NodePath` values. `NpcVisual` relies on the navigation map to values. `NpcVisual` relies on the navigation map to reach typed `ActivitySite`,
reach legacy guard/study/rest markers, `ResourceNode` interaction points, and `StorageNode`, and `ResourceNode` interaction points.
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.
@@ -95,12 +94,12 @@ Main
│ │ ├── ResourceNodes │ │ ├── ResourceNodes
│ │ ├── BerryBush instances │ │ ├── BerryBush instances
│ │ └── Tree instances │ │ └── Tree instances
│ │ ── StorageSites │ │ ── StorageSites
│ │ └── VillagePantry │ │ └── VillagePantry
├── LegacyActivityMarkers temporary non-resource activities │ └── ActivitySites
│ │ ├── GuardZone │ │ ├── GuardPost
│ │ ├── StudyZone │ │ ├── StudyDesk
│ │ └── RestZone │ │ └── RestBench
│ ├── DirectionalLight3D │ ├── DirectionalLight3D
│ └── WorldEnvironment │ └── WorldEnvironment
├── Player ├── Player
@@ -111,11 +110,10 @@ Main
└── UI └── UI
``` ```
`JajceWorld` should contain geography, world-object presentations, and temporary `JajceWorld` should contain geography and world-object presentations. The former
legacy interaction locations. The pantry has since moved out of legacy interaction locations now live as typed storage and activity sites under
`LegacyActivityMarkers` into `WorldObjects/StorageSites/VillagePantry`; guard, `WorldObjects`; `JajceWorld` should not own `SimulationManager`, simulated NPC
study, and rest remain temporary markers. `JajceWorld` should not own data, player state, or UI.
`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:
@@ -295,7 +293,7 @@ Create `world/jajce/JajceWorld.tscn` with:
- placeholder `VillageRoot`; - placeholder `VillageRoot`;
- placeholder `FoliageRoot`; - placeholder `FoliageRoot`;
- a navigation region; - a navigation region;
- resource-node containers plus temporary activity markers; - resource-node, storage-site, and activity-site containers;
- lighting and environment. - lighting and environment.
Create `JajceLookdev.tscn` that instances `JajceWorld` and adds a temporary Create `JajceLookdev.tscn` that instances `JajceWorld` and adds a temporary
@@ -422,8 +420,8 @@ Once the world composition is stable:
2. Disable or remove the old flat world only after the new instance is present. 2. Disable or remove the old flat world only after the new instance is present.
3. Place or move bushes and trees under 3. Place or move bushes and trees under
`JajceWorld/WorldObjects/ResourceNodes` while preserving stable IDs. `JajceWorld/WorldObjects/ResourceNodes` while preserving stable IDs.
4. Reconnect only remaining non-resource activity markers under 4. Reconnect remaining non-resource activities as typed sites under
`JajceWorld/LegacyActivityMarkers`. `JajceWorld/WorldObjects`.
5. Bake navigation for the new walkable area. 5. Bake navigation for the new walkable area.
6. Test each existing task from multiple spawn positions. 6. Test each existing task from multiple spawn positions.
7. Verify task arrival still transitions from traveling to working. 7. Verify task arrival still transitions from traveling to working.
@@ -436,8 +434,8 @@ Once the world composition is stable:
Do not move simulation ownership into `JajceWorld` to make wiring convenient. Do not move simulation ownership into `JajceWorld` to make wiring convenient.
**Status: runtime integration complete.** `main.tscn` now instances the reusable **Status: runtime integration complete.** `main.tscn` now instances the reusable
world and points player/simulation adapters at its stable-ID resources and world and points player/simulation adapters at its stable-ID resources, storage
remaining activity markers. The duplicate flat world objects were removed. site, and activity sites. The duplicate flat world objects were removed.
The runtime regression checks Terrain3D presence, 24 navigation routes, core The runtime regression checks Terrain3D presence, 24 navigation routes, core
task execution, starvation/death, player extraction parity, reservations, and task execution, starvation/death, player extraction parity, reservations, and
NPC visual lifecycle. Terrain sculpting and presentation polish remain NPC visual lifecycle. Terrain sculpting and presentation polish remain
@@ -445,7 +443,7 @@ separate work, not blockers for this wiring milestone.
### Exit condition ### Exit condition
The current three NPCs and player can perform all existing prototype behaviors The current six NPCs and player can perform all existing prototype behaviors
inside the new terrain without regression. inside the new terrain without regression.
### Phase 5 — Make the simulation readable on video ### Phase 5 — Make the simulation readable on video
+5 -3
View File
@@ -652,9 +652,11 @@ Completed after the architecture gate:
The practical next sequence is: The practical next sequence is:
1. Replace the remaining generic activity markers with typed world sites, 1. Add a cinematic/debug toggle and repeatable simulation-garden demo reset.
beginning with the pantry/storage presentation. 2. Expand resource discovery with many finite `ResourceNode` instances placed
2. Add a cinematic/debug toggle and repeatable simulation-garden demo reset. in meaningful foliage, animal-camp, berry, and village-stockpile contexts.
Score them by reachability, distance, safety, profession, and NPC comfort
range rather than reintroducing abstract resource zones.
3. Expand persistence only when schedules, player state, or a real save menu 3. Expand persistence only when schedules, player state, or a real save menu
creates a concrete requirement. creates a concrete requirement.
+30 -34
View File
@@ -180,9 +180,9 @@ The current prototype implements a very early version of observe, autonomous
task choice, physical travel, work completion, resource change, and direct 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, forest, and activity markers have been
temporary activity markers remain for rest, study, and patrol; eating and food removed. Eating and food transfer target the typed village pantry, while rest,
transfer now target the typed village pantry. The migration is documented in study, and patrol target typed activity sites. The migration is documented in
[the ResourceNode plan](RESOURCE_NODE_MIGRATION.md). [the ResourceNode plan](RESOURCE_NODE_MIGRATION.md).
## Current technology ## Current technology
@@ -207,7 +207,7 @@ plugin content, not game architecture.
### World and player ### World and player
- `main.tscn` contains a flat approximately 30×30 prototype ground. - `main.tscn` instances the reusable Jajce Terrain3D world.
- The player is a `CharacterBody3D` represented by placeholder primitive - The player is a `CharacterBody3D` represented by placeholder primitive
geometry. geometry.
- WASD movement is camera-relative. - WASD movement is camera-relative.
@@ -215,17 +215,19 @@ plugin content, not game architecture.
follow/focus behavior. follow/focus behavior.
- Pressing `E` near a berry bush or tree extracts its configured yield into the - Pressing `E` near a berry bush or tree extracts its configured yield into the
village through the same `ResourceNode` contract used by NPCs. village through the same `ResourceNode` contract used by NPCs.
- Guard, study, rest, and food interactions still use temporary activity - Guard, study, rest, and food interactions now use typed world sites.
markers.
- `Escape` releases captured mouse input. - `Escape` releases captured mouse input.
### Village simulation ### Village simulation
The simulation currently creates three NPCs: The simulation currently creates six NPCs:
- Amina - Amina
- Tarik - Tarik
- Jasmin - Jasmin
- Elma
- Mirza
- Lejla
Each receives a random profession from: Each receives a random profession from:
@@ -283,39 +285,34 @@ Needs override ordinary work. Otherwise a utility-like score combines:
- profession preference; - profession preference;
- a small random contribution. - a small random contribution.
The visual NPC walks toward the task's marker. Arrival tells the simulation to The visual NPC walks toward the task's typed target position. Arrival tells the
start work. Work progresses on simulation ticks. The village receives the simulation to start work. Work progresses on simulation ticks. The village
result only when the task duration completes. receives the result only when the task duration completes.
Starvation can reduce productivity and eventually kill an NPC. Dead visuals Starvation can reduce productivity and eventually kill an NPC. Dead visuals
stop moving, change appearance, and no longer collide. stop moving, change appearance, and no longer collide.
### Current activity markers ### Current world targets
The main scene contains placeholder activity markers for guard duty, study, The former farm, forest, food, guard, study, and rest markers have been deleted.
and rest. The former farm and forest zones and their decorative geometry have The runtime now uses semantic world targets:
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. Non-resource
markers are direct transitional targets for non-resource NPC tasks, while actions route through typed sites:
storage actions now route to a typed pantry site:
- patrol → guard marker; - patrol → `ActivitySite` (`guard_post`);
- study → study marker; - study → `ActivitySite` (`study_desk`);
- rest → rest marker; - rest → `ActivitySite` (`rest_bench`);
- eat/deposit/withdraw → `StorageNode` (`village_pantry`). - 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, Future resource expansion should add many finite `ResourceNode` instances rather
deposit, and withdrawal now resolve to `WorldObjects/StorageSites/VillagePantry` than broad resource zones: trees in foliage clusters, berry patches, animal
instead of a pantry marker. camps, and village stockpiles can be ranked by reachability, distance, safety,
profession, and NPC comfort range.
Rest, study, and guard behavior should later migrate to target types that match
their actual semantics rather than treating every usable object as a generic
marker.
### Jajce scaffold ### Jajce scaffold
@@ -327,8 +324,7 @@ marker.
- 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;
- typed village pantry storage plus remaining guard, study, and rest activity - typed village pantry storage and typed guard, study, and rest activity sites;
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.
@@ -506,11 +502,11 @@ NpcVisual navigates through the active world
These are expected prototype constraints, not necessarily isolated bugs: 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 several
marker mapping and several action effects remain hard-coded. action effects remain hard-coded.
- Temporary activity markers remain for rest, study, and patrol; NPC and player - Temporary activity markers have been removed; NPC and player food/wood
food/wood gathering use `ResourceNode` instances with no fallback, while food gathering use `ResourceNode` instances with no fallback, food transfer uses
transfer uses the typed pantry `StorageNode`. the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`.
- 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.
+2 -2
View File
@@ -13,8 +13,8 @@ sources of truth.
visual slice. It must respect the architecture gates in the learning visual slice. It must respect the architecture gates in the learning
roadmap. roadmap.
4. [`RESOURCE_NODE_MIGRATION.md`](RESOURCE_NODE_MIGRATION.md) is a focused 4. [`RESOURCE_NODE_MIGRATION.md`](RESOURCE_NODE_MIGRATION.md) is a focused
migration plan. Phases 15 are complete; Phase 6 tracks replacement of the migration plan. Phases 16 are complete; follow-up work should expand
remaining temporary activity markers. resource discovery without reintroducing abstract resource zones.
5. [`ACTION_SYSTEM_ARCHITECTURE.md`](ACTION_SYSTEM_ARCHITECTURE.md), 5. [`ACTION_SYSTEM_ARCHITECTURE.md`](ACTION_SYSTEM_ARCHITECTURE.md),
[`ECONOMIC_EVENTS.md`](ECONOMIC_EVENTS.md), [`ECONOMIC_EVENTS.md`](ECONOMIC_EVENTS.md),
[`FOOD_STORAGE_ARCHITECTURE.md`](FOOD_STORAGE_ARCHITECTURE.md), [`FOOD_STORAGE_ARCHITECTURE.md`](FOOD_STORAGE_ARCHITECTURE.md),
+20 -8
View File
@@ -10,7 +10,7 @@
| **4a** | Wood target selection (NPC) | ✅ Complete | | **4a** | Wood target selection (NPC) | ✅ Complete |
| **4b** | Player parity (extraction contract) | ✅ Complete | | **4b** | Player parity (extraction contract) | ✅ Complete |
| **5** | Jajce world placement and runtime integration | ✅ Complete | | **5** | Jajce world placement and runtime integration | ✅ Complete |
| **6** | Replace remaining activity markers | 🔶 Resource zones removed; activity targets pending | | **6** | Replace remaining activity markers | ✅ Storage and activity sites authored |
### Key divergences from the original plan ### Key divergences from the original plan
@@ -61,9 +61,9 @@ environment is integrated. It is easier to debug target selection, reservation,
depletion, and task completion without terrain and navigation changes happening depletion, and task completion without terrain and navigation changes happening
at the same time. at the same time.
The old resource zones have been removed. Temporary activity markers remain The old resource zones and temporary activity markers have been removed. Food
only for actions whose real target type does not exist yet. They are not transfer targets `StorageNode`; rest, study, and patrol target `ActivitySite`.
fallbacks and are not part of the target architecture. These are not resource fallbacks and are not resource-search areas.
## Why this is the next systems step ## Why this is the next systems step
@@ -522,10 +522,10 @@ Remove each old zone only when its replacement has:
- debug visibility; - debug visibility;
- save representation. - save representation.
**Progress:** the `TaskZone` container and obsolete `FarmZone`/`ForestZone` **Progress:** the `TaskZone` container, obsolete `FarmZone`/`ForestZone` nodes,
nodes have been deleted from `main.tscn`. The surviving nodes live under and final `LegacyActivityMarkers` container have been deleted from `main.tscn`
`LegacyActivityMarkers` to make their transitional role explicit. Task-to-marker and `JajceWorld`. Task-to-marker mapping has been replaced by typed site
mapping still exists for rest, study, and patrol, so this phase is not complete. queries.
The former `FoodZone`/`PantryMarker` is now an authored `StorageNode` at The former `FoodZone`/`PantryMarker` is now an authored `StorageNode` at
`JajceWorld/WorldObjects/StorageSites/VillagePantry`, backed by `JajceWorld/WorldObjects/StorageSites/VillagePantry`, backed by
@@ -533,9 +533,21 @@ simulation-owned `StorageStateRecord` state and explicit deposit/withdraw/eat
transactions. Successful food transfers emit serializable economic events, and transactions. Successful food transfers emit serializable economic events, and
an active NPC visibly carries food between source, pantry, and consumption. an active NPC visibly carries food between source, pantry, and consumption.
Guard, study, and rest are now authored `ActivitySite` instances at
`JajceWorld/WorldObjects/ActivitySites`. The adapter chooses the nearest loaded
site that supports the requested action. Capacity is currently presentation
metadata; reservation/enforced occupancy can be added when multiple NPCs
compete for the same social/workstation site.
**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.
**Status: complete for the marker-removal migration.** Future work should
extend target scoring rather than reintroducing zones: resources can be spread
as many finite `ResourceNode` instances across foliage, animal camps, berry
patches, and village stockpiles, then ranked by distance, reachability, safety,
profession, and NPC comfort range.
## Test scenarios ## Test scenarios
At minimum, exercise: At minimum, exercise:
+4 -3
View File
@@ -12,10 +12,10 @@ same contract now runs against the integrated `JajceWorld` runtime.
The automated baseline verifies: The automated baseline verifies:
- the scene creates three simulated NPCs; - the scene creates six simulated NPCs;
- four stable-ID ResourceNodes register; - four stable-ID ResourceNodes register;
- three fixed origins can reach the four remaining activity markers and four - three fixed origins can reach the typed pantry, three typed activity sites,
ResourceNode interaction points (24 route checks); and four ResourceNode interaction points (24 route checks);
- a two-tick working task completes; - a two-tick working task completes;
- starvation still reaches death at its configured threshold. - starvation still reaches death at its configured threshold.
@@ -24,6 +24,7 @@ The companion player-parity scenario verifies:
- player food extraction conserves the actual remaining amount; - player food extraction conserves the actual remaining amount;
- player wood extraction uses the configured yield; - player wood extraction uses the configured yield;
- player depletion releases an NPC reservation. - player depletion releases an NPC reservation.
- player pantry and guard interactions use typed world sites.
## Commands ## Commands
+4 -7
View File
@@ -23,14 +23,14 @@ 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_storage", "guard_zone", "study_zone")] [node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "pantry_storage", "guard_site", "study_site")]
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_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
guard_zone = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone") guard_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/GuardPost")
study_zone = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone") study_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/StudyDesk")
stomach_capacity_for_food = 10 stomach_capacity_for_food = 10
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player" unique_id=1249497228] [node name="CollisionShape3D" type="CollisionShape3D" parent="Player" unique_id=1249497228]
@@ -57,11 +57,8 @@ 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_storage")] [node name="ActiveWorldAdapter" type="Node" parent="." node_paths=PackedStringArray("pantry_storage")]
script = ExtResource("9_adapter") script = ExtResource("9_adapter")
guard_marker = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone")
study_marker = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone")
rest_marker = NodePath("../JajceWorld/LegacyActivityMarkers/RestZone")
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry") 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")]
+11 -4
View File
@@ -8,8 +8,8 @@ extends CharacterBody3D
@export var simulation_manager: Node @export var simulation_manager: Node
@export var pantry_storage: StorageNode @export var pantry_storage: StorageNode
@export var guard_zone: Marker3D @export var guard_site: ActivitySite
@export var study_zone: Marker3D @export var study_site: ActivitySite
@export var stomach_capacity_for_food := 5 @export var stomach_capacity_for_food := 5
@export var interaction_range := 3.0 @export var interaction_range := 3.0
@@ -59,10 +59,10 @@ func try_interact() -> void:
if try_harvest_resource_node(): if try_harvest_resource_node():
return return
if is_near(guard_zone): if is_near_activity_site(guard_site):
simulation_manager.add_safety(3.0) simulation_manager.add_safety(3.0)
print("Player helped guard the village.") print("Player helped guard the village.")
elif is_near(study_zone): elif is_near_activity_site(study_site):
simulation_manager.add_knowledge(2.0) simulation_manager.add_knowledge(2.0)
print("Player shared knowledge.") print("Player shared knowledge.")
elif is_near_storage(pantry_storage): elif is_near_storage(pantry_storage):
@@ -112,3 +112,10 @@ func is_near_storage(storage_node: StorageNode) -> bool:
return false return false
return global_position.distance_to(storage_node.get_interaction_position()) <= interaction_range return global_position.distance_to(storage_node.get_interaction_position()) <= interaction_range
func is_near_activity_site(activity_site: ActivitySite) -> bool:
if activity_site == null:
return false
return global_position.distance_to(activity_site.get_interaction_position()) <= interaction_range
+2 -1
View File
@@ -432,7 +432,8 @@ func get_pantry() -> StorageStateRecord:
func register_loaded_storage_nodes() -> void: func register_loaded_storage_nodes() -> void:
for node in StorageNode.get_all(): for candidate in StorageNode.get_all():
var node := candidate as StorageNode
register_storage_node(node) register_storage_node(node)
+1 -1
View File
@@ -14,7 +14,7 @@ func resolve(
npc, origin, definition, simulation_manager, active_world_adapter npc, origin, definition, simulation_manager, active_world_adapter
) )
SimulationIds.TARGET_ACTIVITY: SimulationIds.TARGET_ACTIVITY:
return active_world_adapter.get_activity_target(npc.current_task) return active_world_adapter.get_activity_target(npc.current_task, origin)
SimulationIds.TARGET_FREE: SimulationIds.TARGET_FREE:
return { return {
"target_id": "", "position": origin + simulation_manager.get_wander_offset(npc.id) "target_id": "", "position": origin + simulation_manager.get_wander_offset(npc.id)
+11 -7
View File
@@ -31,6 +31,7 @@ func _run() -> void:
not main_scene.has_node("World") not main_scene.has_node("World")
and not main_scene.has_node("ResourceNodes") and not main_scene.has_node("ResourceNodes")
and not main_scene.has_node("ActivityMarkers") and not main_scene.has_node("ActivityMarkers")
and not main_scene.has_node("JajceWorld/LegacyActivityMarkers")
), ),
"Runtime should not retain duplicate flat-world objects" "Runtime should not retain duplicate flat-world objects"
) )
@@ -57,12 +58,10 @@ func _run() -> void:
) )
var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
destinations.append(pantry.get_interaction_position()) destinations.append(pantry.get_interaction_position())
for marker_path in [ var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites")
"JajceWorld/LegacyActivityMarkers/GuardZone", _check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites")
"JajceWorld/LegacyActivityMarkers/StudyZone", for site in activity_root.get_children():
"JajceWorld/LegacyActivityMarkers/RestZone" destinations.append((site as ActivitySite).get_interaction_position())
]:
destinations.append(main_scene.get_node(marker_path).global_position)
for resource in resource_root.get_children(): for resource in resource_root.get_children():
destinations.append((resource as ResourceNode).interaction_point.global_position) destinations.append((resource as ResourceNode).interaction_point.global_position)
@@ -96,9 +95,14 @@ func _run() -> void:
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY), storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
"Runtime adapter should route storage actions to village_pantry" "Runtime adapter should route storage actions to village_pantry"
) )
var study_target := adapter.get_activity_target(SimulationIds.ACTION_STUDY)
_check(
study_target.get("target_id", "") == "study_desk",
"Runtime adapter should route study to the typed study desk"
)
if failures.is_empty(): if failures.is_empty():
print("[TEST] Jajce runtime passed: 6 NPCs, 4 resources, typed pantry route") print("[TEST] Jajce runtime passed: 6 NPCs, typed storage and activity sites")
quit(0) quit(0)
return return
+21 -3
View File
@@ -127,6 +127,19 @@ func _run() -> void:
not world.has_node("LegacyActivityMarkers/PantryMarker"), not world.has_node("LegacyActivityMarkers/PantryMarker"),
"Pantry should no longer be a legacy activity marker" "Pantry should no longer be a legacy activity marker"
) )
_check(
not world.has_node("LegacyActivityMarkers"),
"JajceWorld should no longer expose legacy activity markers"
)
var activity_root := world.get_node("WorldObjects/ActivitySites")
var activity_ids: Array[String] = []
for site in activity_root.get_children():
activity_ids.append(String((site as ActivitySite).site_id))
activity_ids.sort()
_check(
activity_ids == ["guard_post", "rest_bench", "study_desk"],
"Jajce activity sites should preserve stable semantic IDs"
)
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())
@@ -143,8 +156,8 @@ func _run() -> void:
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()) destinations.append(pantry.get_interaction_position())
for marker in world.get_node("LegacyActivityMarkers").get_children(): for site in activity_root.get_children():
destinations.append((marker as Marker3D).global_position) destinations.append((site as ActivitySite).get_interaction_position())
for origin in origins: for origin in origins:
for destination in destinations: for destination in destinations:
@@ -187,10 +200,15 @@ func _run() -> void:
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01, storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
"Storage actions should use the pantry StorageNode interaction point" "Storage actions should use the pantry StorageNode interaction point"
) )
var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST)
_check(
rest_target.get("target_id", "") == "rest_bench",
"Rest should target the typed RestBench ActivitySite"
)
if failures.is_empty(): if failures.is_empty():
print( print(
"[TEST] Jajce scaffold passed: Terrain3D, storage site, shader water, building blockouts" "[TEST] Jajce scaffold passed: Terrain3D, storage/activity sites, shader water"
) )
quit(0) quit(0)
return return
@@ -68,6 +68,14 @@ func _run() -> void:
pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before, pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before,
"Player should eat from the typed pantry StorageNode" "Player should eat from the typed pantry StorageNode"
) )
var guard_site := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites/GuardPost") as ActivitySite
player.global_position = guard_site.get_interaction_position()
var safety_before: float = simulation_manager.village.safety
player.try_interact()
_check(
simulation_manager.village.safety > safety_before,
"Player should help guard from the typed GuardPost ActivitySite"
)
if failures.is_empty(): if failures.is_empty():
print("[TEST] ResourceNode player parity passed") print("[TEST] ResourceNode player parity passed")
+25 -10
View File
@@ -1,9 +1,6 @@
class_name ActiveWorldAdapter class_name ActiveWorldAdapter
extends Node extends Node
@export var guard_marker: Marker3D
@export var study_marker: Marker3D
@export var rest_marker: Marker3D
@export var pantry_storage: StorageNode @export var pantry_storage: StorageNode
@@ -18,22 +15,40 @@ func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
return candidates return candidates
func get_activity_target(action_id: StringName) -> Dictionary: func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO) -> Dictionary:
var marker: Marker3D
match action_id: match action_id:
SimulationIds.ACTION_PATROL: SimulationIds.ACTION_PATROL:
marker = guard_marker return _get_activity_site_target(action_id, origin)
SimulationIds.ACTION_STUDY: SimulationIds.ACTION_STUDY:
marker = study_marker return _get_activity_site_target(action_id, origin)
SimulationIds.ACTION_REST: SimulationIds.ACTION_REST:
marker = rest_marker return _get_activity_site_target(action_id, origin)
SimulationIds.ACTION_EAT: SimulationIds.ACTION_EAT:
return _get_storage_target(pantry_storage) return _get_storage_target(pantry_storage)
SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD: SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD:
return _get_storage_target(pantry_storage) return _get_storage_target(pantry_storage)
if marker == null: return {}
func _get_activity_site_target(action_id: StringName, origin: Vector3) -> Dictionary:
var best_site: ActivitySite
var best_distance := INF
for candidate in ActivitySite.get_all():
var site := candidate as ActivitySite
if site == null:
continue
if not site.supports_action(action_id):
continue
var distance := origin.distance_squared_to(site.get_interaction_position())
if distance < best_distance:
best_distance = distance
best_site = site
if best_site == null:
return {} return {}
return {"target_id": "", "position": marker.global_position} return {
"target_id": String(best_site.site_id),
"position": best_site.get_interaction_position()
}
func _get_storage_target(storage_node: StorageNode) -> Dictionary: func _get_storage_target(storage_node: StorageNode) -> Dictionary:
+76
View File
@@ -0,0 +1,76 @@
class_name ActivitySite
extends Node3D
static var _all: Array = []
@export var site_id: StringName
@export var action_id: StringName = SimulationIds.ACTION_REST
@export var display_name := "Activity Site"
@export_range(1, 12, 1) var capacity := 1
@export var debug_label_enabled := true
@onready var interaction_point: Marker3D = $InteractionPoint
var _last_label_text := ""
func _ready() -> void:
if site_id.is_empty():
push_error("ActivitySite at %s has empty site_id" % get_path())
_update_presentation()
return
var existing := get_by_id(site_id)
if existing != null:
push_error(
(
"Duplicate ActivitySite site_id '%s' at %s; first registered at %s"
% [site_id, get_path(), existing.get_path()]
)
)
_update_presentation()
return
_all.append(self)
add_to_group("activity_sites")
_update_presentation()
func _process(_delta: float) -> void:
if debug_label_enabled:
_update_presentation()
func _exit_tree() -> void:
_all.erase(self)
func get_interaction_position() -> Vector3:
return interaction_point.global_position if interaction_point != null else global_position
func supports_action(candidate_action_id: StringName) -> bool:
return action_id == candidate_action_id
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\ncap:%d" % [display_name, action_id, capacity]
if text == _last_label_text:
return
_last_label_text = text
label.text = text
static func get_by_id(search_id: StringName):
for node in _all:
if node.site_id == search_id:
return node
return null
static func get_all() -> Array:
return _all.duplicate()
+58 -10
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=25 format=3] [gd_scene load_steps=26 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"]
@@ -14,6 +14,7 @@
[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"] [ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = { _shader_parameters = {
@@ -251,29 +252,76 @@ yield_per_action = 3.0
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")] [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
position = Vector3(0, 0, -8) position = Vector3(0, 0, -8)
[node name="LegacyActivityMarkers" type="Node3D" parent="."] [node name="ActivitySites" type="Node3D" parent="WorldObjects"]
[node name="GuardZone" type="Marker3D" parent="LegacyActivityMarkers"] [node name="GuardPost" type="Node3D" parent="WorldObjects/ActivitySites"]
position = Vector3(0, 0, 8) position = Vector3(0, 0, 8)
script = ExtResource("15_activity")
site_id = &"guard_post"
action_id = &"patrol"
display_name = "Guard Post"
capacity = 2
[node name="GuardTowerVisual" type="MeshInstance3D" parent="LegacyActivityMarkers/GuardZone"] [node name="GuardTowerVisual" type="MeshInstance3D" parent="WorldObjects/ActivitySites/GuardPost"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
mesh = SubResource("Mesh_guard_post") mesh = SubResource("Mesh_guard_post")
[node name="StudyZone" type="Marker3D" parent="LegacyActivityMarkers"] [node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/GuardPost"]
position = Vector3(-6, 0, 6)
[node name="StudyDeskVisual" type="MeshInstance3D" parent="LegacyActivityMarkers/StudyZone"] [node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/GuardPost"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 0)
billboard = 1
no_depth_test = true
font_size = 18
outline_modulate = Color(0, 0, 0, 1)
outline_size = 4
text = "Guard Post"
[node name="StudyDesk" type="Node3D" parent="WorldObjects/ActivitySites"]
position = Vector3(-6, 0, 6)
script = ExtResource("15_activity")
site_id = &"study_desk"
action_id = &"study"
display_name = "Study Desk"
[node name="StudyDeskVisual" type="MeshInstance3D" parent="WorldObjects/ActivitySites/StudyDesk"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.125, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.125, 0)
mesh = SubResource("Mesh_study_desk") mesh = SubResource("Mesh_study_desk")
[node name="RestZone" type="Marker3D" parent="LegacyActivityMarkers"] [node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/StudyDesk"]
position = Vector3(4, 0, 5)
[node name="RestBenchVisual" type="MeshInstance3D" parent="LegacyActivityMarkers/RestZone"] [node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/StudyDesk"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
billboard = 1
no_depth_test = true
font_size = 18
outline_modulate = Color(0, 0, 0, 1)
outline_size = 4
text = "Study Desk"
[node name="RestBench" type="Node3D" parent="WorldObjects/ActivitySites"]
position = Vector3(4, 0, 5)
script = ExtResource("15_activity")
site_id = &"rest_bench"
action_id = &"rest"
display_name = "Rest Bench"
capacity = 2
[node name="RestBenchVisual" type="MeshInstance3D" parent="WorldObjects/ActivitySites/RestBench"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0)
mesh = SubResource("Mesh_rest_bench") mesh = SubResource("Mesh_rest_bench")
[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/RestBench"]
[node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/RestBench"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
billboard = 1
no_depth_test = true
font_size = 18
outline_modulate = Color(0, 0, 0, 1)
outline_size = 4
text = "Rest Bench"
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
rotation_degrees = Vector3(-55, -35, 0) rotation_degrees = Vector3(-55, -35, 0)
light_color = Color(1, 0.87, 0.72, 1) light_color = Color(1, 0.87, 0.72, 1)
+3 -6
View File
@@ -91,15 +91,12 @@ func _update_presentation() -> void:
label.text = text label.text = text
static func get_by_id(search_id: StringName) -> StorageNode: static func get_by_id(search_id: StringName):
for node in _all: for node in _all:
if node.storage_id == search_id: if node.storage_id == search_id:
return node return node
return null return null
static func get_all() -> Array[StorageNode]: static func get_all() -> Array:
var nodes: Array[StorageNode] = [] return _all.duplicate()
for node in _all:
nodes.append(node as StorageNode)
return nodes
+1
View File
@@ -0,0 +1 @@
uid://cgxnb8qsttpqy