WorldForge Studio

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 against apps/web/src, apps/api/src and git log (PRs #26–#120). Ground truth for milestones is superpowers/STATUS.md.

Design elementStatusWhere
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 onlycomponents/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)✅ SHIPPEDTags spec-climate-w1-complete (2D) + spec-3d-seasonal-night-effects-complete (3D).
Weather engine (§5)✅ SHIPPEDRuntime 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)⚠️ PartialSchedule 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)✅ SHIPPEDSpec 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)✅ SHIPPEDM7.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 onlyworld.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.