Back to the blog

October 12, 2026 · 7 min · games · engineering · agents

.md

Build an AI Game NPC That Respects Quest State and Spoilers

Build an AI game NPC with game-owned quest state, spoiler-safe context, retry-safe rewards, save recovery, and optional browser avatar conversations.

To build an AI game NPC with authoritative quest state, let the game decide what happened and let the model discuss only what this player may know. Keep inventory, quest flags, rewards and reveal permissions outside generated dialogue. Supply an allowlisted view of current facts, validate every proposed action against the game state, and display confirmed outcomes separately from the NPC's words. An optional avatar conversation changes presentation, not authority.

Start with one quest, not an autonomous world

Use this fictional fixture: Mira guards a ferry gate. Returning a bronze sigil earns one ferry pass and unlocks a route. Player A has returned it; player B has not. Mira may explain the current objective, but later plot revelations remain unavailable. The following architecture is a recommendation, not a shipped game or a measured player-demand finding.

Authored dialogue is a reasonable baseline, especially for puzzle clues and irreversible choices. Ink's official Writing with ink guide documents global variables declared with VAR and conditions that control choices and text. Running your ink documents runtime access through variablesState, external functions and story-state serialization. Those mechanics support a deterministic narrative foundation; they do not provide an AI adapter or establish demand for generated NPC conversations.

Walk through the sigil hand-in

  1. Read the authenticated player's save. Resolve the player and active run from the application session, never from model arguments. Load the quest, inventory, scene and revision from the authoritative store. For this design, Ink receives projections of game-owned flags; it is not a second independent writer of rewards.
  2. Build the speaking brief. Include Mira's approved persona, the visible objective, whether hand-in is currently available and permitted hint text. Select facts through explicit reveal rules. Never send the hidden plot with an instruction saying not to reveal it. Apply the same filtering to retrieval results and tool responses.
  3. Accept an intention, not a claimed fact. A player saying that they found the sigil does not put it in inventory. Let dialogue suggest a hand-in, then use an explicit game action to request the transition. Keep that action available through the ordinary interface too.
  4. Commit through game rules. Recheck possession, eligibility, scene and expected revision inside the transaction. Consume the sigil, grant the pass and record completion together. Scope the single-grant constraint to player, run and quest, so another request identifier cannot produce another reward.
  5. Return a confirmed outcome. Show the saved quest status and inventory change in the quest panel. Give the NPC only a filtered description of that result. If the outcome is unknown, say it is being checked rather than announcing a reward.

This is illustrative application pseudocode, not Ink, database-library or Realtime Avatar SDK syntax. The transaction requires atomic writes and a unique completion constraint; a check followed by unrelated writes is insufficient.

APPLICATION PSEUDOCODE — server-owned quest transition
player = authenticatedPlayer(request)
run = requireOwnedActiveRun(player)
transaction:
  state = lockQuestAndInventory(player, run, quest)
  if completionExists(player, run, quest):
    return storedCompletion(player, run, quest)
  requireCurrentSceneAndRevision(state, request)
  requireEligibleAndHoldingSigil(state)
  consumeSigil(state)
  grantFerryPass(state)
  return saveCompletionAndAdvanceRevision(state)

Keep read-only lookups distinct from mutations. Ink's external-function documentation distinguishes actions from pure functions because the engine can look ahead while joining text. It says actions should use lookaheadSafe = false; pure functions should be harmless when called repeatedly. A reward grant is an action, not a pure function. That setting is not a substitute for transaction-level duplicate protection.

Cancel stale dialogue without erasing committed rewards

Bind each generated reply to the player, run, scene revision and conversation attempt that requested it. Before displaying it, compare those values with the active scene. Drop a late ferry-gate reply after the player enters the market. Stop the old avatar call when leaving the scene so its speech cannot continue as commentary on the new location.

Cancellation and commitment answer different questions. If the hand-in committed before a disconnect, keep the reward and recover its stored outcome on retry. If a scene change wins before commitment, reject the stale action. Serialize those decisions through the authoritative game state; stopping audio cannot roll back inventory.

Ink documents story.state.ToJson() and story.state.LoadJson(savedJson) for narrative state. Your save still needs the game's inventory, completion records and content version. Save a consistent checkpoint, or restore Ink projections from the authoritative game snapshot. Loading an earlier branch should create an explicit run identity and invalidate pending dialogue. Rebuild the speaking brief; do not replay a transcript containing revelations from the abandoned branch.

Add an optional browser avatar conversation

Realtime Avatar can provide the browser conversation, with managed speech recognition, language-model responses, speech synthesis and video. Create an original or licensed character from a portrait. Keep the API key and session instructions on your server, and supply only the permitted context. The React documentation covers AvatarCall and a server-side proxy; this walkthrough does not assume a native Unity or Unreal plugin or a connection to a 3D character rig.

Start with read-only quest lookup tools while hand-in remains an explicit game command. RTA tools execute in your application, not a hosted executor. The documented tool deadline is 2.5 seconds, and cancellation is cooperative: a handler that ignores its abort signal may still finish after its result is discarded. Return a pending acknowledgement for slower work and reconcile the result in the quest panel. Never infer failure or undo a reward from a conversational timeout.

Failure tests before inviting players

  • Two-player isolation: alternate A and B in separate story instances. B receives no A-specific facts, completion status or reward. Check context caches as well as storage.
  • Spoiler probing: ask for the ending and claim to have unlocked it. Inspect the actual context and tool results for forbidden facts. Player assertions must not change reveal permissions.
  • Duplicate hand-in: submit concurrent requests, including different identifiers. Assert one completion and one pass, then retry after losing the response.
  • Scene race: leave while a lookup or hand-in is running. Reject uncommitted stale actions, suppress obsolete dialogue and preserve any committed reward.
  • Save recovery: restore before and after completion. Confirm matching inventory, flags and permitted hints, with no dialogue inherited from a different branch.

Frequently asked questions

Can the model write Ink variables directly?

Keep that authority in game code. Model output can propose an action; validated rules decide whether the transition is legal and which variables change.

Does filtering context guarantee no spoilers?

No. A model can guess or invent plot details. Use authored lines for critical reveals and puzzles, and test generated dialogue rather than treating a prompt as enforcement.

Will a new avatar call remember the quest?

Not automatically. Your application restores the save and supplies current permitted context. The transcript is not the quest ledger, and a new call does not supply persistent game memory.

Should every NPC use generated dialogue?

No. Compare one optional conversational quest giver with the authored version. Evaluate clue correctness, successful progression and player preference before expanding; official API documentation cannot answer whether players want it.

Prototype Mira's optional browser conversation after the deterministic quest passes these tests. Use the RTA React guide for presentation, and keep the authored quest playable when the call is unavailable.

Meet the live avatars. Hold the first conversation.

Enter the studio