Skip to main content

ADR 0007 — Quest Thread and Mission as Scope Primitives

Context

Valenar's quest taxonomy (Activity Beat / Mission / Local Quest / Quest Thread / Act Arc / Saga Arc) is committed at ../../systems/gd-quest-and-lore-design.md, and the live planning vocabulary (objective / clue or lead / mission / activity / policy) is owned by ../../systems/gd-objectives-clues-missions.md. The dynamic quest pressure model in ../../systems/gd-dynamic-quest-pressure-model.md generates mission candidates and Journal readback hooks against those taxonomies. None of those docs commits to a runtime shape for a Quest Thread or a Mission as primitives the engine can store, transition, or query. The four Act 0 thread owner docs under ../quest-threads/ plus the seven cross-act lanes confirmed by ad-0006 mean Valenar already has ten Quest Threads in active design with more arriving as later acts come online, and each will carry per-instance lifecycle state, mission progress, clue accumulation, and an act-scope qualifier.

Two runtime patterns were viable. The first was a template-only shape: every Quest Thread becomes a template<QuestThread> archetype, and "runtime state" lives as ad-hoc bookkeeping on the host or as tags on the owning entity. The second was a tag-only shape: each thread state becomes a tag value applied to an existing entity, and missions become nested tag clusters. Neither pattern carries per-instance lifecycle state in the runtime in a way the 6-phase channel pipeline can resolve, neither produces a queryable identity, and neither lets later waves bind events (e.g., OnMissionAccepted) to a typed id. The "Templates are data" tenet rules out making the template the owner of mutable per-instance state; the bind-time-predicate nature of tags rules out tag-set as the carrier for per-instance fields. Foundation Hardening needs a primitive shape committed before the Wave 3a state-machine row, the urgency-signal binding wave, and the Journal readback hook wave can land.

Decision

Quest Threads and Missions are committed as a hybrid of scope and template primitives.

A new scope QuestThread carries per-instance runtime state, including at minimum LifecycleState, ActScopeKind, DefinitionTemplateId, a ScopedList<Mission> for child missions, and a ScopedList<ClueRef> for accumulated clues. The exact field list is deferred to the Wave 5a state-machine row authoring pass; this ADR commits the primitive shape, not the final field roster.

A new scope Mission carries per-instance runtime state for each mission within a thread, including at minimum LifecycleState, DefinitionTemplateId, ParentThreadId, and a ScopedList<ActivityBeatRef> for the activity beats the mission contains. Mission walks to its parent QuestThread via the existing SECS scope-walk mechanism.

Two lifecycle-state enums are committed for these scopes. The QuestThreadLifecycleState enum carries the values Hidden, Available, Active, Suspended, Resolved, Failed, and Abandoned. The MissionLifecycleState enum carries the values Pending, Available, Accepted, Active, Resolved, Failed, and Abandoned. Both enums follow the precedent set by FrontStatus in ../../systems/gd-fronts.md and the existing OperationStatus enum in the operations doc: persistent state expressed as a typed enum on the scope, not as a tag set.

Authored content uses template<QuestThread> and template<Mission> archetypes that supply identity, narrative metadata, declared act_scope, declared lane key (one of the seven committed cross-act lane keys or act-scoped), authored beat structure, and the binding rules from the dynamic quest pressure contract. The runtime scope rows reference the archetype via DefinitionTemplateId. This split — scope holds per-instance state, template holds authored archetype data — satisfies the "Templates are data" tenet without giving up the per-instance state that authored quests demand.

Alternatives Considered

A template-only shape (every QuestThread becomes a template<QuestThread> with no scope counterpart). Rejected because runtime per-instance state — accumulated clues, current lifecycle state, suspended-because-precondition records, the specific mission a thread is currently surfacing in the Journal — cannot live on a template without violating "Templates are data" or without inventing a parallel state-holding mechanism the engine does not understand. The dynamic quest pressure contract requires per-thread instance state, so the template-only path would have forced an ad-hoc host-side state store outside SECS, which is exactly the kind of compatibility shim the no-backwards-compat tenet forbids.

A tag-only shape (each thread state becomes a tag value applied to an existing entity, and missions become nested tag clusters). Rejected because tags are bind-time predicates, not data carriers. Tags cannot hold the ActScopeKind, the ParentThreadId, the accumulated clue list, or the lifecycle-state enum without overloading the tag system into a parameter-tuning surface — and "Never tune parameters as a substitute for fixing architecture" applies here directly. Tag-only also produces no queryable identity for binding events, no place to attach the dynamic quest pressure generator's output, and no path to the 6-phase channel pipeline if a Quest Thread later carries a channel (e.g., a derived urgency or freshness signal).

