/** * Missions as data: a mission is a graph of objectives a state machine walks, with named fail * states, per-objective timers, conditional branches and rewards. Nothing here is a genre kit — * escort, chase and defend are compositions of the same objective kinds, built in `compositions.ts`. * * The machine is pure, serializable and bounded: one active mission, one objective's worth of * work per tick, and every fact it reads is supplied by the caller. */ import { type QuestRewards } from "../game/quest.js"; import type { ScenePoint3 } from "../world/sceneShapes.js"; /** Where a mission is in its life. */ export type MissionStatus = "active" | "completed" | "failed"; /** * The primitive goals an objective can have. Escort, chase and defend are not kinds — they are * compositions of these plus fail conditions and timers (see `compositions.ts`). */ export type MissionObjectiveKind = "reach" | "interact" | "survive" | "pursue" | "kill" | "collect"; /** Objective kinds that are satisfied only by a counter — a mission made of these alone is a lint error. */ export declare const MISSION_COUNTER_KINDS: readonly MissionObjectiveKind[]; /** * True when an objective kind completes from a counter rather than a world condition — the test the * counter-only lint is built on. * * @capability mission-validation tell whether an objective kind completes from a counter alone */ export declare function isCounterObjective(kind: MissionObjectiveKind): boolean; /** What a reported world event is. */ export type MissionWorldEventKind = "kill" | "collect" | "deliver"; /** * One thing that happened in the world this tick, reported by the game. A constrained objective counts * matching events and an `event` fail condition reacts to them, so the machine judges the world instead * of the game polling its own counters. * * @capability mission-events count constrained kills and fail a mission on a matching world event */ export interface MissionWorldEvent { kind: MissionWorldEventKind; /** Target catalog id, item id or route id the event concerns. */ target: string; /** Actor that caused it. */ by?: string; /** Free-form tags the game attaches — weapon, element, route, `decoy`. */ tags?: readonly string[]; /** How many the event is worth. Default 1. */ count?: number; } /** Which world events an objective counts or a fail condition reacts to. Absent fields do not filter. */ export interface MissionEventFilter { /** Event kind; a `kill`/`collect` objective defaults to its own kind. */ kind?: MissionWorldEventKind; /** Exact target id. */ target?: string; /** Exact actor that caused it. */ by?: string; /** Every one of these tags must be present. */ tags?: readonly string[]; /** None of these may be present — how a decoy or a wrong-weapon kill is excluded. */ withoutTags?: readonly string[]; } /** A readable fact test a branch or a `fact` fail condition reads. All comparators are optional. */ export interface MissionFactCondition { /** Fact name, or `"score"` for the mission's running survival score. */ fact: string; /** Matches when the fact strictly equals this. */ is?: string | number | boolean; /** Matches when the numeric fact is at least this. */ atLeast?: number; /** Matches when the numeric fact is at most this. */ atMost?: number; /** Invert the whole test. */ not?: boolean; } /** * A condition that fails the mission while the objective is active. `to` names a {@link MissionFailDef}; * with no `to` the mission fails with the generic `"failed"` state. * * `actor-died`, `timer`, `fact` and `left-area` read world state every tick; `event` reacts to a world * event the game reported this tick. */ export interface MissionFailCondition { kind: "actor-died" | "timer" | "fact" | "event" | "left-area"; /** `actor-died`: the tracked actor whose death fails the mission. `left-area`: the actor watched. */ actor?: string; /** `timer`: seconds in the current objective before the mission fails. */ seconds?: number; /** `fact`: the fact and comparator that fail the mission. */ fact?: string; is?: string | number | boolean; atLeast?: number; atMost?: number; /** `event`: which events fail the mission. Omitted matches any event. */ match?: MissionEventFilter; /** `left-area`: authored marker the watched actor must stay near. */ marker?: string; /** `left-area`: distance (m) past which the actor has left. Default {@link DEFAULT_REACH_RADIUS}. */ radius?: number; /** Named fail state id. */ to?: string; /** Human reason carried on the `failed` event. */ message?: string; } /** One outgoing edge of an objective: the first branch whose `when` matches wins over `next`. */ export interface MissionBranch { /** Objective id, a {@link MissionFailDef} id, or `"done"` to complete the mission. */ to: string; when: MissionFactCondition | readonly MissionFactCondition[]; /** Optional label carried on the `branch` event. */ label?: string; } /** One objective. Every field is content; the machine reads only these. */ export interface MissionObjectiveDef { id: string; kind: MissionObjectiveKind; /** Human label for a HUD tracker; falls back to a derived label. */ label?: string; /** Tracked actor for `reach` / `interact` / `survive` / the `pursue` target. */ actor?: string; /** Pursuer for `pursue`; defaults to the runtime's `defaultPursuer`. */ pursuer?: string; /** Authored scene marker id the actor must reach or interact at. */ marker?: string; /** Proximity radius (m) for `reach` / `interact`. Default {@link DEFAULT_REACH_RADIUS}. */ radius?: number; /** Seconds the actor must hold for `survive`. */ duration?: number; /** Keep-close distance (m) for `pursue`. Default {@link DEFAULT_PURSUE_DISTANCE}. */ distance?: number; /** Distance (m) past which a pursuit counts as escaped. Default `distance * 3`. */ escapeDistance?: number; /** Seconds beyond `escapeDistance` before the pursuit fails. Default `0` (instant). */ escapeGrace?: number; /** Fail state a pursuit escape lands in. Default `"failed"`. */ escapeFail?: string; /** Counter key for `kill` / `collect`; defaults to the objective id. */ counter?: string; /** Counter target for `kill` / `collect`. */ count?: number; /** * Count `kill` / `collect` objectives from reported world events instead of a bare counter, so only a * matching event credits progress (`tags`, `withoutTags`, `target`). Omitted keeps the counter path. */ match?: MissionEventFilter; /** Survivors that must remain for `survive`; dropping below fails the mission. */ minSurvivors?: number; /** Fail state a `minSurvivors` breach lands in. Default `"failed"`. */ minSurvivorsFail?: string; /** Seconds in this objective before it expires into `onTimeout`. */ timeout?: number; /** Where expiry goes: an objective id, a fail state id, or `"done"`. Default: fail the mission. */ onTimeout?: string; /** Conditions that fail the mission while this objective is active. */ failOn?: readonly MissionFailCondition[]; /** Next objective (or `"done"`) when the goal is met and no branch matches. */ next?: string; /** Conditional edges, first match wins. */ branches?: readonly MissionBranch[]; } /** A named fail state an objective, timer or branch can land in. */ export interface MissionFailDef { id: string; message?: string; } /** What a mission pays. Economy/XP/items/unlocks flow through the quest appliers; loot names tables. */ export interface MissionRewards extends QuestRewards { /** Loot table ids the game rolls through its own loot registry (`createLootRegistry` / `rollLoot`). */ loot?: string[]; } /** A whole mission: the objective graph plus its fail states and rewards. */ export interface MissionDef { id: string; title: string; description?: string; /** Objective the mission starts on. Defaults to the first objective. */ start?: string; objectives: readonly MissionObjectiveDef[]; /** Named fail states. */ fail?: readonly MissionFailDef[]; /** Rewards paid on completion. */ rewards?: MissionRewards; /** Rewards paid the moment a named objective completes. */ objectiveRewards?: Readonly>; /** One named fact rolled from `options` at start with the injected RNG — deterministic variation. */ rollout?: { fact: string; options: readonly (string | number)[]; }; /** Follow-up mission started on completion — how a chain or campaign is authored. */ next?: string; } /** A live actor the machine tracks: whether it is alive and, when needed, where it is. */ export interface MissionActorFact { /** Defaults to alive. */ alive?: boolean; position?: ScenePoint3; } /** Everything the machine reads for one tick. All optional; a mission only touches what it declares. */ export interface MissionTickInput { actors?: Readonly>; /** Cumulative counters for `kill` / `collect` objectives, by counter key. */ counters?: Readonly>; /** World events that happened this tick — what a `match` objective counts and an `event` fail reacts to. */ events?: readonly MissionWorldEvent[]; /** Transient facts for branch and `fact` fail conditions. */ facts?: Readonly>; /** Actor ids that interacted this frame, for `interact` objectives. */ interact?: readonly string[]; /** Survivors on the ground for a `survive` objective; defaults to the living actors in `actors`. */ survivors?: number; } /** Serializable mission state — one active mission's position in its graph. */ export interface MissionState { missionId: string; status: MissionStatus; /** Current objective id, or null once the mission ended. */ objective: string | null; /** Objective ids entered in order — the branch path a test or journal reads. */ path: string[]; /** Seconds spent in the current objective. */ objectiveElapsed: number; /** Seconds since the mission started. */ elapsed: number; /** Counter reads recorded for the current mission. */ progress: Record; /** Facts the machine set (rollout result, …). */ facts: Record; /** Per-objective transient accumulators (pursuit escape seconds). */ timers: Record; /** Running survival score — survivors counted as `survive` objectives complete. */ score: number; /** Fail state id when failed, else null. */ outcome: string | null; /** Human reason for a failure. */ reason: string | null; } /** Emitted as the machine advances. */ export type MissionEvent = { kind: "started"; missionId: string; mission: MissionDef; } | { kind: "objective"; missionId: string; objectiveId: string; label: string; } | { kind: "branch"; missionId: string; from: string; to: string; label?: string; } | { kind: "timeout"; missionId: string; objectiveId: string; to: string; } | { kind: "reward"; missionId: string; objectiveId: string | null; rewards: MissionRewards; } | { kind: "failed"; missionId: string; state: string; reason: string; } | { kind: "completed"; missionId: string; score: number; rewards: MissionRewards | null; } | { kind: "chained"; missionId: string; nextMissionId: string; }; /** Pooled per-frame readout a HUD tracker renders. */ export interface MissionView { missionId: string; title: string; status: MissionStatus; objectiveId: string | null; label: string; objectiveElapsed: number; /** Seconds left on the current objective's timer (a `timeout` or a `survive` duration), or null when it has none. */ remaining: number | null; /** Counter fraction 0..1 for the current objective, or 0 when it is not a counter. */ progress: number; /** Counter value the current objective has reached. */ counted: number; /** Counter value the current objective needs, or 0 when it is not a counter. */ count: number; /** How many objectives the run has entered — a HUD shows this as "step N". */ step: number; /** Objectives the mission declares; a chain shows "of M". */ steps: number; score: number; /** Fail state id once the run failed, else null. */ outcome: string | null; /** Human failure reason, else null. */ reason: string | null; path: readonly string[]; } /** Counters a devtools probe or a test reads. */ export interface MissionStats { missions: number; ticks: number; started: number; completed: number; failed: number; branches: number; timeouts: number; /** Slowest single `tick` seen so far, in milliseconds. */ slowestTickMs: number; } /** Serializable runtime state for a save. */ export interface MissionRuntimeSnapshot { state: MissionState | null; ticks: number; started: number; completed: number; failed: number; branches: number; timeouts: number; } /** Appliers a mission reward routes through — the same shape quests use, plus a loot-table hook. */ export interface MissionRewardAppliers { grantXp?(amount: number): void; grantEconomy?(currencyId: string, amount: number): void; grantItem?(inventoryId: string, itemId: string, count: number): { reason: string; } | null | void; grantItems?(items: Readonly>): { reason: string; } | null | void; grantUnlock?(unlockId: string): void; /** Roll a named loot table through the game's loot registry. */ grantLoot?(tableId: string): void; } /** Everything a mission runtime needs. */ export interface MissionRuntimeConfig { readonly missions: readonly MissionDef[]; /** Marker positions resolved from the authored scene, by marker id. See `missionMarkers`. */ readonly markers?: Readonly>; /** How marker proximity is measured for `reach`, `interact` and `left-area`. Default horizontal. */ readonly markerProximity?: MissionMarkerProximity; /** Actor id used for a `pursue` objective that names no `pursuer`. */ readonly defaultPursuer?: string; /** `[0,1)` source for a mission's `rollout`; pass `ctx.rng`. Only read when a mission declares one. */ readonly rng?: () => number; /** Register a devtools probe under this name so the overlay shows the active mission. */ readonly devtoolsName?: string; /** Skip catalog validation at construction. Tooling and tests only. */ readonly skipValidation?: boolean; } /** The mission state machine over one active mission. */ export interface MissionRuntime { /** Mission ids this runtime knows. */ missionIds(): readonly string[]; /** A mission definition, or null when unknown. */ mission(id: string): MissionDef | null; /** Start a mission; returns its first state, or null when the id is unknown. Replaces any active run. */ start(missionId: string, facts?: Readonly>): MissionState | null; /** Advance the active mission by `dt` seconds against the world. Returns every event this tick. */ tick(dt: number, input?: MissionTickInput): readonly MissionEvent[]; /** Force an objective complete — the authored-trigger path. */ signal(missionId: string, objectiveId: string): readonly MissionEvent[]; /** Fail the active mission into a named fail state — the authored-trigger path. */ fail(missionId: string, state: string, reason?: string): readonly MissionEvent[]; /** Abandon the active mission without completing or failing it. */ abandon(): void; /** The active mission's live state, or null when none is running. */ state(): MissionState | null; /** The pooled per-frame readout to render (do not retain). */ view(): MissionView | null; /** Observe changes (start, tick, signal, fail, abandon, restore). Returns an unsubscribe fn. */ subscribe(listener: () => void): () => void; stats(): MissionStats; snapshot(): MissionRuntimeSnapshot; restore(next: MissionRuntimeSnapshot): void; /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */ dispose(): void; } /** Which plane a marker proximity test compares in. */ export type MissionMarkerPlane = "horizontal" | "spatial"; /** * How a positional objective compares an actor to its authored marker. * * Authored markers sit on a flat plan (`y = 0`) while the terrain around them rises, so a straight * 3D test can leave a marker unreachable from the ground below it. `"horizontal"` compares the * ground distance and allows the actor up to `tolerance` metres off the marker's height; * `"spatial"` is the plain 3D check, for a world whose stacked platforms must stay distinct. */ export interface MissionMarkerProximity { /** `"horizontal"` (default) or `"spatial"`. */ plane?: MissionMarkerPlane; /** Horizontal plane only: how far (m) the actor may sit above or below the marker. Default {@link DEFAULT_MARKER_TOLERANCE}. */ tolerance?: number; } /** Default proximity radius (m) for a `reach` / `interact` objective. */ export declare const DEFAULT_REACH_RADIUS = 2.5; /** Default vertical allowance (m) for a horizontal marker proximity test. */ export declare const DEFAULT_MARKER_TOLERANCE = 8; /** Default keep-close distance (m) for a `pursue` objective. */ export declare const DEFAULT_PURSUE_DISTANCE = 3; /** A pursuit escapes at this multiple of its keep-close distance when none is declared. */ export declare const DEFAULT_ESCAPE_MULTIPLE = 3; /** * Apply a mission's rewards through the same appliers quests use, then roll any named loot tables. * Economy, XP, items and unlocks go through {@link applyQuestRewards}; loot is a table id the game's * loot registry resolves, so a mission never grows its own reward path. * * @capability mission-rewards pay mission rewards through the quest appliers and the loot registry */ export declare function applyMissionRewards(rewards: MissionRewards, appliers: MissionRewardAppliers): { reason: string; } | null; /** * Run missions as a state machine. One mission is active at a time; `tick` advances it against the * world the caller supplies, moving through objectives on completion, branching on facts, and landing * in a named fail state on a fail condition, an expiry or a `minSurvivors` breach. Completion pays * rewards through {@link applyMissionRewards} appliers the caller wires, and a mission's `next` chains * the follow-up. Escort, chase and defend are compositions of the same objective kinds * (`compositions.ts`), never three bespoke systems. * * Cost per tick is bounded by the active objective's own fail list plus one goal test. * * Marker proximity runs on the plane `markerProximity` picks: horizontal by default, so a marker * authored on a flat `y = 0` plan still triggers from ground that rises under it; `spatial` for a * world whose stacked platforms must stay apart. * * @capability mission-runtime missions as a data-driven objective state machine with fail states, timers, branches and rewards */ export declare function createMissionRuntime(config: MissionRuntimeConfig): MissionRuntime; /** A located mission-catalog problem; `path` is a JSON Pointer into the supplied catalog. */ export interface MissionIssue { code: "duplicate-mission" | "duplicate-objective" | "empty-mission" | "missing-start" | "unknown-objective" | "unknown-fail-state" | "unknown-marker" | "missing-actor" | "missing-marker" | "missing-count" | "missing-duration" | "missing-target" | "invalid-number" | "invalid-match" | "unreachable-objective" | "counter-only"; severity: "error" | "warning"; message: string; path: string; missionId?: string; objectiveId?: string; } /** Declarations a mission catalog is validated against. */ export interface MissionValidationOptions { /** Authored marker ids the missions may reference; omitted means marker references are unchecked. */ markerIds?: Iterable; } /** * Validate a mission catalog without mutation: duplicate ids, unknown branch/next targets, unknown * fail states and markers, per-kind required fields, unreachable objectives, and the counter-only * rule — a mission that has a completion path made entirely of `kill`/`collect` objectives is an * error, because it can be won on a counter alone. * * Games run this over their mission catalogs (and {@link createMissionRuntime} runs it at * construction), so a counter-only mission fails rather than shipping. * * @capability mission-validation lint mission catalogs, including failing a mission that completes on a counter alone */ export declare function validateMissionCatalog(missions: readonly MissionDef[], options?: MissionValidationOptions): MissionIssue[];