Authoring Quests & Bounties

Every Quest field, bounty-board rotation, and turn-in bounty lifecycle

A quest is a per-entry pack asset with objectives and rewards. A bounty is just a repeatable quest tagged into a rotating board pool - there is no separate "Bounty" asset type. This page covers the Quest schema, then the BountyBoard schedule asset that pulls tagged quests into a rotation.

Content pack assetServer/MMOSkillTree/Quests/*.jsonQuests/

Filename is PascalCase (a human echo only); the runtime id is the inner Payload.id, case-sensitive. Templates live in Server/MMOSkillTree/QuestTemplates/*.json.

defaults<pack<owner

Folder layout

YourPack.zip
├── manifest.json
└── Server/MMOSkillTree/
    ├── Quests/
    │   └── Bounty_Cache_Copper.json
    ├── QuestTemplates/
    │   └── Bounty_Kill_Standard.json
    └── BountyBoards/
        └── Daily.json

Field names are read for their shared schemas (Requirements, Cost, Reward, ObjectiveSpec) - see Shared Schemas for those tables; this page only lists the fields unique to Quest and BountyBoard.

Quest fields

FieldTypeDefaultDescription
id*string-Read separately, not through the codec body. Rejects reserved delimiters.
displayNamestringechoes idDeprecated raw fallback - prefer titleKey.
descriptionstring""Deprecated raw fallback - prefer flavorKey.
titleKeystringnullLocalization key for the quest title. Convention: quest.<id>.title.
flavorKeystringnullLocalization key for markdown flavor/lore text. Convention: quest.<id>.flavor.
titleArgs / flavorArgsstring[][]Ordered template args (@amount/@skill/literal) for the matching key.
categorystring"misc"UI grouping; built-ins sort main, daily, misc first, then custom alphabetically.
enabledbooltrueRaw config flag - use the quest's runtime availability check, not this alone, for gating (also folds requiresFeatures).
requiresFeaturesstring[] (or bare string)[]See Feature gating.
sortOrderint0UI sort order.
autoAcceptboolfalseAuto-accepted for every eligible player at PlayerReady.
autoTrackboolfalseAuto-pinned to the tracker the moment it is accepted.
sequentialboolfalseLegacy ordering flag - prefer per-objective order.
repeatableboolfalseRe-offerable after cooldownSeconds once COMPLETED.
cooldownSecondslong0Only relevant when repeatable is true.
prerequisitesstring[] or PrerequisiteGroup[]A flat array is an AND-all of quest ids. An object is a full recursive group - see PrerequisiteGroup.
requiredAchievementsstring[][]Shorthand merged into the same prerequisite group as achievements.
permissionstringnullPermission-node gate.
requirementsobject or arrayoptionalLegacy shorthand (minLevel/minSkill/minSkillLevel). Prefer the unified Requirements/prerequisites block on new content.
requiresSkills{SKILL: level}{}Alias-folds into the same skill-requirement set as requirements-derived entries.
autoClaimRewardsboolfalsefalse parks rewards in COMPLETED_UNCLAIMED until claimed. Bounties force false regardless of this flag.
objectives*ObjectiveSpec[]-A quest with zero authored objectives and no turnInNpcId is rejected. See ObjectiveSpec.
rewardsReward[][]See Reward.
visibility{hidden, permission, requirePrerequisites, requireLevel}all false/nullhidden means never shown in the normal quest UI - bounties set this.
tagsstring[][]Free-form. Bounties tag ["bounty","board:<id>","diff:<x>","weight:<n>"].
resetsOnCompletestring[] (quest ids)[]Resets those other quests' progress when this one completes. Self-reference is warned and ignored.
variables{name: value}{}Free-form key-value store, keys keep authored casing.
npcViewIdstringnullThe giver NPC's view id; also gates the quest into that NPC's listing.
turnInNpcIdstringnull"giver" auto-appends a synthetic report-back TURN_IN objective at npcViewId (warned+skipped if no npcViewId); any other value turns in at a different NPC than the giver. Skipped if a TURN_IN objective is already hand-authored.
completionDialoguestringnullDialogue id the NPC page hands off to on completion, instead of reopening the quest list.
npcDisplayNamestringnull
npcRequiredToAcceptboolfalse
incompleteMarkdown / activeMarkdown / completeMarkdownstringnullPer-state NPC-page detail markdown.
* required

matchMode default: CONTAINS, not EXACT

Quest objectives fall back to matchMode: "CONTAINS" when omitted - the opposite of achievement criteria, which default to EXACT. Always author matchMode explicitly. See ObjectiveSpec.

Localization keys

Prefer titleKey/flavorKey (and, per objective, textKey) over the deprecated raw displayName/description/displayText literals - a raw literal skips localization entirely and cannot be translated by the localization pipeline. Leaving displayText unset on an objective lets the objective text renderer auto-generate a localized, pluralized sentence from type/target/amount, which is usually enough.

Template DSL

Quests support the shared extends/params template DSL (override field objectiveOverrides, extras field extraObjectives, matched by objective id). This is how the shipped bounty pack generates dozens of near-identical bounty entries from a handful of QuestTemplates/*.json skeletons. Full mechanics: Template Extension.

Minimal quest example

Server/MMOSkillTree/Quests/Simple_Kill.json
{
  "Name": "Simple_Kill",
  "Payload": {
    "id": "simple_kill",
    "category": "misc",
    "objectives": [
      { "id": "main", "type": "KILL_ENTITY", "target": "*", "amount": 5, "matchMode": "CONTAINS" }
    ],
    "rewards": [
      { "type": "XP", "skill": "SWORDS", "amount": 500 }
    ]
  }
}

Every-field example (real bounty template)

Server/MMOSkillTree/QuestTemplates/Bounty_Gather_Standard.json
{
  "Name": "Bounty_Gather_Standard",
  "Payload": {
    "category": "bounty",
    "repeatable": true,
    "cooldownSeconds": 79200,
    "autoClaimRewards": false,
    "npcViewId": "bounty_board",
    "visibility": { "hidden": true },
    "tags": ["bounty", "board:{{boards}}", "diff:{{difficulty}}", "weight:{{weight}}"],
    "objectives": [
      { "id": "main", "type": "BREAK_BLOCK", "target": "{{target}}", "amount": 1, "matchMode": "CONTAINS" }
    ],
    "rewards": [
      { "type": "CURRENCY", "currencyId": "bounty_token", "amount": 1 }
    ]
  }
}
Server/MMOSkillTree/Quests/Bounty_Cache_Copper.json - a concrete entry extending the above
{
  "Name": "Bounty_Cache_Copper",
  "Payload": {
    "extends": "bounty_gather_standard",
    "id": "bounty_cache_copper",
    "params": { "boards": "daily", "difficulty": "normal", "weight": "2", "target": "Ore_Copper" },
    "objectiveOverrides": { "main": { "amount": 30 } },
    "rewards": [
      { "type": "CURRENCY", "currencyId": "bounty_token", "amount": 160 },
      { "type": "XP", "skill": "MINING", "amount": 1400 },
      { "type": "COMMAND", "command": "/give {player} Ingredient_Bar_Copper --quantity=8" }
    ]
  }
}

Bounty authoring: BountyBoard

The board asset is only the schedule - which tagged quests get drawn into which slots, on what cadence, and who can accept each difficulty. The bounty content itself is authored as ordinary Quest entries tagged ["bounty", "board:<id>", "diff:<x>", "weight:<n>"], npcViewId: "bounty_board", visibility.hidden: true.

Content pack assetServer/MMOSkillTree/BountyBoards/*.jsonBountyBoards/

Filename (lowercased) is the runtime board id and the value bounty quests target with their board:<id> tag.

defaults<pack<owner
FieldTypeDefaultDescription
titleKey / descriptionKeystringnull
iconstringnull
enabledbooltruePer-board on/off toggle.
orderint0Sort order and default-board tiebreak.
rotationobjectdaily intervalSee Rotation, selection, slots, reroll.
selectionobjectweighted_random / period
slotsarray[]Each slot's tier filter matches a bounty's diff:<x> tag (alias difficulty in a filter sub-object).
rerollCostobjectRerollCost.NONE (no paid reroll)
requirementsRequirementsungatedWho can even open this board.
combatLevelRequirements{difficulty: minCombatLevel}{}Per-difficulty accept gate, checked at accept time, not selection time - everyone sees the same board, low-level players just see locked rows. Keys are lower-cased difficulty names; an omitted difficulty is ungated. Negative/non-numeric values are warned and skipped (treated as ungated).
* required

Rotation, selection, slots, reroll (shared with ShopPool)

This same shape drives the Token Shop's rotating pools too - see Shop.

FieldTypeDefaultDescription
rotation.type"interval" (only shipped type)optional
rotation.period"daily" | "weekly" | "<N>s"optionale.g. "7200s".
rotation.anchor"utc" | "utc_monday"optional
rotation.offsetMinutesintoptional
selection.type"weighted_random" (only shipped type)optional
selection.seed"period" (only shipped seed)optional
slots[].filter.tierstringoptionalAlias difficulty. Flat fields (tier/difficulty/tag directly on the slot, no nested filter) are also accepted.
slots[].filter.tagstringoptional
slots[].countint1Clamped to [1, 64].
slots[].optionalboolfalseSkipped silently if unfillable.
rerollCost.currencystring (currency id)optional
rerollCost.amountlongoptional
rerollCost.maxPerPeriodint0 (unlimited)
* required

TURN_IN / deliver bounties and the claim-at-board lifecycle

A bounty can require the player to physically deliver items at the board rather than just complete an objective in the field:

  • Author an explicit TURN_IN objective inside the bounty quest's objectives[], scoped by turnInNpcId. Two shipped shapes: a plain deliver bounty is a single TURN_IN objective against a concrete item id (the Bounty_TurnIn_Standard template, extended by the Bounty_Deliver_* entries - the player brings items gathered elsewhere); a gather-and-deliver bounty is TWO objectives, a BREAK_BLOCK gather step plus a TURN_IN deliver step for a portion of the haul (the Bounty_GatherDeliver_Standard template, extended by entries like Bounty_Mine_Iron).
  • Because bounties force autoClaimRewards: false, rewards park as COMPLETED_UNCLAIMED until the player opens the board again and claims them - the claim step happens at the board, not automatically on objective completion.
  • Deliver-style bounties reward CURRENCY and XP only by convention, not a hard engine constraint - a claimed board bounty can safely carry a /give item reward (the Claim button is inventory-space-checked, the same as any other bounty), but doing so right after the player just emptied that inventory space to turn in items is awkward, so the shipped pack keeps deliver bounties CURRENCY + XP only.

Minimal board example

Server/MMOSkillTree/BountyBoards/Simple.json
{
  "Name": "Simple",
  "Payload": {
    "order": 5,
    "slots": [ { "filter": { "difficulty": "normal" } } ]
  }
}

Every-field example (real: Daily board)

Server/MMOSkillTree/BountyBoards/Daily.json
{
  "Name": "Daily",
  "Payload": {
    "titleKey": "ui.bounty.board.daily",
    "descriptionKey": "ui.bounty.board.daily.desc",
    "order": 0,
    "rotation": { "type": "interval", "period": "daily", "anchor": "utc", "offsetMinutes": 0 },
    "selection": { "type": "weighted_random", "seed": "period" },
    "combatLevelRequirements": { "normal": 25, "hard": 60 },
    "slots": [
      { "filter": { "difficulty": "training" } },
      { "filter": { "difficulty": "training" } },
      { "filter": { "difficulty": "easy" } },
      { "filter": { "difficulty": "normal" } },
      { "filter": { "difficulty": "normal" } },
      { "filter": { "difficulty": "hard" }, "optional": true }
    ],
    "rerollCost": { "currency": "bounty_token", "amount": 25, "maxPerPeriod": 3 }
  }
}

The public pack that ships this content is the MMO Skill Bounty Pack and the MMO Skill Quest Pack; see Add-ons for install links. Precedence for both Quest and BountyBoard content resolves as

defaults<pack<owner
- the server owner's own quest/board files under mods/mmoskilltree/ always win.