World / Game Flow

How a campaign run stays in one encounter-governed room loop from tavern entry through chat, combat, and onward.

This page documents the live runtime loop: room entry starts encounter governance immediately, every action is validated against round/turn authority, chat and hostile actions share one framework, and the server returns canonical state after each mutation.

Player-facing loop

Think of the run as one persistent encounter framework. Tavern entry starts round/turn governance, movement/search/talk all happen inside that same room loop, and hostile escalation adds combat persistence without switching to a separate room mode.

  • A campaign and selected character define the launch context.
  • Startup narration and room state are delivered before the first governed room turn.
  • Chat is part of the active encounter room loop, not a separate world map or menu mode.
  • Combat escalation stays inside encounter governance and returns to the same room loop when resolved.

System-facing loop

The runtime stays server-authoritative. HexMap bootstraps launch payload, GameCoordinator loads state and unseen events, EncounterMaster validates/runs intents, and the server returns canonical game_state/available_actions/events after each meaningful step.

  • HexMapController hydrates launch context and dungeon payload.
  • GameCoordinatorService ensures game_state, campaign_clock, and startup room-entry events exist.
  • EncounterPhaseHandler (EncounterMaster) is the authoritative contract for room turns and action legality.
  • Narration, chat, world mutations, and combat persistence all flow through one canonical action path.

Primary campaign loop

End-to-end run lifecycle

This is the top-level player journey for an active campaign run.

flowchart TD A["Campaign selected"] --> B["Tavern entrance"] B --> C["HexMapController"] C --> D["GameCoordinatorController"] D --> E["GameCoordinatorService.getFullState"] E --> F["ensureGameState + bootstrapInitialRoomEntry"] F --> G["GameEventLogger.logEvents (emit room_entered)"] G --> H["Return canonical payload: events, game_state, available_actions"] H --> I["NarrationEngine overlays room_entered narration"] I --> J["Client processes startup events and enters governed room loop"] J --> K["Encounter room loop (transition/search/talk/turn decisions)"] K --> L{"Hostile escalation committed?"} L -- No --> K L -- Yes --> M["startCombatEncounter keeps encounter contract active"] M --> N["CombatEngine handles initiative/strike/turn resolution"] N --> O{"Hostile encounter resolved?"} O -- No --> N O -- Yes --> P["Persist dungeon_data and resume same room loop"] P --> K

Key points

  • Sequence is implemented by HexMapController -> GameCoordinatorController -> GameCoordinatorService (getFullState/processAction).
  • ensureGameState() and bootstrapInitialRoomEntry() create the initial room_entered event persisted via GameEventLogger::logEvents().
  • Client-side UI consumes the returned canonical payload (events, game_state, available_actions) before normal polling resumes.
  • EncounterPhaseHandler/CombatEngine own encounter rules; hostile escalation resolves back into the same encounter-governed room loop.

Launch and tavern startup

How the run boots into the first room

The launch path starts before the player can move: route selection, state hydration, startup narration, and the first encounter-ready room view.

flowchart LR A["Campaign tavern entrance"] --> B["HexMapController"] B --> C["GameCoordinatorController"] C --> D["GameCoordinatorService.getFullState"] D --> E["ensureGameState + bootstrapInitialRoomEntry"] E --> F["GameEventLogger.logEvents (room_entered)"] F --> G["Return initial payload: events, game_state, available_actions"] G --> H["NarrationEngine overlays room_entered narration"] H --> I["Client processes initial events and begins encounter-governed room loop"]

Key points

  • Startup narration is delivered as a real room_entered event.
  • Campaign clock and current phase are part of the canonical state payload.
  • The client processes initial events before normal polling continues.

Encounter room loop

Movement, investigation, turn order, and room-state updates

Encounter is the default runtime loop on the hexmap. Room actions, movement, and conversation stay in this one framework.

flowchart TD A["Encounter phase active"] --> B["Current actor chooses an intent"] B --> C{"Intent type"} C -->|Transition| D["Server validates connected room and moves party"] C -->|Search| E["Run encounter search"] C -->|End / choose not to act| F["Log explicit turn-ending decision"] C -->|Talk| G["Open room chat or direct chat"] D --> H["Server updates game_state, round/turn, and event log"] E --> H F --> H G --> I["Chat reply and transcript update"] H --> J{"Next actor?"} J --> K["Emit round_start / turn_start as needed"] I --> M["Encounter remains active"] K --> M M --> A

Key points

  • Movement, search, talk, and explicit end-turn/no-action choices stay in encounter.
  • Room narration is first-visit gated and emitted through the event pipeline.
  • Chat can update world context without forcing a phase change.

Chat loop

Conversation inside the current room

Room chat runs inside the live hexmap shell so players can converse without leaving the run. The GM path can be deterministic, cached, or LLM-backed; NPC room reactions and private channels each have their own model operations.

