Quests
Multi-stage objectives, mixing hard gates with model judgment.
The editor side is on Quests without code. This page covers the fields and the rule-side patterns.
A quest is a list of steps (stages). Each has an optional condition and a list of rule steps that run when it completes.
- With a condition: completes the moment it holds. Exact and cheap.
- Without one: the model judges it from the story. The
labelis what it judges, so write something observable: "Convince the harbormaster to look the other way".
Rewards
- Each step's rule steps run as that step completes.
rewardStepson the quest is the final payoff.rewardis text the quest-giver says out loud.
Wiring with world flags
Have actions set a world flag, and gate later steps on it:
$.setWorld({ key: 'harbor', value: 'closed' })
Rules read it back through $.world:
$.world.harbor === 'closed'
World flags are visible everywhere, so a location, an NPC and a quest step can all react to the same fact without knowing about each other. Declare a flag under World flags before a rule names it.
Forks
Steps run in order and nothing jumps the queue. To fork, give every path its own steps and
retire the one not taken with skipIf: a condition that, when true, marks the step failed
and moves on.
$.setWorld({ key: 'path', value: 'mercy' }) // the step that forks writes the choice
| Step | condition |
skipIf |
|---|---|---|
| Escort him to the checkpoint | escorted = true |
path = blood |
| Report the body to the precinct | reported = true |
path = mercy |
| Collect your pay | paid = true |
none: this is where they rejoin |
The skipped step shows as failed, never completed. Three-way forks are two skips in a row.
When the choice is made in play
A choice made in conversation (taking one side of a feud, sparing a prisoner) has no rule to run
$.setWorld. Make the step before the fork a choice: give it an outcome with a key and the
ways it can go, and the fork's skipIf gates read that key.
{ "id": "DECIDE", "label": "Decide the courier's fate",
"outcome": { "key": "path", "choices": [
{ "value": "mercy", "when": "The player let the courier go." },
{ "value": "blood", "when": "The player killed the courier." } ] } }
While the step is active the narrator is asked which choice happened, and only then: the step
completes with path set to one of the listed values. A rule that sets path first (using an
item, say) completes it the same way. A condition on a choice step is what must hold before
it is asked, such as being in the right place.
For a choice that belongs to no quest, a plugin can ask the narrator instead.
optional: true is not a fork. It means "not needed to finish". An incomplete optional
step still blocks the quest, so two optional arms on opposite flags park the quest forever.
Or branch the outcome
When both paths do the same tasks and only the payoff differs, keep one list and branch inside the step's rule steps:
if ($.world.path === 'mercy') {
$.grantXp(50)
} else {
$.give({ item: 'Bounty Purse' })
}
Or branch the story
For a choice that changes everything after it: quest A's rewardSteps sets a flag, and two
follow-up quests have opposite introduceConditions on it. Only one is ever offered.
Quests with no NPC
A notice-board bounty, a curse that starts in the crypt, the game's main thread: these live on
the game in quests[], not on a character.
locationNames scopes where the quest can be introduced. ["Notice Board"] means only
there; empty means anywhere. Once open, the quest travels with the player.
Three ways it opens:
$.startQuest({ quest: 'HARBOR_BOUNTY' })from any rule body: an item, a location event, a hook. Safe to run twice; an open or finished quest is left alone.autoIntroduce: trueopens it the momentintroduceConditionholds. Good for the main thread ("the plague has started").- Judged discovery: leave
autoIntroduceoff and set anintroduceCondition. Each turn the condition holds, the narrator is asked whether the player has actually learned of the quest (read the notice, overheard it). The condition is required here: without it the quest would be put to the narrator every turn forever, so an ungated one is never judged.
Who pays
With no rewardGiver, a world quest pays as soon as its last step completes. Set
rewardGiver to an NPC and the reward waits for the player to go back to them.
When the giver dies
The reward comes from a person. If the person who pays dies, the quest fails, even if the
work was done. Set failOnGiverDeath: false for jobs that outlive the giver, like a letter
addressed to someone else.
Example
The keeper's silence
- Talk to the keeper. No condition; judged.
- Find the logbook. Condition: has item Logbook. Rule step:
$.setWorld({ key: 'knows_about_tides', value: 'yes' }) - Confront the harbormaster. No condition. The narrator knows about the logbook because step 2 set a flag.
- Reward:
$.grantXp(50)and an item.