Page Ordering Principle

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.

1

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

goblin_hunt_complete questCompleted:goblin_hunt
NPC thanks the player for completing the job.
goblin_hunt_turnin questStarted:goblin_hunt & kill:Goblin*:10
Turn-in page — all kills done, collect reward.
goblin_hunt_progress questStarted:goblin_hunt & not:kill:Goblin*:10
Progress check — quest active, still need more kills.
goblin_hunt_intro questNotStarted:goblin_hunt
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": [] }]
}
2

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"
    ]
  }]
}
Page vs. Response Requirement

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.

3

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"] }
  ]
}
💡
Hub Sub-Pages Don't Need Quest Requirements

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.

4

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": [] }]
}
{cooldown:key:totalSeconds} Variable

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".

5

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
    ]
  }]
}
6

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"
    ]
  }]
}
Assignment Tags are Permanent

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.

7

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"
    ]
  }]
}
💡
Clean Up Step Tags

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.