flowchart TD A["Player sends room message"] --> B["RoomChatService persists player message"] B --> C{"Channel type"} C -->|Room| D["Resolve room context, intent, cache, deterministic shortcuts"] C -->|Private whisper or ability channel| E["LLM call: channel_npc_reply"] D --> F{"Deterministic or cache hit?"} F -- Yes --> G["Return GM reply without LLM"] F -- No --> H["LLM call: room_chat_gm_reply"] H --> I{"Mechanical actions invalid?"} I -- Yes --> J["LLM call: room_chat_gm_retry"] I -- No --> K["Accept parsed GM reply"] J --> K G --> L{"Room NPC interjections enabled?"} K --> L E --> M["Return private NPC reply"] L -- No --> N["Send final chat payload to UI"] L -- Yes --> O["Per candidate NPC: LLM call npc_interjection_eval_single"] O --> P{"NPC should speak?"} P -- No --> Q["Log NPC choose_not_to_act"] P -- Yes --> R["LLM call: npc_room_dialogue"] Q --> N R --> N M --> N N --> S["UI updates transcript inside encounter"]

Key points

  • Room channel GM narration uses operation `room_chat_gm_reply`, with optional `room_chat_gm_retry` if authoritative action validation fails.
  • Private channels bypass the GM layer and go straight to `channel_npc_reply` for in-character NPC speech.
  • Room interjections are two-stage: `npc_interjection_eval_single` decides whether an NPC speaks, then `npc_room_dialogue` generates the actual line for NPCs that passed.
  • Deterministic shortcuts and GM response cache hits can skip some or all LLM calls for low-variance turns.

Chat workflow detail

Every LLM call in the chat pipeline

These are the concrete model operations used by RoomChatService. They do not all fire on every turn; the pipeline branches based on channel type, deterministic shortcuts, response cache hits, and whether NPC interjections are even eligible.

Order Operation When it runs Purpose
1 room_chat_gm_reply Room channel only, after deterministic handling and cache lookup both miss. Primary GM narration/action generation for the current player turn.
2 room_chat_gm_retry Only after the primary GM reply proposed mechanical actions that failed authoritative validation. Regenerates the GM reply using a reality snapshot and validation errors.
3 npc_interjection_eval_single Room channel only, once per candidate NPC after the GM reply, excluding directly addressed NPCs that are already forced into consideration. Binary SPEAK/PASS gate to decide whether a specific NPC should take a turn this round.
4 npc_room_dialogue Only for NPCs that passed the interjection gate, unless a deterministic NPC response already handled them. Produces the actual in-room spoken line for that NPC.
A channel_npc_reply Private whisper/ability channels instead of the room GM path. Generates the direct in-character NPC reply for that private channel conversation.

Branching rules

  • If the turn is handled by deterministic room logic, the chat response can complete with zero LLM calls.
  • If a low-variance GM narration turn hits the response cache, the GM reply also completes with zero GM LLM calls.
  • Private channels use channel_npc_reply instead of room_chat_gm_reply.
  • Each candidate interjecting NPC is evaluated separately, so crowded rooms can trigger multiple npc_interjection_eval_single calls and multiple npc_room_dialogue calls in one player turn.

Combat loop

Encounter phase from initiation through resolution

Combat escalates inside the encounter framework, then hands control back to the same room turn loop after the hostile encounter ends.

flowchart TD A["Encounter phase starts"] --> B["Create or sync combat encounter"] B --> C["Roll initiative and build turn order"] C --> D["Active combatant turn"] D --> E{"Whose turn?"} E -->|Player| F["Choose strike, stride, spell, skill, interact, end turn"] E -->|NPC| G["Auto-play AI or fallback turn"] F --> H["Combat API validates and resolves action"] G --> H H --> I["Update HP, conditions, positions, logs, and world delta"] I --> J{"Encounter over?"} J -- No --> K["Advance turn and round"] K --> D J -- Yes --> L["End hostile encounter and emit narration"] L --> M["Resume encounter room loop (social/search/transition)"]

Key points

  • Encounter state stays server-authoritative through combat APIs and services.
  • NPC turns can auto-play through AI or fallback logic.
  • Resolved hostile encounters resume the same encounter room governance loop instead of switching room modes.

Authority and transition flow

How client actions become canonical state

The client proposes intent; the server returns the state that actually counts.

flowchart LR A["Player click or action"] --> B["GameCoordinatorApi / Client"] B --> C["GameCoordinatorController.action"] C --> D["GameCoordinatorService.processAction"] D --> E["getPhaseHandler -> PhaseHandler.validateIntent"] E --> F["PhaseHandler.processIntent (e.g. EncounterPhaseHandler.processIntent)"] F --> G{"Intent type"} G -->|talk| H["EncounterPhaseHandler.processTalk -> RoomChatService.postMessage"] G -->|strike| I["EncounterPhaseHandler.processStrike -> CombatEngine.resolveAttack"] H --> J["RoomChatService persists chat, may call LLM ops, returns state_diff and gm_response"] I --> K["CombatEngine updates HP, conditions, and CombatEncounterStore"] J --> L["GameEventLogger.logEvents"] K --> L L --> M["GameCoordinatorService persists dungeon_data and returns canonical response"] M --> N["Client refreshes UI: hexmap, action rail, chat, and narration"]

Key points

  • Encounter contracts own room action legality, turn authority, and combat escalation under one runtime framework.
  • The action rail, narration overlay, and current phase all refresh from returned state.
  • This is why campaign time, room narration, and hostile escalation state must stay aligned with server responses.