Files
gamedev-the-steward/docs/ACTION_SYSTEM_ARCHITECTURE.md
T
2026-08-03 12:47:54 +02:00

12 KiB

The Steward — Action System Architecture

Current flow

SimulationManager tick
    -> ActionExecutionSystem advances needs and working progress
    -> SimulationPopulationView refreshes transient living/starving indexes
    -> VillageOpportunitySystem re-derives the current capable helper
    -> ActionSelectionSystem chooses an action when the NPC is idle
    -> SimulationManager stores the selected action
    -> npc_target_requested
    -> WorldViewManager supplies the active visual's current position
    -> ActionTargetResolver consumes simulation state + ActiveWorldAdapter facts
    -> SimulationManager stores/reserves the target
    -> npc_travel_requested
    -> WorldViewManager commands NpcVisual travel
    -> arrival/navigation failure returns to SimulationManager

Responsibilities

ActionSelectionSystem

  • reads NPC and village state;
  • queries RelationshipSystem after personal survival/schedule overrides, so a trusted starving acquaintance can redirect ordinary work toward food gathering while the pantry is low;
  • consumes the manager's current SimulationPopulationView for that social query instead of rebuilding an all-NPC lookup for each idle decision;
  • accepts the current ephemeral OpportunityHelperResult and, only for its matching NPC, selects the reported ordinary gather/deposit action after urgent self-care and sleep/meal schedule branches;
  • evaluates current utility scores;
  • consumes the NPC's deterministic decision RNG;
  • returns an action ID, optional urgent-duration override, branch reason, compared utility scores, and definition-backed rejection reasons;
  • does not mutate targets, navigation, or visuals.

ActionExecutionSystem

  • advances hunger, energy, starvation, death, and working progress;
  • marks an action complete when its duration elapses;
  • does not select the next action or resolve a target.

ActionTargetResolver

  • reads target metadata from ActionDefinition;
  • consumes active-world facts through ActiveWorldAdapter;
  • expands resource queries through nearby grid ranges and prunes farther cells only when authoritative risk/comfort/priority bounds prove they cannot win;
  • preserves the former registration-order tie behavior and keeps a linear compatibility path for isolated adapters;
  • checks simulation-owned ResourceStateRecord availability;
  • reserves and returns stable resource target IDs;
  • reserves the exact loaded animal before returning the pantry pickup waypoint, then resolves only that same claim for the delivery leg;
  • skips full-capacity activity sites based on authoritative NPC target claims;
  • returns typed activity-site, storage, or wander positions without owning presentation.

ActiveWorldAdapter

  • owns the disposable LoadedResourceSpatialIndex of loaded stable-ID resource interaction positions;
  • refreshes the index as ResourceNodes enter, leave, rebind authoritative state, or move;
  • exposes loaded storage and activity-site interaction positions;
  • exposes activity-site capacity as an active-world fact;
  • contains active-world query facts, not persistent mutable authority;
  • does not choose actions or reserve resources.

WorldViewManager

  • spawns and tracks NpcVisual instances;
  • supplies a visual's current position when resolution is requested;
  • applies travel commands;
  • reports arrival, navigation failure, and visual death state;
  • does not map actions, select targets, write target IDs, or mutate resources.

SimulationManager

SimulationManager remains the orchestrator and event boundary. It owns simulation records, invokes the focused systems, stores selected actions and targets, and translates presentation callbacks into simulation transitions. It also owns one disposable SimulationPopulationView, rebuilt at tick start and refreshed after each NPC advances so interleaved decision semantics remain unchanged. The manager publishes the latest ActionSelectionResult for presentation; the UI does not recompute decisions. Each idle selection re-derives the capable helper instead of consulting persisted assignment state. At completion it atomically pays any definition-backed stored-resource cost before applying the action effect. feed_animal is the one bounded compound exception: pantry arrival withdraws the exact cost into inventory and requests the already claimed animal as the second destination; goat arrival then enters normal working completion. A late shortfall suppresses the effect and records a task_blocked fact. The manager also captures actor/nearby knowledge before forwarding newly recorded events into RelationshipSystem. When an NPC arrives beside a worker at the same non-storage activity site, the manager may transfer one direct known fact and applies its consequence only to that newly informed listener. An open opportunity's exact trigger receives bounded priority in that conversation before normal lasting/recent ranking. It exposes knowledge, provenance, relationship, and cause queries without moving social authority into UI. After consequences are known, it protects current causal facts, enforces the recent-memory cap, and runs age review from authoritative simulation ticks.

VillageEconomy

  • owns storage and NPC-inventory transfer operations;
  • supports exact all-or-nothing pickup and carried-inventory removal for the animal-care cost;
  • keeps village.food and village.wood synchronized as aggregate views;
  • validates and pays definition-backed completion costs;
  • emits completed transaction facts without owning their history.

