diff --git a/docs/ACTION_SYSTEM_ARCHITECTURE.md b/docs/ACTION_SYSTEM_ARCHITECTURE.md index 7d4de58..d4d91f5 100644 --- a/docs/ACTION_SYSTEM_ARCHITECTURE.md +++ b/docs/ACTION_SYSTEM_ARCHITECTURE.md @@ -39,12 +39,13 @@ SimulationManager tick - consumes active-world facts through ActiveWorldAdapter; - checks simulation-owned ResourceStateRecord availability; - 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 - 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; - does not choose actions or reserve resources. diff --git a/docs/BUILD_IN_PUBLIC_PLAN.md b/docs/BUILD_IN_PUBLIC_PLAN.md index 83aed8f..e12426f 100644 --- a/docs/BUILD_IN_PUBLIC_PLAN.md +++ b/docs/BUILD_IN_PUBLIC_PLAN.md @@ -41,8 +41,8 @@ Do not redo these tasks: - hunger, energy, starvation, death, task scoring, and task duration; - task-change and village-change signals; - visual NPC navigation and arrival reporting; -- finite berry/tree ResourceNodes plus food, guard, study, and rest activity - markers; +- finite berry/tree ResourceNodes plus typed food, guard, study, and rest + world sites; - aggregate village UI; - initial tree assets; - a baked navigation region for the current flat test area. @@ -56,21 +56,20 @@ plugin content. They are references, not the game. ```text Main -├── current flat World and NavigationRegion3D +├── JajceWorld ├── Player ├── CameraRig ├── SimulationManager ├── WorldViewManager ├── ActiveNPCs -├── ActivityMarkers +├── typed world sites inside JajceWorld/WorldObjects ├── WorldEnvironment └── UI ``` -The player and `WorldViewManager` still reference non-resource activity sites -through exported `NodePath` values. `NpcVisual` relies on the navigation map to -reach legacy guard/study/rest markers, `ResourceNode` interaction points, and -the typed village pantry `StorageNode`. +The player still references interactable world sites through exported `NodePath` +values. `NpcVisual` relies on the navigation map to reach typed `ActivitySite`, +`StorageNode`, and `ResourceNode` interaction points. Replacing the ground without respecting those references would break behavior that already works. @@ -95,12 +94,12 @@ Main │ │ ├── ResourceNodes │ │ ├── BerryBush instances │ │ └── Tree instances -│ │ └── StorageSites -│ │ └── VillagePantry -│ ├── LegacyActivityMarkers temporary non-resource activities -│ │ ├── GuardZone -│ │ ├── StudyZone -│ │ └── RestZone +│ │ ├── StorageSites +│ │ │ └── VillagePantry +│ │ └── ActivitySites +│ │ ├── GuardPost +│ │ ├── StudyDesk +│ │ └── RestBench │ ├── DirectionalLight3D │ └── WorldEnvironment ├── Player @@ -111,11 +110,10 @@ Main └── UI ``` -`JajceWorld` should contain geography, world-object presentations, and temporary -legacy interaction locations. The pantry has since moved out of -`LegacyActivityMarkers` into `WorldObjects/StorageSites/VillagePantry`; guard, -study, and rest remain temporary markers. `JajceWorld` should not own -`SimulationManager`, simulated NPC data, player state, or UI. +`JajceWorld` should contain geography and world-object presentations. The former +legacy interaction locations now live as typed storage and activity sites under +`WorldObjects`; `JajceWorld` should not own `SimulationManager`, simulated NPC +data, player state, or UI. Create a small look-development wrapper that instances the same world: @@ -295,7 +293,7 @@ Create `world/jajce/JajceWorld.tscn` with: - placeholder `VillageRoot`; - placeholder `FoliageRoot`; - a navigation region; -- resource-node containers plus temporary activity markers; +- resource-node, storage-site, and activity-site containers; - lighting and environment. 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. 3. Place or move bushes and trees under `JajceWorld/WorldObjects/ResourceNodes` while preserving stable IDs. -4. Reconnect only remaining non-resource activity markers under - `JajceWorld/LegacyActivityMarkers`. +4. Reconnect remaining non-resource activities as typed sites under + `JajceWorld/WorldObjects`. 5. Bake navigation for the new walkable area. 6. Test each existing task from multiple spawn positions. 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. **Status: runtime integration complete.** `main.tscn` now instances the reusable -world and points player/simulation adapters at its stable-ID resources and -remaining activity markers. The duplicate flat world objects were removed. +world and points player/simulation adapters at its stable-ID resources, storage +site, and activity sites. The duplicate flat world objects were removed. The runtime regression checks Terrain3D presence, 24 navigation routes, core task execution, starvation/death, player extraction parity, reservations, and NPC visual lifecycle. Terrain sculpting and presentation polish remain @@ -445,7 +443,7 @@ separate work, not blockers for this wiring milestone. ### 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. ### Phase 5 — Make the simulation readable on video diff --git a/docs/LEARNING_ROADMAP.md b/docs/LEARNING_ROADMAP.md index 4f06e3d..ad34e75 100644 --- a/docs/LEARNING_ROADMAP.md +++ b/docs/LEARNING_ROADMAP.md @@ -652,9 +652,11 @@ Completed after the architecture gate: The practical next sequence is: -1. Replace the remaining generic activity markers with typed world sites, - beginning with the pantry/storage presentation. -2. Add a cinematic/debug toggle and repeatable simulation-garden demo reset. +1. Add a cinematic/debug toggle and repeatable simulation-garden demo reset. +2. Expand resource discovery with many finite `ResourceNode` instances placed + 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 creates a concrete requirement. diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index da5c2b9..c21b131 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -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 player assistance. NPC `gather_food` and `gather_wood` tasks now use finite world `ResourceNode` instances (berry bushes and trees) rather than abstract -zone markers. The obsolete farm and forest markers have been removed. Separate -temporary activity markers remain for rest, study, and patrol; eating and food -transfer now target the typed village pantry. The migration is documented in +zone markers. The obsolete farm, forest, and activity markers have been +removed. Eating and food transfer target the typed village pantry, while rest, +study, and patrol target typed activity sites. The migration is documented in [the ResourceNode plan](RESOURCE_NODE_MIGRATION.md). ## Current technology @@ -207,7 +207,7 @@ plugin content, not game architecture. ### 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 geometry. - WASD movement is camera-relative. @@ -215,17 +215,19 @@ plugin content, not game architecture. follow/focus behavior. - Pressing `E` near a berry bush or tree extracts its configured yield into the village through the same `ResourceNode` contract used by NPCs. -- Guard, study, rest, and food interactions still use temporary activity - markers. +- Guard, study, rest, and food interactions now use typed world sites. - `Escape` releases captured mouse input. ### Village simulation -The simulation currently creates three NPCs: +The simulation currently creates six NPCs: - Amina - Tarik - Jasmin +- Elma +- Mirza +- Lejla Each receives a random profession from: @@ -283,39 +285,34 @@ Needs override ordinary work. Otherwise a utility-like score combines: - profession preference; - a small random contribution. -The visual NPC walks toward the task's marker. Arrival tells the simulation to -start work. Work progresses on simulation ticks. The village receives the -result only when the task duration completes. +The visual NPC walks toward the task's typed target position. Arrival tells the +simulation to start work. Work progresses on simulation ticks. The village +receives the result only when the task duration completes. Starvation can reduce productivity and eventually kill an NPC. Dead visuals 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, -and rest. The former farm and forest zones and their decorative geometry have -been deleted, and food storage now uses a typed pantry site. +The former farm, forest, food, guard, study, and rest markers have been deleted. +The runtime now uses semantic world targets: **Migration status:** NPC `gather_food` and `gather_wood` target `ResourceNode` -instances (berry bushes and trees), with no marker fallback. The remaining -markers are direct transitional targets for non-resource NPC tasks, while -storage actions now route to a typed pantry site: +instances (berry bushes and trees), with no marker fallback. Non-resource +actions route through typed sites: -- patrol → guard marker; -- study → study marker; -- rest → rest marker; +- patrol → `ActivitySite` (`guard_post`); +- study → `ActivitySite` (`study_desk`); +- rest → `ActivitySite` (`rest_bench`); - eat/deposit/withdraw → `StorageNode` (`village_pantry`). The player also harvests food and wood directly from nearby `ResourceNode` instances. Extraction returns the actual amount removed, updates the village by 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. - -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. +Future resource expansion should add many finite `ResourceNode` instances rather +than broad resource zones: trees in foliage clusters, berry patches, animal +camps, and village stockpiles can be ranked by reachability, distance, safety, +profession, and NPC comfort range. ### Jajce scaffold @@ -327,8 +324,7 @@ marker. - fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke; - warm sky, fog, shadows, and wind-reactive foliage; - four ResourceNodes preserving the flat-map stable IDs; -- typed village pantry storage plus remaining guard, study, and rest activity - markers; +- typed village pantry storage and typed guard, study, and rest activity sites; - a temporary baked navigation loop with 24 tested routes; - 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: -- Action and profession IDs are stable and definition-backed, but activity - marker mapping and several action effects remain hard-coded. -- Temporary activity markers remain for rest, study, and patrol; NPC and player - food/wood gathering use `ResourceNode` instances with no fallback, while food - transfer uses the typed pantry `StorageNode`. +- Action and profession IDs are stable and definition-backed, but several + action effects remain hard-coded. +- Temporary activity markers have been removed; NPC and player food/wood + gathering use `ResourceNode` instances with no fallback, food transfer uses + the typed pantry `StorageNode`, and patrol/study/rest use `ActivitySite`. - Current NPC, village, resource, storage, event, clock, and RNG state serialize through world schema v3. F5/F9 provide one validated local quicksave; a save menu, metadata, and player-transform persistence remain deferred. diff --git a/docs/README.md b/docs/README.md index eb78337..485a86b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,8 +13,8 @@ sources of truth. visual slice. It must respect the architecture gates in the learning roadmap. 4. [`RESOURCE_NODE_MIGRATION.md`](RESOURCE_NODE_MIGRATION.md) is a focused - migration plan. Phases 1–5 are complete; Phase 6 tracks replacement of the - remaining temporary activity markers. + migration plan. Phases 1–6 are complete; follow-up work should expand + resource discovery without reintroducing abstract resource zones. 5. [`ACTION_SYSTEM_ARCHITECTURE.md`](ACTION_SYSTEM_ARCHITECTURE.md), [`ECONOMIC_EVENTS.md`](ECONOMIC_EVENTS.md), [`FOOD_STORAGE_ARCHITECTURE.md`](FOOD_STORAGE_ARCHITECTURE.md), diff --git a/docs/RESOURCE_NODE_MIGRATION.md b/docs/RESOURCE_NODE_MIGRATION.md index c2b0b67..6bb9794 100644 --- a/docs/RESOURCE_NODE_MIGRATION.md +++ b/docs/RESOURCE_NODE_MIGRATION.md @@ -10,7 +10,7 @@ | **4a** | Wood target selection (NPC) | ✅ Complete | | **4b** | Player parity (extraction contract) | ✅ 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 @@ -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 at the same time. -The old resource zones have been removed. Temporary activity markers remain -only for actions whose real target type does not exist yet. They are not -fallbacks and are not part of the target architecture. +The old resource zones and temporary activity markers have been removed. Food +transfer targets `StorageNode`; rest, study, and patrol target `ActivitySite`. +These are not resource fallbacks and are not resource-search areas. ## Why this is the next systems step @@ -522,10 +522,10 @@ Remove each old zone only when its replacement has: - debug visibility; - save representation. -**Progress:** the `TaskZone` container and obsolete `FarmZone`/`ForestZone` -nodes have been deleted from `main.tscn`. The surviving nodes live under -`LegacyActivityMarkers` to make their transitional role explicit. Task-to-marker -mapping still exists for rest, study, and patrol, so this phase is not complete. +**Progress:** the `TaskZone` container, obsolete `FarmZone`/`ForestZone` nodes, +and final `LegacyActivityMarkers` container have been deleted from `main.tscn` +and `JajceWorld`. Task-to-marker mapping has been replaced by typed site +queries. The former `FoodZone`/`PantryMarker` is now an authored `StorageNode` at `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 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, 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 At minimum, exercise: diff --git a/docs/baselines/FLAT_MAP_BASELINE.md b/docs/baselines/FLAT_MAP_BASELINE.md index c857a43..145e5fb 100644 --- a/docs/baselines/FLAT_MAP_BASELINE.md +++ b/docs/baselines/FLAT_MAP_BASELINE.md @@ -12,10 +12,10 @@ same contract now runs against the integrated `JajceWorld` runtime. The automated baseline verifies: -- the scene creates three simulated NPCs; +- the scene creates six simulated NPCs; - four stable-ID ResourceNodes register; -- three fixed origins can reach the four remaining activity markers and four - ResourceNode interaction points (24 route checks); +- three fixed origins can reach the typed pantry, three typed activity sites, + and four ResourceNode interaction points (24 route checks); - a two-tick working task completes; - 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 wood extraction uses the configured yield; - player depletion releases an NPC reservation. +- player pantry and guard interactions use typed world sites. ## Commands diff --git a/main.tscn b/main.tscn index 5474ac1..87bb317 100644 --- a/main.tscn +++ b/main.tscn @@ -23,14 +23,14 @@ height = 1.7 [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) script = ExtResource("1_h2yge") camera_rig = NodePath("../CameraRig") simulation_manager = NodePath("../SimulationManager") pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry") -guard_zone = NodePath("../JajceWorld/LegacyActivityMarkers/GuardZone") -study_zone = NodePath("../JajceWorld/LegacyActivityMarkers/StudyZone") +guard_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/GuardPost") +study_site = NodePath("../JajceWorld/WorldObjects/ActivitySites/StudyDesk") stomach_capacity_for_food = 10 [node name="CollisionShape3D" type="CollisionShape3D" parent="Player" unique_id=1249497228] @@ -57,11 +57,8 @@ deadzone_forward = 2.0 current = true 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") -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") [node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")] diff --git a/player/player.gd b/player/player.gd index 15f29ee..8410947 100644 --- a/player/player.gd +++ b/player/player.gd @@ -8,8 +8,8 @@ extends CharacterBody3D @export var simulation_manager: Node @export var pantry_storage: StorageNode -@export var guard_zone: Marker3D -@export var study_zone: Marker3D +@export var guard_site: ActivitySite +@export var study_site: ActivitySite @export var stomach_capacity_for_food := 5 @export var interaction_range := 3.0 @@ -59,10 +59,10 @@ func try_interact() -> void: if try_harvest_resource_node(): return - if is_near(guard_zone): + if is_near_activity_site(guard_site): simulation_manager.add_safety(3.0) print("Player helped guard the village.") - elif is_near(study_zone): + elif is_near_activity_site(study_site): simulation_manager.add_knowledge(2.0) print("Player shared knowledge.") elif is_near_storage(pantry_storage): @@ -112,3 +112,10 @@ func is_near_storage(storage_node: StorageNode) -> bool: return false 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 diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 03bb31b..9312815 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -432,7 +432,8 @@ func get_pantry() -> StorageStateRecord: 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) diff --git a/simulation/actions/ActionTargetResolver.gd b/simulation/actions/ActionTargetResolver.gd index 681e831..fd63160 100644 --- a/simulation/actions/ActionTargetResolver.gd +++ b/simulation/actions/ActionTargetResolver.gd @@ -14,7 +14,7 @@ func resolve( npc, origin, definition, simulation_manager, active_world_adapter ) 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: return { "target_id": "", "position": origin + simulation_manager.get_wander_offset(npc.id) diff --git a/tests/jajce_runtime_integration_test.gd b/tests/jajce_runtime_integration_test.gd index f52ba62..cf83610 100644 --- a/tests/jajce_runtime_integration_test.gd +++ b/tests/jajce_runtime_integration_test.gd @@ -31,6 +31,7 @@ func _run() -> void: not main_scene.has_node("World") and not main_scene.has_node("ResourceNodes") and not main_scene.has_node("ActivityMarkers") + and not main_scene.has_node("JajceWorld/LegacyActivityMarkers") ), "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 destinations.append(pantry.get_interaction_position()) - for marker_path in [ - "JajceWorld/LegacyActivityMarkers/GuardZone", - "JajceWorld/LegacyActivityMarkers/StudyZone", - "JajceWorld/LegacyActivityMarkers/RestZone" - ]: - destinations.append(main_scene.get_node(marker_path).global_position) + var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites") + _check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites") + for site in activity_root.get_children(): + destinations.append((site as ActivitySite).get_interaction_position()) for resource in resource_root.get_children(): 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), "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(): - 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) return diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index e6e4259..16507f4 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -127,6 +127,19 @@ func _run() -> void: not world.has_node("LegacyActivityMarkers/PantryMarker"), "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_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid()) @@ -143,8 +156,8 @@ func _run() -> void: for resource in resources: destinations.append((resource as ResourceNode).interaction_point.global_position) destinations.append(pantry.get_interaction_position()) - for marker in world.get_node("LegacyActivityMarkers").get_children(): - destinations.append((marker as Marker3D).global_position) + for site in activity_root.get_children(): + destinations.append((site as ActivitySite).get_interaction_position()) for origin in origins: for destination in destinations: @@ -187,10 +200,15 @@ func _run() -> void: storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01, "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(): print( - "[TEST] Jajce scaffold passed: Terrain3D, storage site, shader water, building blockouts" + "[TEST] Jajce scaffold passed: Terrain3D, storage/activity sites, shader water" ) quit(0) return diff --git a/tests/resource_node_player_parity_test.gd b/tests/resource_node_player_parity_test.gd index 57cf2eb..df72360 100644 --- a/tests/resource_node_player_parity_test.gd +++ b/tests/resource_node_player_parity_test.gd @@ -68,6 +68,14 @@ func _run() -> void: pantry_state.get_amount(SimulationIds.RESOURCE_FOOD) < pantry_food_before, "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(): print("[TEST] ResourceNode player parity passed") diff --git a/world/active_world_adapter.gd b/world/active_world_adapter.gd index 82ae0d3..b46500b 100644 --- a/world/active_world_adapter.gd +++ b/world/active_world_adapter.gd @@ -1,9 +1,6 @@ class_name ActiveWorldAdapter extends Node -@export var guard_marker: Marker3D -@export var study_marker: Marker3D -@export var rest_marker: Marker3D @export var pantry_storage: StorageNode @@ -18,22 +15,40 @@ func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: return candidates -func get_activity_target(action_id: StringName) -> Dictionary: - var marker: Marker3D +func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO) -> Dictionary: match action_id: SimulationIds.ACTION_PATROL: - marker = guard_marker + return _get_activity_site_target(action_id, origin) SimulationIds.ACTION_STUDY: - marker = study_marker + return _get_activity_site_target(action_id, origin) SimulationIds.ACTION_REST: - marker = rest_marker + return _get_activity_site_target(action_id, origin) SimulationIds.ACTION_EAT: return _get_storage_target(pantry_storage) SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD: 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 {"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: diff --git a/world/activity/ActivitySite.gd b/world/activity/ActivitySite.gd new file mode 100644 index 0000000..5bc70c9 --- /dev/null +++ b/world/activity/ActivitySite.gd @@ -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() diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index 7ccf6bc..9aa4447 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -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="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="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="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"] _shader_parameters = { @@ -251,29 +252,76 @@ yield_per_action = 3.0 [node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")] 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) +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) mesh = SubResource("Mesh_guard_post") -[node name="StudyZone" type="Marker3D" parent="LegacyActivityMarkers"] -position = Vector3(-6, 0, 6) +[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/GuardPost"] -[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) mesh = SubResource("Mesh_study_desk") -[node name="RestZone" type="Marker3D" parent="LegacyActivityMarkers"] -position = Vector3(4, 0, 5) +[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/StudyDesk"] -[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) 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="."] rotation_degrees = Vector3(-55, -35, 0) light_color = Color(1, 0.87, 0.72, 1) diff --git a/world/storage/StorageNode.gd b/world/storage/StorageNode.gd index 9a8b1bb..5fa4846 100644 --- a/world/storage/StorageNode.gd +++ b/world/storage/StorageNode.gd @@ -91,15 +91,12 @@ func _update_presentation() -> void: label.text = text -static func get_by_id(search_id: StringName) -> StorageNode: +static func get_by_id(search_id: StringName): 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 +static func get_all() -> Array: + return _all.duplicate() diff --git a/world/storage/StorageNode.gd.uid b/world/storage/StorageNode.gd.uid new file mode 100644 index 0000000..ddeb50f --- /dev/null +++ b/world/storage/StorageNode.gd.uid @@ -0,0 +1 @@ +uid://cgxnb8qsttpqy