# Quests, missions and dialogue (quest.*, mission.*, npc.dialogue, dialogue.voice) Objectives, branching stories and conversations as data. Readable offline: `gripforge docs quests`, or the MCP tool `gripforge_docs` (topic `quests`). ## What it is for | Kit | What it adds | | --- | --- | | `mission.system` | ONE active mission at a time: a list of objectives (sequential or parallel), conditions, actions, rewards, zone markers, plus trigger rules that run the same condition and action language outside any mission | | `quest.graph` | several quests at once as graphs of nodes with choices, branches, timeouts and failure; a quest log, a tracked objective with a world marker and a compass | | `npc.dialogue` | talking to an NPC: typed lines, choices that run actions and continue the conversation | | `dialogue.voice` | pre-recorded voices for those dialogues, with lip movement driven by the real audio clock; no AI call while the game runs | `mission.system` is the small, shared, engine-portable machine; `quest.graph` is for stories with choices. They work together: a `mission` node of a quest starts a mission and waits for its outcome. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "quest.graph" } gripforge_gamekit_configure { project_id: "gkp_…", id: "quest.graph", config: { "failOnDeath": true, "arriveRadius": 4 } } gripforge_gamekit_install { project_id: "gkp_…", id: "npc.dialogue" } ``` `mission.system` requires `spawn.entities`, `world.zones` and `game.engine`. `quest.graph` requires `input.actions` and `game.engine` and uses the inventory, money and level kits for rewards when they are installed. `npc.dialogue` requires `npc.core`, `interaction.use` and `hud.bind`. `dialogue.voice` requires `npc.dialogue`, `audio.core` and `game.engine`. ## Missions (mission.system) Missions are documents of the `missions` collection (`gripforge.mission/1`); trigger rules are documents of `triggers`. Both are validated against the installed kits before play, so a missing kit is a readable error. | Key | Default | Meaning | | --- | --- | --- | | `autoStart` | `first` | `none`, `first`, or a mission id to start when the world is ready | | `markerColor` | `#ffcc00` | colour of the objective zone marker | | `failOnDeath` | `true` | fail the active mission when the player is arrested or dies | Objective steps: `go_to { zone }`, `wait { seconds }`, `interact { entity | tag }`, `event { topic, match? }`, `escape_zone { zone, until? }`; other kits add theirs (vehicles bring `enter_vehicle`, `exit_vehicle`, `deliver_vehicle`; dialogue brings `talk_to { npc?, topic? }`). A `timeout` fails the mission unless the step is `optional`. Conditions: `{ var, op, value }`, `{ event, match? }`, `{ in_zone }`, `{ has_tag }`, `{ all }`, `{ any }`, `{ not }` and the conditions of the kits (`has_item`, `money_at_least`, `quest_status`...). Actions: `set_var`, `inc_var`, `emit`, `spawn`, `despawn`, `notify`, `start_mission`, `complete_objective`, `fail_mission`, `teleport`, `wait`, and the verbs of the kits. Rewards: `money`, `reputation`, `items`, `vars`. Variables: `mission.status`, `mission.objective.label`, `mission.timer`. Events: `mission.started`, `mission.objective.completed`, `mission.completed`, `mission.failed`, `mission.aborted`. ## Quests (quest.graph) ```json { "id": "lost_lamb", "title": "The lost lamb", "giver": { "npc": "shepherd" }, "start": "find", "rewards": { "money": 20, "xp": 50 }, "nodes": [ { "id": "find", "type": "go_to", "label": "Search the old mill", "at": [40, 0, 12], "radius": 6, "next": "decide" }, { "id": "decide", "type": "choice", "prompt": "The lamb is hurt.", "choices": [ { "id": "carry", "text": "Carry it home", "next": "home" }, { "id": "keep", "text": "Keep it", "next": "betray" } ] }, { "id": "home", "type": "talk", "label": "Bring it to the shepherd", "npc": "shepherd", "timeout": 300 }, { "id": "betray", "type": "fail", "reason": "kept the lamb" } ] } ``` Node types: `go_to` (zone, or `at` + `radius`), `collect` (`item`, `count`, `take`), `talk` (an NPC document or a tag), `kill` (`count` of a `tag` or `kind`), `use` (an item or a tagged entity), `event` (a bus topic, `match`, `count`), `wait`, `choice`, `branch` (first `branches[].when` that holds, else `else`), `mission`, `complete`, `fail`. A node without `next` completes the quest. Any node may carry `timeout` / `onTimeout`, `failWhen`, `actions`, `rewards` and `marker: false`; a quest has `conditions.start` and `conditions.fail`. Rewards: `items`, `money`, `xp`, `vars`. When a dialogue with `giver.npc` ends, the quest starts, unless `giver.auto` is `false` (then a dialogue choice runs `{ "type": "start_quest", "quest": "…" }`). | Key | Default | Meaning | | --- | --- | --- | | `autoTrack` | `true` | a started quest is tracked when nothing else is | | `failOnDeath` | `false` | `player.died` fails every running quest | | `arriveRadius` | 3 | m, for a `go_to` node that gives no radius | | `markerColor` | `#7fd4ff` | colour of the tracked objective marker | | `presentationProfile` | empty | a game-owned presentation profile (slot `materials`, objective) for the beacon | Service `quest.graph`: `quests()`, `runs()`, `start(id)`, `choose(id, choice)`, `fail(id, reason?)`, `abandon(id)`, `track(id | null)`, `tracked()`, `objective(id?)`, `target()` (distance and bearing for a compass), `openLog()`. Verbs `start_quest`, `quest_choose`, `fail_quest`, `track_quest`; conditions `quest_status`, `quest_choice`, `quest_at`. Events `quest.started`, `quest.node`, `quest.progress`, `quest.objective.completed`, `quest.choice`, `quest.completed`, `quest.failed`, `quest.tracked`. The runs are saved. ## Dialogue (npc.dialogue) A key (topic or dialogue id) resolves to a `dialogues` document with that id, then to a document of this NPC whose `topic` (default `greet`) matches, then to the `dialogue.lines[key]` of the NPC document. ```json { "id": "job_offer", "npc": "barman", "topic": "greet", "speaker": "Sal", "lines": ["Looking for work?", "I have a car that needs moving."], "choices": [ { "id": "yes", "text": "I'm in.", "then": [{ "start_mission": "move_car" }] }, { "id": "later", "text": "Not now.", "next": "bye" } ] } ``` Every NPC with something to say is registered with `interaction.use` ("Talk to ..."). Lines are typed at `charsPerSecond` (40); `autoAdvanceSeconds` (4, 0 disables) moves a typed line on, except the last line when choices wait (keys 1 to 4 pick them); `pauseWorld` (`false`) pauses the game while talking. The conversation also ends when the player walks more than 6 m away. HUD variables: `dialogue.active`, `dialogue.speaker`, `dialogue.text`, `dialogue.choices`, `dialogue.choice.1` to `.4`. Service `dialogue.lines`: `say(npc, topic?)`, `current()`, `active()`, `advance()`, `choose(index | id)`, `end(reason?)`, `topics(npc)`. Verb `start_dialogue`. Events `dialogue.started`, `dialogue.choice`, `dialogue.ended`, `dialogue.refused`. ## Voices (dialogue.voice) Recordings are produced once, before the game ships, and stored with it. A clip names a dialogue document, a zero-based line, a language, the exact translated text, a sound on the voice bus, its duration and timed mouth cues; changed text or another language never plays a stale clip. A character points at a rig with `voice: { rig: "hero_face" }`; the rig document maps each mouth cue to morph targets and/or a jaw bone. Without a recording the subtitles and choices carry on and one `voice.missing` event is emitted per line. Settings: `enabled` `true`, `followDialogue` `true`, `volume` 1, `blendSeconds` 0.04, `loadTimeoutSeconds` 20. Set `audio.core.voiceDuringPause` when `pauseWorld` is on. Collections: `dialogue_voices`, `voice_rigs`, `voice_cast`, `dialogue_voice_directions`. ## Hooks and modding | Hook | Payload | What a tap may do | | --- | --- | --- | | `mission.start` | `{ mission }` | cancel (the mission stays closed) | | `mission.fail` | `{ mission, reason }` | change `reason`, cancel (the mission goes on) | | `mission.complete` | `{ mission, title, rewards }` | change the rewards, cancel (the last objective is armed again); a kernel hook of `game.engine` | | `quest.start` | `{ quest }` | cancel | | `quest.choose` | `{ quest, node, choice }` | cancel | | `quest.advance` | `{ quest, from, to }` | change `to` | | `quest.complete` | `{ quest, node, money, xp }` | change `money` and `xp` | | `quest.fail` | `{ quest, node, reason }` | change `reason`, cancel | | `dialogue.start` | `{ npc, npcId, dialogue, topic }` | change `topic`, cancel | | `dialogue.choose` | `{ npc, npcId, topic, choice, index }` | change `choice`, cancel | | `dialogue.advance` | `{ npc, dialogue, index, origin }` (`auto`, `input` or `service`) | cancel (the voice kit holds an automatic advance while a line is spoken; an explicit skip still works) | ```json { "id": "generous", "hook": "quest.complete", "match": { "quest": "lost_lamb" }, "mul": { "money": 2 } } ``` Moddable: the settings above and the collections `missions`, `triggers`, `quests`, `dialogues`. The failures a graph decides itself (a `fail` node, a timeout, `failWhen`) are not behind a hook. ## Engines `mission.system` is delivered to Godot and Unity as files and to Unreal through the shared kit library, with the same step machine. `npc.dialogue` is delivered to Godot and Unity as files. `quest.graph` and `dialogue.voice` run on the web and in the native player; the hooks run in those two as well. In a web game the quest log and tracker are drawn by the interface layer; `dialogue.voice` plays through `audio.core` and depends on the audio device clock for the lips. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | A mission never starts | `autoStart` is `none`, or the mission has a `giver` with `auto: false` (the NPC gives it) | | A quest stays on its node | the node waits for something that never happens: check the `tag`, the `zone`, or the `match` fields of an `event` node | | A dialogue offers nothing | the key resolves to no `dialogues` document and no `dialogue.lines`: use the topic `greet` or name one | | A choice does not run its actions | read `dialogue.refused`: a `dialogue.choose` tap cancelled it | | Voices do not play | no recording for this line and language (see `voice.missing`), or the text changed since the recording |