518 lines
15 KiB
Markdown
518 lines
15 KiB
Markdown
# The Steward — ResourceNode Migration Plan
|
|
|
|
## Decision
|
|
|
|
The current task zones are temporary prototype scaffolding.
|
|
|
|
NPCs and the player should ultimately interact with actual world objects rather
|
|
than travel to one abstract marker per task:
|
|
|
|
```text
|
|
Current:
|
|
gather_food -> FarmZone -> village food increases
|
|
|
|
First migration:
|
|
gather_food -> choose BerryBush_01 -> travel to its interaction point
|
|
-> extract berries -> bush amount decreases
|
|
-> village food increases by the extracted amount
|
|
```
|
|
|
|
This migration should happen on the current flat map before the Jajce
|
|
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 zones remain only as a temporary fallback while actions are migrated.
|
|
They are not part of the target architecture.
|
|
|
|
## Why this is the next systems step
|
|
|
|
Resource nodes provide the first concrete bridge between:
|
|
|
|
- abstract simulation decisions;
|
|
- spatial target selection;
|
|
- active-world navigation;
|
|
- finite world state;
|
|
- visible cause and effect;
|
|
- player and NPC use of the same objects;
|
|
- future inventories, ownership, regeneration, and persistence.
|
|
|
|
They also make build-in-public footage more legible. A viewer can understand a
|
|
villager walking to a specific berry bush and depleting it more easily than a
|
|
villager entering an invisible “food zone.”
|
|
|
|
## Scope the first version correctly
|
|
|
|
The first `ResourceNode` supports only depletable material sources:
|
|
|
|
- berry bush or crop source -> food;
|
|
- tree or wood pile -> wood.
|
|
|
|
Do not initially use `ResourceNode` for:
|
|
|
|
- beds or campfires;
|
|
- guard posts;
|
|
- study locations;
|
|
- food storage;
|
|
- homes;
|
|
- generic workstations.
|
|
|
|
Those objects share target-selection behavior but not resource-extraction
|
|
semantics.
|
|
|
|
The likely family of world targets is:
|
|
|
|
```text
|
|
WorldActionTarget
|
|
├── ResourceNode finite extraction: bush, tree, ore
|
|
├── ActivitySite rest, patrol, study, socialize
|
|
├── StorageNode deposit and withdraw items
|
|
└── Workstation transform inputs into outputs
|
|
```
|
|
|
|
Do not implement this complete inheritance tree now. Build `ResourceNode` from
|
|
real requirements, then extract shared target behavior when a second target
|
|
type proves what is actually common.
|
|
|
|
## Fit with the current code
|
|
|
|
The existing flow is:
|
|
|
|
```text
|
|
SimNPC chooses a task string
|
|
-> SimulationManager emits npc_task_changed
|
|
-> WorldViewManager maps the task to one Marker3D
|
|
-> NpcVisual travels to that marker
|
|
-> arrival tells SimulationManager to start work
|
|
-> task duration completes
|
|
-> SimVillage applies a hard-coded task result
|
|
```
|
|
|
|
The migrated flow should become:
|
|
|
|
```text
|
|
SimNPC chooses an action
|
|
-> target resolver finds available matching ResourceNodes
|
|
-> one target is reserved for the NPC
|
|
-> NPC simulation stores the target's stable ID
|
|
-> NpcVisual travels to the target interaction point
|
|
-> arrival begins work
|
|
-> completion extracts the actual available amount
|
|
-> village receives exactly that amount
|
|
-> target updates or becomes depleted
|
|
-> reservation is released
|
|
```
|
|
|
|
During migration:
|
|
|
|
```text
|
|
gather_food -> prefer matching ResourceNode -> fallback FarmZone
|
|
gather_wood -> prefer matching ResourceNode -> fallback ForestZone
|
|
other tasks -> existing zones
|
|
```
|
|
|
|
Fallbacks should log clearly so accidental reliance on zones is visible.
|
|
|
|
## Repository-specific file layout
|
|
|
|
Keep scripts beside their feature scenes, matching the repository's existing
|
|
feature-oriented structure:
|
|
|
|
```text
|
|
world/
|
|
└── resource_nodes/
|
|
├── ResourceNode.gd
|
|
├── ResourceNode.gd.uid
|
|
└── ResourceNode.tscn
|
|
```
|
|
|
|
Add instances under the current `main.tscn` while developing:
|
|
|
|
```text
|
|
Main
|
|
├── ResourceNodes
|
|
│ ├── BerryBush_01
|
|
│ ├── BerryBush_02
|
|
│ ├── Tree_01
|
|
│ └── Tree_02
|
|
└── TaskZone temporary fallback
|
|
```
|
|
|
|
When `JajceWorld` is integrated, move or recreate the world-object instances
|
|
under:
|
|
|
|
```text
|
|
JajceWorld
|
|
└── WorldObjects
|
|
└── ResourceNodes
|
|
```
|
|
|
|
Target discovery must not depend on that exact scene path. Resource nodes should
|
|
register through a group or a small registry.
|
|
|
|
## First ResourceNode contract
|
|
|
|
The first node should answer:
|
|
|
|
- What is my stable world-object ID?
|
|
- Which action can use me?
|
|
- Which resource do I yield?
|
|
- How much remains?
|
|
- How much can one completed action extract?
|
|
- Am I enabled and usable?
|
|
- Who, if anyone, has reserved me?
|
|
- Where should an active character stand?
|
|
- Did extraction change or deplete me?
|
|
|
|
Recommended initial data:
|
|
|
|
```gdscript
|
|
class_name ResourceNode
|
|
extends Node3D
|
|
|
|
signal amount_changed(node_id: StringName, amount_remaining: float)
|
|
signal depleted(node_id: StringName)
|
|
signal reservation_changed(node_id: StringName, agent_id: int)
|
|
|
|
@export var node_id: StringName
|
|
@export var action_id: StringName = &"gather_food"
|
|
@export var resource_id: StringName = &"food"
|
|
@export var amount_remaining: float = 10.0
|
|
@export var yield_per_action: float = 2.0
|
|
@export var enabled: bool = true
|
|
@export var can_npcs_use: bool = true
|
|
@export var can_player_use: bool = true
|
|
@export var hide_visual_when_depleted: bool = false
|
|
|
|
@onready var interaction_point: Marker3D = $InteractionPoint
|
|
```
|
|
|
|
Use `StringName` IDs as a small migration step away from duplicated free-form
|
|
`String` values. Later, action and resource definitions can become custom
|
|
`Resource` assets without changing every world node's conceptual contract.
|
|
|
|
### Avoid duplicated state
|
|
|
|
Do not store a separately mutable `is_depleted` flag in the first version.
|
|
Derive it:
|
|
|
|
```gdscript
|
|
func is_depleted() -> bool:
|
|
return amount_remaining <= 0.0
|
|
```
|
|
|
|
If regeneration or non-depleting sources later require more states, introduce
|
|
them deliberately.
|
|
|
|
### Extraction result is authoritative
|
|
|
|
Extraction must return the amount actually removed:
|
|
|
|
```gdscript
|
|
func extract(requested_amount: float = yield_per_action) -> float:
|
|
if not can_extract():
|
|
return 0.0
|
|
|
|
var extracted := minf(requested_amount, amount_remaining)
|
|
amount_remaining -= extracted
|
|
# Emit signals and update presentation.
|
|
return extracted
|
|
```
|
|
|
|
The village receives this returned amount. Do not let both `ResourceNode` and
|
|
`SimVillage.apply_npc_task()` independently award the configured yield.
|
|
|
|
### Stable IDs
|
|
|
|
`node_id` must be:
|
|
|
|
- non-empty;
|
|
- unique inside the world;
|
|
- stable across save/load;
|
|
- independent of display name and scene path.
|
|
|
|
For the first hand-authored nodes, explicit IDs such as
|
|
`&"berry_bush_01"` are acceptable. Add startup validation for duplicates.
|
|
|
|
## Scene structure
|
|
|
|
The first reusable scene can remain small:
|
|
|
|
```text
|
|
ResourceNode (Node3D)
|
|
├── Visual (Node3D)
|
|
├── InteractionPoint (Marker3D)
|
|
└── DebugLabel (Label3D)
|
|
```
|
|
|
|
Use `Node3D` for `Visual` rather than requiring `MeshInstance3D`; this permits a
|
|
mesh, imported model, particles, or multiple visual children later.
|
|
|
|
The debug label should be controlled by a project/debug setting or inspector
|
|
toggle rather than a compile-time constant. It should show:
|
|
|
|
- node ID;
|
|
- resource;
|
|
- remaining amount;
|
|
- reservation owner;
|
|
- depleted/disabled state.
|
|
|
|
## Reservation rules
|
|
|
|
Without reservations, several NPCs can choose one nearly depleted bush and all
|
|
walk toward it.
|
|
|
|
The first version needs a single-user reservation:
|
|
|
|
```text
|
|
available -> reserved by agent -> in use -> released
|
|
```
|
|
|
|
A reservation must be released when:
|
|
|
|
- the task completes;
|
|
- the NPC dies;
|
|
- the target depletes;
|
|
- travel fails;
|
|
- the action is interrupted or replaced;
|
|
- the target is disabled;
|
|
- the NPC or visual is removed.
|
|
|
|
Reservation is not ownership. Ownership, permission, crime, and social
|
|
consequences belong to later systems.
|
|
|
|
## Target selection
|
|
|
|
The first resolver may choose the nearest available matching node. It must still
|
|
return a reason trace:
|
|
|
|
```text
|
|
candidate BerryBush_01: accepted, distance 8.2
|
|
candidate BerryBush_02: rejected, reserved by NPC 2
|
|
candidate BerryBush_03: rejected, depleted
|
|
selected BerryBush_01
|
|
```
|
|
|
|
Future scoring can include:
|
|
|
|
- travel cost;
|
|
- remaining yield;
|
|
- danger;
|
|
- tool requirements;
|
|
- ownership and permission;
|
|
- crowding;
|
|
- profession;
|
|
- expected completion time;
|
|
- knowledge of the target.
|
|
|
|
Do not implement those before the nearest-available version works.
|
|
|
|
## Simulation-state boundary
|
|
|
|
The first `ResourceNode` is a Godot world node and may temporarily own its
|
|
remaining amount. That is acceptable for the migration prototype, but it is not
|
|
the final persistence boundary.
|
|
|
|
Preserve these rules now:
|
|
|
|
- `SimNPC` stores a target ID, not a `Node` reference;
|
|
- simulation decisions do not store `NodePath`;
|
|
- visual movement receives only a resolved position;
|
|
- target extraction is coordinated through one adapter/registry;
|
|
- node state has an obvious serialization representation.
|
|
|
|
Later, authoritative resource state can move into serializable simulation
|
|
records while `ResourceNode` becomes its active-world presentation.
|
|
|
|
## Required changes by existing file
|
|
|
|
### `simulation/SimNPC.gd`
|
|
|
|
- add a selected target ID;
|
|
- clear it when returning to idle or changing actions;
|
|
- do not store `ResourceNode` references;
|
|
- preserve current task lifecycle during the first migration.
|
|
|
|
### `world/world_view_manager.gd`
|
|
|
|
- query available resource nodes for `gather_food` and `gather_wood`;
|
|
- reserve the selected target;
|
|
- send its interaction position to `NpcVisual`;
|
|
- use the old marker only when no valid node exists;
|
|
- distinguish arrival at a specific target from arrival at a generic task zone.
|
|
|
|
Target discovery can begin here, but should move into a focused resolver or
|
|
registry once selection has meaningful rules.
|
|
|
|
### `simulation/SimulationManager.gd`
|
|
|
|
- coordinate target completion and reservation release;
|
|
- extract from the selected node once;
|
|
- give `SimVillage` the actual result;
|
|
- handle interruption, death, and failure cleanup;
|
|
- prevent old hard-coded production from also firing.
|
|
|
|
### `simulation/SimVillage.gd`
|
|
|
|
- add an explicit resource-delta method;
|
|
- separate generic resource application from task-name matching;
|
|
- retain current action effects for non-migrated tasks.
|
|
|
|
### `player/player.gd`
|
|
|
|
- eventually resolve and use the same world targets as NPCs;
|
|
- do not add a second player-only harvest path with different rules.
|
|
|
|
Player use can follow NPC migration rather than block its first test.
|
|
|
|
## Migration phases
|
|
|
|
### Phase 1 — ResourceNode scene and registration
|
|
|
|
- create `ResourceNode.gd` and `ResourceNode.tscn`;
|
|
- create a `ResourceNodes` parent in `main.tscn`;
|
|
- place two bushes and two trees;
|
|
- give every node a stable unique ID;
|
|
- show debug state;
|
|
- expose group/registry discovery.
|
|
|
|
**Exit:** nodes register, validate IDs, change amount, deplete, and update debug
|
|
presentation without involving NPC logic.
|
|
|
|
### Phase 2 — Food target selection and reservation
|
|
|
|
- migrate only `gather_food`;
|
|
- find nearest available bush;
|
|
- reserve it;
|
|
- store its ID on the NPC;
|
|
- navigate to its interaction point;
|
|
- release on interruption and death;
|
|
- retain `FarmZone` fallback with a visible warning.
|
|
|
|
**Exit:** two NPCs do not select the same single-user bush, and an NPC can
|
|
replan when its chosen bush becomes unavailable.
|
|
|
|
### Phase 3 — Authoritative completion
|
|
|
|
- extract only after work duration completes;
|
|
- return actual extracted amount;
|
|
- apply that amount to village food;
|
|
- prevent double production;
|
|
- deplete and hide/alter the source as configured;
|
|
- choose a new source for later tasks.
|
|
|
|
**Exit:** resource conservation holds:
|
|
|
|
```text
|
|
total bush decrease == total village food increase
|
|
```
|
|
|
|
for the initial gather-food scenario.
|
|
|
|
### Phase 4 — Wood and player parity
|
|
|
|
- migrate `gather_wood`;
|
|
- configure tree visuals and yields;
|
|
- route player interaction through the same extraction contract;
|
|
- add clear feedback for unavailable or depleted targets.
|
|
|
|
**Exit:** food and wood no longer require their abstract zones during normal
|
|
play. NPC and player extraction obey the same availability and depletion rules.
|
|
|
|
### Phase 5 — Jajce world placement
|
|
|
|
- place bushes and trees as real world objects in `JajceWorld`;
|
|
- keep their IDs stable;
|
|
- validate interaction points against terrain and navigation;
|
|
- use the old task zones only for rest, eat, patrol, and study;
|
|
- verify target discovery does not rely on parent scene paths.
|
|
|
|
**Exit:** the existing food and wood loops work after moving from the flat map
|
|
to Terrain3D.
|
|
|
|
### Phase 6 — Remove the remaining zone model
|
|
|
|
This is not part of the first ResourceNode implementation.
|
|
|
|
Introduce the next concrete target types:
|
|
|
|
- storage for eating and item transfer;
|
|
- activity sites for rest, study, and guard work;
|
|
- workstations when production recipes begin.
|
|
|
|
Remove each old zone only when its replacement has:
|
|
|
|
- target selection;
|
|
- active interaction position;
|
|
- reservation/capacity;
|
|
- completion or ongoing-use behavior;
|
|
- failure/interruption handling;
|
|
- debug visibility;
|
|
- save representation.
|
|
|
|
**Exit:** `TaskZone` and task-to-marker mapping are deleted, not merely unused.
|
|
|
|
## Test scenarios
|
|
|
|
At minimum, exercise:
|
|
|
|
1. one NPC and one bush;
|
|
2. two NPCs and one bush;
|
|
3. one NPC and two bushes at different distances;
|
|
4. selected bush depletes before arrival;
|
|
5. selected bush depletes exactly on completion;
|
|
6. NPC dies while traveling;
|
|
7. NPC dies while working;
|
|
8. navigation fails;
|
|
9. node is disabled while reserved;
|
|
10. fallback zone is used because no matching node exists;
|
|
11. player and NPC contend for the same source;
|
|
12. duplicate node IDs are detected.
|
|
|
|
Use a fixed seed and concise reason tracing where possible.
|
|
|
|
## Video opportunities
|
|
|
|
The migration naturally creates strong short-form demonstrations:
|
|
|
|
- “My NPCs stopped walking to invisible work zones.”
|
|
- “Two hungry villagers want the same berry bush.”
|
|
- “This bush contains the food—the UI counter no longer creates it.”
|
|
- “What happens when the closest resource is already reserved?”
|
|
- “A depleted forest changes what every woodcutter decides.”
|
|
|
|
Show the beautiful result first, then reveal target selection, reservation, and
|
|
resource conservation through debug presentation.
|
|
|
|
## Immediate implementation order
|
|
|
|
1. Create `world/resource_nodes/ResourceNode.gd`.
|
|
2. Create `world/resource_nodes/ResourceNode.tscn`.
|
|
3. Add `Main/ResourceNodes`.
|
|
4. Place `BerryBush_01`, `BerryBush_02`, `Tree_01`, and `Tree_02`.
|
|
5. Add stable IDs and registration validation.
|
|
6. Verify extraction and depletion manually.
|
|
7. Add target ID to `SimNPC`.
|
|
8. Migrate `gather_food` target selection.
|
|
9. Add reservation and cleanup.
|
|
10. Apply actual extracted yield on completion.
|
|
11. Add fixed-seed regression scenarios.
|
|
12. Migrate wood.
|
|
13. Give the player the same extraction path.
|
|
14. Continue with the Jajce world scaffold.
|
|
|
|
## Definition of done for the first migration
|
|
|
|
- Food and wood use actual finite world objects.
|
|
- NPCs select available targets and reserve them.
|
|
- NPC state stores stable target IDs rather than nodes or scene paths.
|
|
- Active visuals navigate to explicit interaction points.
|
|
- Work duration completes before extraction.
|
|
- Village resources increase only by the amount actually extracted.
|
|
- Depletion changes availability and presentation.
|
|
- Failure, interruption, and death release reservations.
|
|
- Old food and forest zones remain only as logged fallbacks.
|
|
- The player and NPCs can use the same extraction contract.
|
|
- Behavior is inspectable and covered by reproducible scenarios.
|
|
|