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;
- 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.
+23 -25
View File
@@ -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
+5 -3
View File
@@ -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.
+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
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.
+2 -2
View File
@@ -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 15 are complete; Phase 6 tracks replacement of the
remaining temporary activity markers.
migration plan. Phases 16 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),
+20 -8
View File
@@ -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:
+4 -3
View File
@@ -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
+4 -7
View File
@@ -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")]
+11 -4
View File
@@ -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
+2 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+11 -7
View File
@@ -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
+21 -3
View File
@@ -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
@@ -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")
+25 -10
View File
@@ -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:
+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="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)
+3 -6
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cgxnb8qsttpqy