05 — Simulation & Game Modes
The same world document drives five distinct runtime experiences: Editor, Explorer, Cinematic, GM and First-Person Player. All five share a single simulation runtime that derives NPC behaviour, weather, time, and quests from
World.json+ live inputs.
0. Where we actually are (refreshed 2026-09-06)
The sections below describe the intended runtime design. This block records what is actually on
main, verified againstapps/web/src,apps/api/srcandgit log(PRs #26–#120). Ground truth for milestones issuperpowers/STATUS.md.
| Design element | Status | Where |
|---|---|---|
| Five mode shells (§8) — Editor / Explorer / Cinematic / GM / Replay | ✅ SHIPPED (PR #26, 2026-08-21) | components/modes/{ModeShell,ModeSwitcher,ExplorerHud,GmHud,GmClockRail,ReplayHud,ReplayEventOverlay}.tsx; routes /explorer/[worldId], /gm/[worldId], /replay/[worldId]; Cinematic = /editor/[worldId]/director. First-Person Player mode is ❌ — no pointer-lock / walk controller exists in packages/scene-view or apps/web. |
| Fog of war (§9) | ⚠️ Atmospheric overlay only | components/simulation/FogOverlay.tsx driven by useWorldStore.fogMode (off / dim / hide, written only by GmHud). Server-filtered per-region reveal shipped as Spec 11 Phase 4 (tags spec-11-phase-4-w1..w4-complete); the GM HUD exposes per-region reveal/hide (gm-hud-region-fog-*). Per-tile line-of-sight masking is not built. |
| GM tools (§8 GM mode) | ✅ 4/4 wired (2026-08-22) | GmHud.tsx: world clock (GmClockRail), NPC visibility toggle, spawn encounter, broadcast form. |
| Simulation event bus + replay log (§10) | ✅ SHIPPED (2026-08-22) | components/SimEventObservers.tsx (bus observers), lib/replay-log.ts (writer/reader), components/ReplayLogBoot.tsx mounted from app/layout.tsx; Explorer HUD shows a recent-events row. |
| Replay scene playback (§10) | ✅ SHIPPED (PR #66, 2026-08-23) | ReplayHud.tsx drives usePlaybackStore.selectShot() as the cursor moves (as-of-cursor semantics, same as weather replay). Deferred: NPC schedule replay — SimulationView does not consume schedule overrides and no per-frame log exists. |
| World clock / seasons / day-night (§2, §6) | ✅ SHIPPED | Tags spec-climate-w1-complete (2D) + spec-3d-seasonal-night-effects-complete (3D). |
| Weather engine (§5) | ✅ SHIPPED | Runtime set_weather trigger (schema 0.10.0) + weather particles in the 3D view (M2.6 / M7.11 T5-runtime, PR #18, tag m7-11-follow-ups-complete). |
| NPC schedules / memory / goals (§3) | ⚠️ Partial | Schedule editor exists (components/inspector/ScheduleEditor.tsx); AI-backed memory in apps/api/src/agent/ai-memory.ts (M7.3). Rule-based goal planner as designed in §3 is not built. |
| Quest runtime (§4) | ✅ SHIPPED | Spec 12 encounter runtime (tag spec-12-complete), real trigger handlers + dialogue trees (tag real-trigger-handlers-and-dialogue-complete, schema 0.10.0), POI↔dialogue binding (poi-part-6-complete, 0.11.0). |
| AI NPC layer (§12) | ✅ SHIPPED | M7.3 AI NPC dialogue + memory (tag m7-3-complete, schema 0.12.0) and M7.4 AI quest generation (tag m7-4-complete, 0.13.0) — apps/api/src/agent/{ai-dialogue,ai-memory,ai-quest,ai-quest-resolver,ai-budget}.ts. Player BYOK + plan-tier daily budget. |
| Factions (§7) | ⚠️ Data only | world.factionTerritories overlay (schema 0.8.0); no faction reputation runtime. |
| Multi-server simulation (§14) | ❌ Not started | — |
1. The simulation runtime
The runtime is a pure, deterministic event-sourcing engine over the canonical world model. It is the single source of truth for runtime state — never let the 3D engine or the editor UI hold their own simulation copy.
World.json (canonical)
│
▼
Simulation Runtime (deterministic)
│
├── Clock ─ world time, day/night, seasons
├── Weather ─ active weather profile per region
├── NPCs ─ positions, schedules, memory, goals
├── Quests ─ active stages, completion, rewards
├── Factions ─ reputation, events
└── Event Log ─ immutable history (replay source)
Every state mutation goes through the runtime. The 3D engine, the editor UI, and the cinematic director all subscribe to runtime events.
2. World clock
interface WorldClock {
/** Real seconds elapsed since the world opened. */
realTime: number;
/** In-world minutes elapsed (configurable speed: 1×, 10×, 60×, paused). */
inWorldTime: number;
/** 'HH:mm' string at the current in-world time. */
timeOfDay: string;
/** 'spring' | 'summer' | 'autumn' | 'winter' | null. */
season: Season | null;
/** Day number (1..N) within the active season. */
dayOfSeason: number;
}
The clock advances when the world is in any runtime mode. The user can pause, slow down, or speed up the clock from the bottom bar.
3. NPC simulation
interface NpcState {
id: string;
position: { x: number; y: number };
schedule: Array<{ from: string; to: string; at: string }>;
memory: Array<{ event: string; at: number; payload?: unknown }>;
goals: Array<{ id: string; status: 'pending' | 'active' | 'done' }>;
}
3.1 Schedules
Each NPC has a schedule. At any moment, the runtime resolves the NPC's location by
matching clock.timeOfDay against the schedule.
Example:
{
"name": "Captain Arvid",
"schedule": [
{ "from": "08:00", "to": "12:00", "at": "market" },
{ "from": "12:00", "to": "14:00", "at": "tavern" },
{ "from": "22:00", "to": "06:00", "at": "home" }
]
}
If no schedule entry matches, the NPC stays at their last known location.
3.2 Goals
Goals drive reactive behaviour:
sell goods— patrols the market during opening hours.find rumours— moves to the tavern at midday to overhear.defend_village— heads to the village gate when an alarm fires.
Goals are evaluated each tick; the runtime chooses the highest-priority unblocked goal and routes the NPC accordingly.
3.3 Memory
NPCs remember events. When a player burns a village, NPCs in the area record:
{ "event": "village_burned", "at": 1234, "payload": { "byPlayer": "p1" } }
Memory affects goals (e.g. find_rumours becomes avenge).
3.4 Dialogue
Dialogue lives in GAS-packaged dialogue trees:
gas://dialogue/arvid@1.0.0
├── nodes/
│ ├── greeting.json
│ ├── rumours.json
│ └── quest_offer.json
└── manifest.json
The dialogue runner resolves the tree based on NPC memory + player reputation.
4. Quest runtime
interface QuestState {
id: string;
status: 'inactive' | 'active' | 'completed' | 'failed';
currentStage: string | null;
history: Array<{ stage: string; at: number; outcome: 'success' | 'failure' }>;
}
Stages defined in World.json#quests[].stages[]:
find_region— succeeds when the player enters the target region.defeat_encounter— succeeds when the target encounter is defeated.collect_resource— succeeds when the player collects N of the target resource.talk_to_npc— succeeds when the dialogue node completes.visit_tile— succeeds when the player steps on the target tile.custom— payload-driven; the simulation emits an event the quest stage listens to.
Quests are visible in GM mode and in the player's companion app. Quest progression emits runtime events that any projection (cinematic, GM, export) can subscribe to.
5. Weather engine
interface WeatherState {
active: GasAssetId;
perRegion: Map<string, GasAssetId>;
intensity: number; // 0..1
transitionStartedAt: number;
}
Weather transitions are eased over 30 seconds (configurable). The 3D engine reads the active weather and applies particle systems, shader overrides, and ambient sound.
Region overrides (weather.overrides[]) let a desert region stay sunny while a
mountains region is stormy.
6. Day / night
Driven by World.json#world.lighting:
{
"dayNightCycle": true,
"sunrise": "06:00",
"sunset": "18:00"
}
The runtime publishes clock.timeOfDay; the 3D engine derives sun azimuth +
altitude. NPC schedules depend on timeOfDay.
7. Factions
interface FactionState {
id: string;
reputation: number; // -100..100
members: string[]; // NPC ids
controlledRegions: string[]; // Region ids
events: Array<{ kind: string; at: number; payload?: unknown }>;
}
Reputation is earned by completing quests, trading, and NPC interactions. Events broadcast to all members and to the cinematic recording.
8. Five modes
8.1 Editor mode
The default. Full tool palette. Simulation runs in the background but UI state shows the world in "author time" — paused or freely scrubbed.
8.2 Explorer mode
- 3D scene, free-fly camera.
- No time acceleration by default; user can speed up.
- All simulation events visible via a debug overlay (toggleable).
- Useful for previewing before showing to players.
8.3 Cinematic mode
- Plays
world.cameraPaths[]in sequence. - Drives the simulation clock at the configured speed (real-time or faster).
- Renders to a canvas + captures frames for export.
- See
06-cinematic-director.md.
8.4 GM mode
- 2D or 3D view of the world.
- Fog of war: regions the players haven't discovered are dimmed.
- GM tools: spawn encounter, fast-forward clock, broadcast message, toggle NPC visibility.
- Real-time notes pinned to regions / NPCs.
- Connected to adventure-craft multiplayer (Phase 6+).
8.5 Player mode
- First-person or third-person view.
- Read-only world (the player cannot edit).
- Inventory, quests, dialogue, character sheet.
- Optionally connected to a Game Master running GM mode in another tab.
8.6 Mobile companion mode
- Read-only.
- Inventory + quests + character sheet + mini-map.
- Tablet shows the GM's pinned notes.
9. Fog of war
interface FogState {
discoveredRegions: Set<string>; // regions the player has entered
discoveredTiles: Set<string>; // tile coords the player has seen
visibleToPlayers: boolean; // global flag (GM can toggle)
}
The 2D renderer dims undiscovered tiles. The 3D renderer hides undiscovered objects / regions entirely. GM mode shows everything.
10. Replay
Because the simulation is event-sourced, every world session is replayable.
interface SimulationLog {
worldId: string;
startedAt: number;
endedAt: number | null;
events: Array<{
t: number; // in-world time
kind: string;
payload: unknown;
}>;
}
Use cases:
- Cinematic replay — record a play session, then play it back as a cinematic.
- Bug reports — attach a replay log to a bug report.
- Sharing — share a recorded session as an interactive viewer.
- Branching — fork a replay log and try a different decision.
11. Determinism
Same input → same output. Same World.json + same runtime inputs → same event log.
This is mandatory for replay correctness.
Triggers that can violate determinism (e.g. real-time clock drift, animation RNG) must use a deterministic RNG seeded by the world id + tick number.
12. AI NPC layer (Phase 7)
A future enhancement: NPCs may be backed by an LLM for open-ended dialogue and goal generation. The LLM operates on top of the deterministic runtime; it can propose new goals, dialogue trees, and events, but every proposal must be committed through the runtime's event log to preserve determinism.
Memory of NPC interactions is summarised into the NPC's memory array on
session end, so future sessions see consistent behaviour.
13. Performance
- The runtime is pure: no DOM, no Three.js. Runs in a Web Worker.
- The 3D engine subscribes to runtime events; no per-frame IPC.
- NPC ticks run at 1 Hz (configurable); event-driven updates fire instantly.
- For worlds with > 10 000 NPCs, the runtime supports spatial bucketing so only nearby NPCs update.
14. Multi-server simulation (Phase 7+)
For MMO-style worlds:
- A thin authoritative server hosts the canonical
World.json+ the runtime log. - Clients (player, GM, cinematic recorder) connect over WebSocket.
- Conflict resolution: last-writer-wins on terrain / objects, CRDT (Yjs) on collaboration metadata.
This is the bridge to adventure-craft. See
09-multiplayer-and-versioning.md.