SimulationEventLog

  • owns ordered economic and narrative event identity;
  • answers recent-history, actor-history, and consumption-rate queries;
  • restores persisted history without performing or replaying transactions.

EventKnowledgeSystem

  • stores stable (knower_id, event_id) references separately from objective history;
  • initially observes successful food-deposit events for the actor and living NPCs within a fixed proximity radius;
  • preserves first acquisition as performed, witnessed, communicated, or legacy, with a stable source NPC ID for direct communication;
  • permits one performed/witnessed fact to cross one co-located social hop, but does not relay communicated or ambiguous legacy facts;
  • tries the active opportunity trigger first when the speaker directly knows it, then orders remaining communication by lasting relationship relevance and acquisition recency;
  • retains current relationship causes and at most three other recent facts per NPC, expiring day-old recent facts at deterministic review boundaries;
  • captures evidence only when the event happens, never from current positions during restore;
  • answers deterministic known-event and knower queries without formatting prose or choosing actions.

RelationshipSystem

  • owns directed familiarity/trust records and stable relationship queries;
  • applies a bounded trust gain when a hungry familiar NPC knows about a contributor's successful food-deposit event;
  • stores the exact event ID as the trust cause and ignores replayed or older events;
  • selects trusted starving subjects deterministically for action selection;
  • uses a supplied per-tick population view when available, with a compatibility fallback for isolated callers;
  • does not choose actions, perform transactions, or format presentation text.

These collaborators are RefCounted rule services, not additional scene-tree managers. Further decomposition should follow measured pressure and a proven gameplay consumer.

Player context-interaction contract

Player.get_interaction_context() derives one ephemeral PlayerInteractionResult in the same priority order used by E: hungry animal, usable finite resource, guard site, study site, then pantry. The result contains the exact stable target ID, action, display name, prompt copy, availability, and loaded target reference. try_interact() executes that single resolved result rather than maintaining a second target map.

PlayerInteractionHud formats the result and transient outcome feedback. It does not select targets, mutate simulation state, or enter the save record. State restore clears active feedback and re-derives the prompt from current simulation and loaded-world facts. The HUD is separate from development overlays so the embodied action remains legible in cinematic mode.

Player villager-inspection contract

WorldViewManager.find_nearest_active_npc_id() resolves only loaded, living NPC presentations within the player's bounded range. It compares presentation positions and uses stable NPC ID order for equal distances; it does not inspect tasks or choose actions. Player.get_nearby_villager_inspection() reacquires that ID from the manager's current SimNPC records and constructs one ephemeral VillagerInspectionResult from the authoritative name, action, task state, exact target ID, and carried inventory.

The optional decision reason is honest only when the current transient ActionSelectionResult still names the NPC's live action. A missing or stale trace renders Awaiting the next decision. VillagerFieldNoteHud formats that result in a separate player-facing layer, so observation never changes E priority or execution. Visual unload removes the context, while restore reacquires the newly reconstructed NPC record and derives saved facts again; neither the selection, result, nor HUD enters simulation state or checksums.

Compound animal-care delivery contract

NPC feed_animal remains one task and one exact animal claim across two travel legs. ActionTargetResolver first chooses and reserves the nearest eligible loaded animal but returns ActiveWorldAdapter's pantry interaction position when the caretaker lacks the definition cost. Pantry arrival atomically moves the missing amount into SimNPC.inventory; the existing inventory signal makes the carried-food prop visible. The resolver then looks up only the claimed animal and requests its current loaded interaction position.

The second leg is derived from carried inventory rather than a separate saved phase. Pickup occurs only when the completed saved waypoint still matches the current pantry waypoint. A compatible older route with empty inventory and a goat destination is first redirected to the pantry instead of withdrawing remotely. An older already-working feed state with no carried food recovers the same route on its first resumed tick and preserves its work progress. Before a successful pickup publishes inventory, event, or village signals, the saved destination already names the exact goat; synchronous observers therefore cannot capture carried food with a stale pantry waypoint. At the goat, completion removes the exact cost from the matching NPC inventory, mutates only that animal, releases its claim, and records the inventory-to-animal fact. If pantry contention loses, no partial transfer is allowed, one task_blocked fact names the pantry shortfall, and only that caretaker's goat claim is released. Player feeding remains the bounded direct pantry-to-animal path because player inventory is still deferred.

Active-position contract

NpcVisual emits active position changes after successful movement. WorldViewManager forwards those facts through SimulationManager.synchronize_npc_position(). Arrival, navigation failure, and visual unload also perform a final synchronization.

SimNPC stores both authoritative position and the resolved travel destination. The destination is versioned simulation state, so reloading a visual resumes the same route request without rerolling wander, resolving another resource, or changing a reservation.

tests/npc_visual_lifecycle_test.gd proves that visual unload/reload preserves action, target ID, travel destination, resource reservation, position, and state checksum.

Unloaded NPCs do not yet resolve abstract travel time; they remain in their current task state until an active visual or a future simulation-LOD travel system advances them.