Skip to content

Quests, Goals & Rewards

A quest is an Ink knot with a # quest: tag. It has goals that the game checks, and rewards that the player gets at the end. Quests show in the Quest Ledger, and the player can track them on screen.

=== nets ===
# quest: Mend the nets
# about: The fishers need a tailor to mend their nets.
# goal: tailor_here
# goal: catches(20)
# reward: tackle_box
# skip if: tailor_here
# label: About the nets
Half these nets are more hole than net.
->->
= waiting
Any word on a tailor's shop?
-> DONE
= done
The nets hold! We pulled in more today than all week.
-> DONE
= skipped
You already have a tailor? Then the nets are as good as mended.
-> DONE
TagDescription
# quest: <title>Makes the knot a quest. The title shows in the Quest Ledger.
# about: <text>A short description for the Quest Ledger.
# goal: <name>A goal that must be met. Repeat the tag for more goals. With parameters: # goal: catches(20).
# reward: <name>A reward paid at the end. Repeat the tag for more rewards.
# skip if: <name>A goal that, when already met as the quest starts, completes the quest at once. It cannot be an event goal.
# label: <text>The story’s entry in the dialogue screen while the quest is open.
StitchPlays
= waitingWhen the player talks to the teller while the quest is open, and no scene is ready. See scene_ready().
= doneThe next time the player talks to the teller after every goal is met.
= skippedInstead of done, when # skip if: was already met as the quest started.

All three are optional. A quest without = waiting does not change where a talk starts. Leave it out of a side quest, so that the other scenes still play while the side quest is open.

  1. Start. The quest starts the first time the story enters the knot. A tunnel (-> nets ->) is a good way to enter it and come back.
  2. Open. The Quest Ledger shows the quest, its goals and their progress. A message shows the new quest to the player.
  3. Ready. When every goal is met, the teller has something to say. A mark shows over their head.
  4. Hand back. The next conversation starts at = done (or = skipped). When it has played, the quest is complete.
  5. Rewards. Townstead pays the rewards once, when the quest is complete.

A quest with no = done stitch completes as soon as its goals are met, and pays its rewards then.

Each quest runs once for each player and each copy of the story. See Bind.

A goal is a JSON object. Write goals in story.json or persona.json under goals, or share them in a goal file.

"goals": {
"tailor_here": {
"text": "Build a tailor's shop in {teller}'s village",
"condition": { "type": "pheno:village", "has_building": "tailors_shop" }
}
}

Every goal needs text, the line in the Quest Ledger, and one of these shapes:

ShapeFieldsMet when
ConditionconditionThe Pheno condition is true.
Valuevalue, targetThe Pheno value reaches target. The ledger shows progress.
Eventevent, count, where, entity, condition, distinctThe event happened count times. See Event Goals.
Seasonsseasons, condition, and moreThe condition held for that many good seasons. See Season Goals.
FieldDescription
textRequired. The ledger line. {teller} is the teller’s name, {player} the player’s, and {count} the target number.
subjectWho the goal is tested on: teller (default) or player. A condition goal tests the condition on that entity.
paramsParameter names, for a goal with values. See Parameters.
markerA place marked for the player. {x} and {z} in the text show its position.

Townstead checks condition and value goals on a timer, and again after events that can change them. A goal can be met while the player is far away.

An event goal counts Townstead events, such as work done or a building raised.

"catches": {
"params": ["count"],
"text": "{teller} brings in {count} catches",
"event": "townstead:work_completed",
"count": "$count",
"where": { "worker": "teller", "verb": "townstead:fished" }
}
FieldDescription
eventThe event ID. See Events.
countHow many times. The default is 1.
whereFields of the event that must match. See below.
entity, conditionA Pheno condition tested on the entity in the event field entity.
distinctAn event field. Each value of it counts once, such as "objectId" for different crops.