Tenets Applied

  • "Future-proofed solutions only — design for the long term, not for what's easiest today." A scope-plus-template hybrid scales to every later Quest Thread, every cross-act lane authored after Foundation Hardening, and every Wave M act-specific content slot without reshape.
  • "Templates are data." Template carries the authored archetype; per-instance lifecycle state lives on the scope. The two responsibilities stay separated.
  • "No backwards compatibility." Neither the template-only fallback nor a tag-shim path is preserved. Quest Thread runtime state is the scope row, full stop.
  • "AAA = COMPLETE correct algorithm." The committed AAA precedents (RimWorld Quest entity plus QuestPart sub-stages plus QuestScriptDef archetype; CK3 Story cycle entity plus story_cycle types; Stellaris event chain instances plus event_chain definitions; Witcher 3 quest objects plus quest definitions) all use the same instance-state carrier plus archetype split, and this ADR commits Valenar to that same complete shape rather than a simplified subset.

Runtime Backing Status

Status at this ADR's acceptance time: contract-only.

  • Host/runtime owner: legacy/v1/examples/valenar/Host/Data/ (new QuestThreadData.cs and MissionData.cs to be authored as part of the Wave 5a runtime-backing wave; the names are the host-data pattern, not committed source syntax).
  • Generated/.secs owner: legacy/v1/examples/valenar/Content/quests/ to host scopes.secs, templates.secs, and any future channels.secs for derived per-thread channels; matched by the parallel legacy/v1/examples/valenar/Generated/Quests/ Generated stand-in tree authored in the same Wave 5a runtime-backing wave.
  • Read-model/UI owner: deferred — the Journal-readback hook wave is the earliest consumer and will surface QuestThread.LifecycleState, Mission.LifecycleState, and current Mission readback rows.
  • Tests: deferred to the Wave 5a runtime-backing wave; the unit-test fixtures live alongside the host data classes when authored.
  • Known gaps: the exact field roster on scope QuestThread and scope Mission beyond the minimum committed here; the ActScopeKind enum's exact values (the seven committed cross-act lane keys plus act-scoped is the working set, but the enum is not authored yet); the binding rules for how a generated mission candidate from the dynamic quest pressure contract becomes an accepted scope Mission instance.
  • Illegal fallback behavior: no host-side parallel state store may carry Quest Thread or Mission per-instance state outside the SECS scope rows. No ad-hoc tag-cluster pattern may stand in for MissionLifecycleState even temporarily.
  • Next closure wave: Wave 5a runtime-backing wave authors the host data classes, the Content/quests/scopes.secs and Content/quests/templates.secs, and the matching Generated stand-in module registrations. The Journal-readback hook wave follows.

Glossary Propagation

The following terms MUST land in gd-glossary.md, gd-canon.md, and README.md per the contract-backing rule (.claude/rules/valenar-contract-backing.md § Canonical Glossary Targets). Glossary propagation itself is Wave 3c — this section declares the obligation:

  • QuestThread (scope primitive).
  • Mission (scope primitive).
  • QuestThreadLifecycleState (enum with the seven values committed above).
  • MissionLifecycleState (enum with the seven values committed above).
  • ActScopeKind (enum referencing the seven committed cross-act lane keys plus act-scoped; exact value list deferred to Wave 5a).

Consequences

For future authoring of any new Quest Thread owner doc (e.g., the forthcoming gd-quest-thread-and-mission-state-machine.md to be authored in Wave 3a), the doc may reference scope QuestThread, scope Mission, the two lifecycle-state enums, and the template-archetype split by name as the committed primitive shape. No later doc may reintroduce a template-only or tag-only Quest Thread shape without superseding this ADR.

For the dynamic quest pressure contract in ../../systems/gd-dynamic-quest-pressure-model.md, the generator's "mission candidate" output becomes a candidate to instantiate a scope Mission row tied to a parent scope QuestThread once accepted by the player or the policy layer. The contract doc may be extended in a later wave to reference these scope primitives by name; this ADR does not edit that doc.

For the four committed Act 0 thread owner docs (Survive, The Old Stones, The Ruin Beneath, Make This Place Hold) and the seven cross-act lane Quest Thread owners, no edits are required from this ADR alone — those docs remain at the authored-archetype layer; the scope rows that back them are authored in the Wave 5a runtime-backing wave.

For the verifier, any future doc claiming a Quest Thread or Mission exists at runtime without a scope QuestThread / scope Mission row backing it is a layering FAIL. Any doc that introduces a third QuestThread runtime shape (neither scope nor template nor the hybrid committed here) requires superseding this ADR before acceptance.

References