Appearance
Quests & Achievements
Structured narrative progression: multi-stage quests with rewards, and binary/tiered achievements.
Quest System
Structured narrative progression: multi-stage tasks with NPC offer flows, rewards, and reactive $quest.* signals.
Signals
Each defined quest automatically gets a $quest.<id> signal. Its value is one of:
| Value | Meaning |
|---|---|
"unavailable" | Not yet offered to the player (the starting state) |
"available" | Flagged with (quest-unlock:); the giver offers it but it hasn't started |
"active" | Started, and the quest has no stages defined |
"<stageId>" | Started with stages; the value is the current stage id, not "active" |
"ready" | All stages done; waiting for the player to turn it in to the giver |
"complete" | Finished |
"failed" | Failed |
ana
(if: $quest.find_thief is "search")[The alley is quiet...]
(if: $quest.find_thief is "complete")[Old Bill thanks you warmly.]⚠️ Staged quests never hold
"active". For a quest with stages, the signal holds the current stage id, so(if: $quest.find_thief is "active")silently never matches. To check "is this quest underway?" regardless of stages, use(quest-is: "find_thief", active), which returnstruefor any started quest. Only compare against"active"directly if you know the quest has no stages.
The state machine: unavailable → ((quest-unlock:)) → available → ((quest-start:)) → active/<stageId> → ((quest-advance:) past the last stage) → ready → ((quest-turnin:)) → complete. (quest-fail:) moves any non-terminal state to failed; (quest-complete:) jumps straight to complete. The computed offer path (requires/dependsOn) lets the giver offer an unavailable quest without ever passing through available; see (quest-available:).
(quest-define: "id", title, "...", desc, "...", [kwargs])
Defines a quest. Call in @system(init).
Required: "id" (string), title, desc
Optional kwargs:
| Kwarg | Type | Description |
|---|---|---|
giver | string | NPC id that offers the quest |
requires | expr | Lazy condition; quest only appears when true |
dependsOn | array | Quest IDs that must be complete first |
reward-item | string [, n] | Award N items (default 1) on completion |
reward-rel | string, number | Award relationship delta on completion |
ana
(quest-define: "find_thief",
title, "The Missing Coin",
desc, "Someone stole from Old Bill. Find out who.",
giver, "bartender",
requires, ($rel.bartender > 20),
dependsOn, ["meet_bartender"],
reward-item, "gold_coin", 5,
reward-rel, "bartender", 15
)Multiple reward-item and reward-rel kwargs can be combined in one definition.
(quest-stage: "questId", "stageId", "description")
Adds a stage to an existing quest (call after (quest-define:), in order). Stages execute in insertion order.
ana
(quest-stage: "find_thief", "search", "Search the alley for clues.")
(quest-stage: "find_thief", "confront", "Confront the suspect at the docks.")State management
(quest-unlock: "id") → boolean
Flags a quest as available (unavailable → available). Once flagged, the quest's giver NPC offers it on the next visit, regardless of the quest's requires condition. Use this for story-driven availability: something happens, and now the quest can be picked up. Returns false if the quest isn't in the unavailable state.
This is the explicit counterpart to the computed offer path: if you set up requires/dependsOn, the giver offers the quest automatically when those are satisfied, and you never need (quest-unlock:). Reach for (quest-unlock:) when the trigger is a scripted event rather than a state condition.
ana
// After the player overhears a rumor:
(quest-unlock: "find_thief")
// Next time they talk to the bartender, the quest appears in (quest-available:).(quest-start: "id") → boolean
Starts a quest (unavailable or available → active). Updates $quest.id. Returns true on success. Returns false if the quest is already started or its prerequisites (dependsOn, requires) are not met.
ana
(if: (quest-start: "find_thief") is false)[
The quest has already started or prerequisites not met.
](quest-advance: "id") → boolean
Advances to the next stage. When past the last stage, the quest transitions to ready status, which means "waiting for turn-in", not complete. The quest is not complete yet; only (quest-turnin:) or (quest-complete:) moves it to complete. Returns true if something changed.
Stageless quests: A quest defined without any (quest-stage:) calls goes directly to ready status when (quest-start:) is called, since there are no stages to advance through. Use (quest-turnin:) or (quest-complete:) to finalize it.
(quest-complete: "id") → boolean
Force-state macro. Completes the quest regardless of its current status (even if failed or still active). Applies rewards and fires @on(questComplete). Returns false if already complete. This is a state management tool; use it for quests that complete automatically (story beats, exploration milestones). For normal quest flow that requires returning to an NPC, use (quest-turnin:) instead.
(quest-turnin: "id") → boolean
Finalizes a quest that is in ready state: all stages are complete and the player is formally handing it in. Applies rewards and fires @on(questComplete). Returns false if the quest is not in ready state. This is the intended path for quests requiring NPC turn-in.
ana
(if: (quest-is: "the_missing", ready))[
(link: "Tell him what you found")[(action: Bar_TurnIn)]
]
:: Bar_TurnIn
(quest-turnin: "the_missing")
"Good work. Here's what I promised."(quest-fail: "id") → boolean
Force-state macro. Fails the quest regardless of its current status (even if complete). Fires @on(questFail). Returns false if already failed. This is a state management tool; it overrides whatever status the quest is currently in. Use (quest-advance:) / (quest-turnin:) for normal quest flow.
NPC offer flow
(quest-available: "npcId") → string[]
Returns quest IDs that are currently available from that NPC. A quest qualifies when its giver is this NPC and either it was explicitly flagged with (quest-unlock:) (status available), or it is still unavailable but all its dependsOn quests are complete and its requires condition is met right now.
ana
(each: _q in (quest-available: "bartender"))[
(quest-offer: _q)
(link: "Accept")[(set: $world.selectedQuest to _q)(action: AcceptThisQuest)]
(link: "Not now")[(goto: BartenderMain)]
](quest-offer: "id")
Renders the quest title and description as prose in the current zone. Authors write their own accept/decline links.
ana
// Bartender passage
(if: (quest-is: "find_thief", unavailable))[
"Can you help me find who stole from me?"
(quest-offer: "find_thief")
(link: "Yes, I'll look into it")[(goto: AcceptFindThief)]
(link: "Sorry, can't help")[(goto: BartenderMain)]
]
// AcceptFindThief passage
(quest-start: "find_thief")
(goto: BartenderMain)Query macros
(quest-status: "id") → string
Returns the canonical status string: "unavailable", "available", "active", "ready", "complete", or "failed". "ready" means all stages are complete and the quest is awaiting formal turn-in. Always one of these six values.
(quest-is: "id", status) → boolean
Sugar for (quest-status: "id") is status.
ana
(if: (quest-is: "find_thief", active))[You're on the case.](quest-stage-is: "id", stageId) → boolean
Returns true if the quest is active and currently at the given stage.
ana
(if: (quest-stage-is: "find_thief", search))[Search the alley...](quest-list:) → string[]
(quest-list: filter) → string[]
Returns quest IDs filtered by status. Filter values: active (default, includes both active and ready), ready, complete, failed, available, all.
ana
(each: _q in (quest-list: active))[
Active: (quest-title: _q)
]Display helpers
(quest-title: "id") → string
Returns the quest's display title.
(quest-desc: "id") → string
Returns the quest's description.
(quest-stage-desc: "id") → string
Returns the description of the currently active stage (empty string if no active stage).
(quest-event-id:) → string
Returns the quest ID that triggered the current @on(questComplete) or @on(questFail) handler. Use this instead of a temp var since the handler runs in its own scope.
ana
:: QuestCompleteHandler @on(questComplete)
(set: _q to (quest-event-id:))
(notify: "Quest complete: " + (quest-title: _q))Quest log UI
The quest log is implemented as a passage template; copy templates/quest_log.ana to passages/ui/ and customize. It uses (quest-list:), (quest-title:), (quest-stage-desc:), and (quest-desc:) to build the UI.
The Q key opens QuestLog by default (engine keybind quests).
Events
| Event | Fires when |
|---|---|
@on(questComplete) | Any quest is completed via (quest-complete:) or (quest-turnin:) |
@on(questFail) | Any quest is failed via (quest-fail:) |
Use (quest-event-id:) inside the handler to identify which quest fired the event.
(quest-link-format: offer, "prefix", turnin, "prefix") (GameInit only)
Customizes the text prefix for quest offer and turn-in links injected by (load-npc:). Both kwargs are optional; omit either to keep the default.
ana
// GameInit — change the ★ prefix to something else
(quest-link-format: offer, "📋 ", turnin, "✓ ")The links injected by (load-npc:) appear in the options zone when the NPC has available or ready quests. Default prefix for offer links is "★ " and for turn-in links is "✓ ".
Achievement System
Tracks player accomplishments. Supports binary (condition-met) achievements and counted/tiered achievements (reach 10, then 25, then 50 of something). Achievements are checked automatically on every (time-advance:), or manually with (achieve-check:).
Open the achievements screen with the A key (default), or call (goto: AchievementsScreen).
(achieve-define: "id", name, "...", desc, "...", condition, <expr>)
(achieve-define: "id", name, "...", desc, "...", count, <expr>, tiers, N, N, ...)
Defines an achievement. Call in GameInit. Key-value pairs after the id can appear in any order.
| Key | Value | Notes |
|---|---|---|
name | string | Display name |
desc | string | Short description |
hidden | boolean | true hides name/desc until unlocked |
condition | expression | For binary achievements; unlocks when truthy |
count | expression | For counted/tiered; any expression returning a number |
tiers | N, N, ... | Bare numbers; defines unlock thresholds |
ana
// Binary — unlocked when condition becomes true
(achieve-define: "quest_done",
name, "Hero's Journey",
desc, "Complete the first quest",
condition, ($quest.stage is "complete"))
// Counted/tiered — tiers unlock as count crosses each threshold
(achieve-define: "crystal_hoarder",
name, "Crystal Hoarder",
desc, "Collect energy crystals",
count, $player.crystals,
tiers, 10, 25, 50)
// Item-set achievement using (contains-count:)
(achieve-define: "bronze_warrior",
name, "Bronze Warrior",
desc, "Collect the full bronze armor set",
count, (contains-count: $inv, "bronze_gloves", "bronze_chest", "bronze_helm"),
tiers, 1, 2, 3)
// Hidden achievement
(achieve-define: "secret",
name, "???",
desc, "...",
hidden, true,
condition, ($player.events contains "found_easter_egg"))(achieve-check: "id")
Evaluates the achievement's condition/count right now and unlocks the next tier if the threshold is met. Pushes a notification to the notify zone on unlock.
ana
// After a scripted event that might meet the condition:
(achieve-check: "quest_done")Auto-check runs for all achievements on every (time-advance:); you only need (achieve-check:) for immediate feedback without advancing time.
(achieve-unlock: "id")
Force-unlocks the next unprogressed tier regardless of condition. Use for scripted story moments (or for cheaters).
ana
(achieve-unlock: "quest_done")(achieve-unlocked: "id") → boolean
Expression macro. Returns true if the achievement is fully unlocked (all tiers complete).
ana
(if: (achieve-unlocked: "quest_done"))[
You remember your triumph.
](achieve-progress: "id") → number
Expression macro. Evaluates and returns the current count expression value for a counted achievement.
ana
Crystals: (achieve-progress: "crystal_hoarder") / 50(achieve-list:) → array
(achieve-list: all) → array
Expression macro. Returns an array of achievement IDs. Without all, returns only fully unlocked IDs. With all, returns every defined ID.
ana
You have unlocked (achieve-list:) achievements.
(each: _id in (achieve-list: all))[
(achieve-name: _id)
]Display helpers
Use these to build a custom achievements UI. Copy templates/achievements.ana to passages/ui/ as a starting point.
| Macro | Returns | Notes |
|---|---|---|
(achieve-name: "id") | string | "???" if hidden and not yet unlocked |
(achieve-desc: "id") | string | "..." if hidden and not yet unlocked |
(achieve-tiers-complete: "id") | number | How many tiers the player has unlocked |
(achieve-tiers-total: "id") | number | Total tiers defined (1 for binary) |
(achieve-next-threshold: "id") | number | Threshold for the next tier (0 for binary) |
(achieve-progress: "id") | number | Current count expression value |
Save/load
Achievement progress is included in the game save automatically. Achievement definitions (set in GameInit) are re-registered on every load.