Quest Patterns
Complete worked examples for the most common quest structures. Each example includes full JSON for all pages and explains the page ordering principle in context.
QuestLines returns the first page whose requirements all pass. This means more specific states (completed, in-progress with objectives met) must appear before more general states (in-progress, not-started). Every example here follows this rule — study the Pages array order carefully.
Linear Quest with Kill Objective
The classic "kill N enemies" quest. A blacksmith asks the player to defeat goblins.
Goblins come in multiple variants (Goblin_Duke, Goblin_Miner, Goblin_Scout, etc.),
so the tracking tag uses Goblin* to count all of them together.
The quest has four pages covering each stage of the lifecycle.
Page Flow
NPC thanks the player for completing the job.
Turn-in page — all kills done, collect reward.
Progress check — quest active, still need more kills.
Offer page — player hasn't started yet.
Quest File
// quests/goblin_hunt.json
{
"Title": "Goblin Hunt",
"Description": "The blacksmith needs 10 goblins dealt with.",
"Requirements": [],
"Actions": [],
"Pages": [
"goblin_hunt_complete", // most specific first
"goblin_hunt_turnin",
"goblin_hunt_progress",
"goblin_hunt_intro" // least specific last
]
}
Page 1 — Intro
// pages/goblin_hunt_intro.json
{
"Id": "goblin_hunt_intro",
"Title": "A Smith's Plea",
"Name": "Aldric the Smith",
"Requirements": ["questNotStarted:goblin_hunt"],
"Dialog": "Those goblins are ransacking my supplies! Kill 10 of them and I'll make it worth your while, {username}.",
"Responses": [{
"Text": "I'll take care of it.",
"Actions": [
"questStarted:goblin_hunt",
"track:goblin_kills:kill:Goblin*" // counts Goblin_Duke, Goblin_Miner, Goblin_Scout, etc.
]
}, { "Text": "Not now.", "Actions": [] }]
}
Page 2 — Progress
// pages/goblin_hunt_progress.json
{
"Id": "goblin_hunt_progress",
"Title": "Keep At It",
"Name": "Aldric the Smith",
"Requirements": [
"questStarted:goblin_hunt",
"not:kill:Goblin*:10"
],
"Dialog": "Not done yet! You've killed {track:goblin_kills} out of 10 goblins. Keep going!",
"Responses": [{ "Text": "Right, back to it.", "Actions": [] }]
}
Page 3 — Turn In
// pages/goblin_hunt_turnin.json
{
"Id": "goblin_hunt_turnin",
"Title": "Job Done!",
"Name": "Aldric the Smith",
"Requirements": [
"questStarted:goblin_hunt",
"kill:Goblin*:10"
],
"Dialog": "You've done it! All 10 goblins slain. Here's your reward, {username}.",
"Responses": [{
"Text": "Happy to help.",
"Actions": [
"questCompleted:goblin_hunt",
"untrack:goblin_kills",
"item:gold_coin:25"
]
}]
}
Page 4 — Completed
// pages/goblin_hunt_complete.json
{
"Id": "goblin_hunt_complete",
"Title": "A Hero's Thanks",
"Name": "Aldric the Smith",
"Requirements": ["questCompleted:goblin_hunt"],
"Dialog": "Thanks to you, {username}, my forge is safe. I won't forget it.",
"Responses": [{ "Text": "Glad to help.", "Actions": [] }]
}
Item Turn-In Quest
The NPC asks the player to bring a specific item. The item:...:true
requirement on the turn-in response both gates the button and consumes the item
when clicked.
Quest File
// quests/lost_relic.json
{
"Title": "The Lost Relic",
"Pages": [
"relic_complete",
"relic_turnin",
"relic_fetch",
"relic_intro"
]
}
Intro Page
// pages/relic_intro.json
{
"Id": "relic_intro",
"Name": "Curator Voss",
"Requirements": ["questNotStarted:lost_relic"],
"Dialog": "The museum's prized relic was stolen! Retrieve the {b}Sunstone Amulet{/} and bring it to me.",
"Responses": [{ "Text": "I'll find it.", "Actions": ["questStarted:lost_relic"] }]
}
Fetch Page (no item yet)
// pages/relic_fetch.json
{
"Id": "relic_fetch",
"Name": "Curator Voss",
"Requirements": [
"questStarted:lost_relic",
"not:item:sunstone_amulet:1"
],
"Dialog": "You don't have the amulet yet. Try the old tomb to the east.",
"Responses": [{ "Text": "I'll keep searching.", "Actions": [] }]
}
Turn-In Page (has item — consumes it)
// pages/relic_turnin.json
{
"Id": "relic_turnin",
"Name": "Curator Voss",
"Requirements": [
"questStarted:lost_relic",
"item:sunstone_amulet:1" // no :true here — check only at page level
],
"Dialog": "You found it! The Sunstone Amulet — please, hand it over and I'll reward you.",
"Responses": [{
"Text": "Here you go.",
"Requirements": ["item:sunstone_amulet:1:true"], // :true = consume on click
"Actions": [
"questCompleted:lost_relic",
"item:museum_pass:1"
]
}]
}
The page has item:sunstone_amulet:1 (no consume) to select the
turn-in dialogue. The response has item:sunstone_amulet:1:true
to actually consume the item when the player clicks. This two-layer check
prevents the page being shown only to consume on an invisible button.
Branching Dialogue
A hub page offers multiple topics. Each response uses page:pageId
to navigate without NPC re-interaction. Sub-pages can return to the hub or lead
deeper.
Hub Page
// pages/elder_hub.json
{
"Id": "elder_hub",
"Name": "Elder Maeris",
"Requirements": [],
"Dialog": "Greetings, {username}. What would you like to know?",
"Responses": [
{ "Text": "Tell me about the ancient war.", "Actions": ["page:elder_lore_war"] },
{ "Text": "What can you tell me about the artifacts?", "Actions": ["page:elder_lore_artifacts"] },
{ "Text": "Goodbye.", "Actions": [] }
]
}
Sub-Page A — War Lore
// pages/elder_lore_war.json
{
"Id": "elder_lore_war",
"Name": "Elder Maeris",
"Requirements": [],
"Dialog": "A thousand years ago, the Sunken Kingdom fell in a war that shook the foundations of this world...",
"Responses": [
{ "Text": "Tell me more.", "Actions": ["page:elder_lore_war_2"] },
{ "Text": "I have other questions.", "Actions": ["page:elder_hub"] }
]
}
Sub-pages used purely for lore or dialogue don't need requirements — they are
only reached via page: actions, never by the page resolution
algorithm. You can optionally add them to a "lore_quest" to keep them organised,
or leave them as standalone pages. If you add them to a quest's Pages list,
give them restrictive requirements so they don't fire on normal NPC interaction.
Daily Reward with Cooldown
Two pages on one NPC. The "ready" page shows when the cooldown has elapsed
(or never been set). The "waiting" page shows while the cooldown is active.
Because cooldown: returns true when never set, the ready page
must be listed first.
Quest File
// quests/daily_reward.json
{
"Title": "Daily Reward",
"Pages": [
"daily_reward_ready", // cooldown elapsed OR never set — MUST be first
"daily_reward_waiting" // cooldown still active
]
}
Ready Page
// pages/daily_reward_ready.json
{
"Id": "daily_reward_ready",
"Name": "Innkeeper Brom",
"Requirements": ["cooldown:daily_reward:86400"],
"Dialog": "Welcome back, {username}! Your daily stipend is ready. Here, take it.",
"Responses": [{
"Text": "Thank you!",
"Actions": [
"item:daily_stipend:1",
"setTimestamp:daily_reward"
]
}]
}
Waiting Page
// pages/daily_reward_waiting.json
{
"Id": "daily_reward_waiting",
"Name": "Innkeeper Brom",
"Requirements": ["not:cooldown:daily_reward:86400"],
"Dialog": "You've already collected today. Come back in {cooldown:daily_reward:86400}.",
"Responses": [{ "Text": "Understood.", "Actions": [] }]
}
The {cooldown:daily_reward:86400} variable in the dialog text
renders as a human-readable countdown like "4 hours 23 minutes".
When the timer has elapsed it renders as "ready".
Timed Quest
A courier quest where the player must reach a destination within 5 minutes. The timer starts when the quest is accepted. A failure page shows if time expires before completion.
Page Order & Why
Both timedActive and timedExpired return false before
timedStart is called. That means the intro page (requiring
questNotStarted) catches the player initially. Once started, the
expired page is evaluated before the active page — if the timer has run out,
the failure screen shows; otherwise the active tracking page shows.
Quest File
// quests/courier_run.json
{
"Title": "The Courier Run",
"Pages": [
"courier_complete", // questCompleted — most specific
"courier_expired", // timedExpired — failure state, BEFORE active
"courier_active", // timedActive — quest in progress
"courier_intro" // questNotStarted — least specific
]
}
Intro Page (NPC A — Quest Giver)
// pages/courier_intro.json
{
"Id": "courier_intro",
"Name": "Dispatch Master",
"Requirements": ["questNotStarted:courier_run"],
"Dialog": "I need this package delivered to the docks in 5 minutes. Can you do it?",
"Responses": [{
"Text": "I'll run it now.",
"Actions": [
"questStarted:courier_run",
"item:delivery_package:1",
"timedStart:courier_timer" // start the 5-minute clock
]
}]
}
Active Page (NPC B — Dock Master)
// pages/courier_active.json
{
"Id": "courier_active",
"Name": "Dock Master",
"Requirements": [
"questStarted:courier_run",
"timedActive:courier_timer:300"
],
"Dialog": "A package for me? Excellent — you have {timeleft:courier_timer:300} left. Hand it over!",
"Responses": [{
"Text": "Here's the delivery.",
"Requirements": ["item:delivery_package:1:true"],
"Actions": [
"questCompleted:courier_run",
"item:gold_coin:30"
]
}]
}
Expired Page
// pages/courier_expired.json
{
"Id": "courier_expired",
"Name": "Dock Master",
"Requirements": [
"questStarted:courier_run",
"timedExpired:courier_timer:300"
],
"Dialog": "You're too late — the shipment window has closed. Return to the dispatch office.",
"Responses": [{
"Text": "My apologies.",
"Actions": [
"questRemoved:courier_run" // reset so player can try again
]
}]
}
Random Quest Pool Assignment
An NPC can be configured to randomly assign one quest per pool to each player on
first interaction. This is done via the NpcRandomGroupMap in
npcs.json. Players then see different quest content depending on
which variant was assigned.
NPC Config
// npcs.json — RandomGroups for citizen_10
{
"NpcRandomGroupMap": {
"citizen_10": "quest_hunt_wolves|quest_hunt_spiders;quest_gather_herbs|quest_gather_ore"
// ^--- pool 1 (pick one) ---------------^ ^--- pool 2 ---^
}
}
On first interaction, QuestLines picks one quest from each pool and stores it as
a tag on the player: assigned_quest:questId:citizenId. Use
hasTag:assigned_quest:quest_hunt_wolves:citizen_10 to show
quest-variant-specific content.
Variant-Specific Intro Pages
// pages/bounty_wolves_intro.json — only shown if wolves quest was assigned
{
"Id": "bounty_wolves_intro",
"Name": "Bounty Master",
"Requirements": [
"questNotStarted:quest_hunt_wolves",
"hasTag:assigned_quest:quest_hunt_wolves:citizen_10"
],
"Dialog": "Today's bounty: wolves are threatening the northern farms. Clear them out.",
"Responses": [{
"Text": "I'll handle it.",
"Actions": [
"questStarted:quest_hunt_wolves",
"track:wolf_kills:kill:Wolf_Black"
]
}]
}
// pages/bounty_spiders_intro.json — shown if spiders quest was assigned
{
"Id": "bounty_spiders_intro",
"Name": "Bounty Master",
"Requirements": [
"questNotStarted:quest_hunt_spiders",
"hasTag:assigned_quest:quest_hunt_spiders:citizen_10"
],
"Dialog": "Cave spiders are blocking the mine shaft. Clear them for a reward.",
"Responses": [{
"Text": "Done deal.",
"Actions": [
"questStarted:quest_hunt_spiders",
"track:spider_kills:kill:Spider_Cave"
]
}]
}
The assigned_quest:questId:citizenId tag is stored on the player
indefinitely. If you want a new assignment on the next cycle, remove the tag
via removeTag:assigned_quest:quest_hunt_wolves:citizen_10 in the
quest completion actions. On the next interaction, QuestLines will assign a new
random quest.
Multi-NPC Quest Chain
A quest that progresses through interactions with several different NPCs in sequence. Tags are used to track which step the player is on, since a single NPC cannot know which step a player is at without explicit markers.
Quest Design
The quest has three stages: talk to the Blacksmith, then the Herbalist, then the
Sage. The quest_chain quest's pages are spread across three different
NPCs, each linked to the same quest in npcs.json.
NPC Config
// All three NPCs share the same quest
{
"NpcQuestMap": {
"citizen_smith": "ancient_ritual",
"citizen_herbalist": "ancient_ritual",
"citizen_sage": "ancient_ritual"
}
}
Quest File
// quests/ancient_ritual.json
{
"Title": "The Ancient Ritual",
"Pages": [
"ritual_complete",
"ritual_sage_meeting", // step 3: talk to sage
"ritual_herbalist_meeting", // step 2: talk to herbalist
"ritual_smith_intro" // step 1: smith starts the quest
]
}
Step 1 — Blacksmith (quest giver)
// pages/ritual_smith_intro.json
{
"Id": "ritual_smith_intro",
"Name": "Aldric the Smith",
"Requirements": ["questNotStarted:ancient_ritual"],
"Dialog": "I need your help with an ancient ritual. First, go speak with the Herbalist about the reagents.",
"Responses": [{
"Text": "I'll help.",
"Actions": [
"questStarted:ancient_ritual",
"addTag:ritual_step_herbalist" // advance to step 2
]
}]
}
Step 2 — Herbalist
// pages/ritual_herbalist_meeting.json
{
"Id": "ritual_herbalist_meeting",
"Name": "Herbalist Lyra",
"Requirements": [
"questStarted:ancient_ritual",
"hasTag:ritual_step_herbalist",
"notTag:ritual_step_sage"
],
"Dialog": "Aldric sent you? The reagents are ready. Take them to the Sage on the hill.",
"Responses": [{
"Text": "I'll bring them to the Sage.",
"Actions": [
"item:ritual_reagents:1",
"removeTag:ritual_step_herbalist",
"addTag:ritual_step_sage" // advance to step 3
]
}]
}
Step 3 — Sage (final turn-in)
// pages/ritual_sage_meeting.json
{
"Id": "ritual_sage_meeting",
"Name": "Sage Ondrel",
"Requirements": [
"questStarted:ancient_ritual",
"hasTag:ritual_step_sage"
],
"Dialog": "At last! The reagents. Hand them over and the ritual can begin.",
"Responses": [{
"Text": "Here they are.",
"Requirements": ["item:ritual_reagents:1:true"],
"Actions": [
"questCompleted:ancient_ritual",
"removeTag:ritual_step_sage",
"item:ritual_reward:1"
]
}]
}
Remove step tags when transitioning to the next step (as shown above) rather than accumulating them. This keeps player data tidy and prevents incorrect page matches if the quest is ever reset and replayed.