In where, each key is a field of the event, and its value must match:

  • text or a number;
  • "teller" or "player": that person, or their village for a village field;
  • a list: any of the values;
  • {"min": 2, "max": 5}: a number in the range.

When a field name is wrong, /townstead story errors lists the fields that the event has.

An event goal named in the story’s own JSON counts from the moment the player meets the teller, quest or not. Ink can read it with count("name").

The event ID is the event’s name in snake case, with the townstead: namespace.

GroupEvents
Work and careerswork_completed, villager_tier_changed, villager_skill_learned, villager_skill_forgotten, villager_profession_changed, worksite_registered, worksite_removed
Buildings and villagesbuilding_established, building_upgraded, building_removed, settlement_founded, village_needs_band_changed, village_spirit_changed
Villagersvillager_born, villager_died, villager_married, villager_life_stage_changed, villager_village_changed, villager_root_changed, villager_collapsed, villager_recovered, villager_refueled, villager_crisis
Socialconversation_held, hangout_started, hangout_ended, dialogue_opened, dialogue_closed, bond_changed, chronicle_event_recorded
Politicsfaction_founded, faction_identity_changed, faction_kind_changed, faction_status_changed, settlement_faction_changed, legitimacy_changed, seat_designated, seat_moved, seat_damaged, seat_repaired, seat_lost
Worldcalendar_rollover, world_settings_changed

Fields of some common events:

EventFields
work_completedworker, uuid, professionId, verb, objectId, magnitude, appliedXp, tierBefore, tierAfter
villager_tier_changedworker, uuid, professionId, tierBefore, tierAfter, appliedXp
building_establishedvillage, buildingId, type, family, tier
building_upgradedvillage, buildingId, typeBefore, typeAfter, tierBefore, tierAfter
villager_collapsedvillager, uuid, energy
villager_bornbaby, uuid

A season goal counts good seasons. A season is good when its condition held on enough of its days.

"steady": {
"text": "Keep five people in {teller}'s village for a whole season",
"seasons": 1,
"condition": { "type": "pheno:village", "min_population": 5 },
"share": 0.75
}
FieldDescription
seasonsHow many good seasons.
conditionRequired. What makes a day good. Townstead checks it once a day.
shareThe share of checked days that must be good, from 0 to 1. The default is 0.75.
event, where, per_seasonOptional. The event must also happen per_season times in the season.
season_daysThe length of a season in days, when no seasons mod is installed. The default is 24.

With a seasons mod, Townstead uses that mod’s seasons.

A goal with params takes values where it is used. In the goal, write $name for each value. In text, write {name}.

"have_profession": {
"params": ["profession", "count"],
"text": "Have {count} {profession}s",
"value": { "type": "pheno:count", "on": { "type": "pheno:village",
"where": { "type": "pheno:profession", "profession": "$profession" } } },
"target": "$count"
}
# goal: have_profession(farmer, 3)

A reward is a JSON object, written under rewards or in a reward file. A quest names it with # reward:.

ShapeExampleNotes
Items{ "items": [{ "item": "minecraft:cod", "count": 8 }] }The ledger shows the items. No text needed.
Loot table{ "text": "A sack of whatever {teller} found", "loot_table": "minecraft:chests/village/village_fisher" }Rolled when paid. Needs text.
Action{ "text": "A tackle box", "action": { "type": "pheno:give_story_item", "item": "examplepack:tackle_box" } }A Pheno action on the player. Needs text.

Rewards take params in the same way as goals: # reward: bread(6).

Townstead pays each reward once, when its quest is complete. When an update adds a reward to a quest that a player already completed, Townstead pays the new reward then.

Goals and rewards that several stories use can have their own files:

data/<namespace>/goal/<name>.json
data/<namespace>/reward/<name>.json

The file holds one goal or one reward. A story finds a name in this order:

  1. its own goals or rewards;
  2. a file with that name in the story’s namespace;
  3. a file with that name in the townstead namespace.

A name with a namespace, such as examplepack:good_standing, names a file directly.