Template Extension

Collapse repetitive content into reusable skeletons with a shared extends / params / overrides DSL

Why templates

When a pack ships many near-identical entries — one mastery track per combat skill, one quest per mob tier, the same milestone reward on every skill — a template captures the shared shape once and each concrete entry fills in only what differs. The Mastery Pack's combat tracks dropped from ~210 lines each to ~20–40, and its mastery-point milestone rewards collapsed from 2,386 lines to 6.

Five content types support templates: Mastery, Quest, Achievement, CommandReward, and Class.

The DSL

Every resolver runs the same pipeline (the shared primitives live in one place — JsonTemplateUtil):

  1. Deep-clone the template payload (the cached template is never mutated).
  2. {{paramName}} substitution — every string value is walked and {{key}} tokens are replaced with params.get(key). An empty param drops the holding key entirely, so an entry can opt out of an optional template field by passing "".
  3. Field overlay — every top-level field on the concrete entry except the reserved keys (extends, params, the overrides/extras keys) wins over the template.
  4. Overrides — for each id in the overrides map, find the matching entry in the primary array (by post-substitution id) and deep-merge: object keys merge recursively; primitives and arrays replace wholesale.
  5. Extras — append wholly new entries. A new id must NOT collide with a template id (use overrides to modify existing entries).

A missing param surfaces as a literal {{key}} in the resolved JSON (never silently dropped), so content typos are visible during validation. Unknown template ids log a warning and the entry is dropped. Template id lookup is case-insensitive.

Per-type field names

TypeTemplate storeOverridesExtrasArray
MasteryMasteryTemplates/nodeOverridesextraNodesnodes
QuestQuestTemplates/objectiveOverridesextraObjectivesobjectives
AchievementAchievementTemplates/criterionOverridesextraCriteriacriteria
ClassClassTemplates/advancementOverrides + baseGrantsOverridesextraAdvancementsadvancements
CommandRewardCommandRewardTemplates/levelOverridesextraLevelslevel map

Mastery template example

A track extends a skeleton, plugs in params, tweaks one node, and appends a unique capstone:

{
  "Name": "Archery_Mastery",
  "Payload": {
    "extends": "combat6_standard",
    "target": "skill:ARCHERY",
    "displayName": "Archery Mastery",
    "params": {
      "prefix": "arc",
      "gateSkill": "ARCHERY",
      "dmgIngredient": "Ingredient_Lightning_Essence",
      "combatTarget": "ARCHERY"
    },
    "nodeOverrides": {
      "arc_t1_dmg": { "cost": { "items": [ { "id": "Ingredient_Lightning_Essence", "count": 6 } ] } }
    },
    "extraNodes": [
      { "id": "arc_unique_capstone", "tier": 10, "displayName": "Eagle Eye", "cost": { }, "modifiers": [ ] }
    ]
  }
}

Magic and Artillery pass "combatTarget": "" so the empty-resolve rule drops the combatTarget key from every modifier — one template, different behavior per track.

CommandReward templates & {{ALL_SKILLS}}

A CommandReward template is a level → rewards map (the shape a single skill block carries). A CommandRewards payload then references it per skill — or uses the {{ALL_SKILLS}} sentinel as a top-level key to fan the template out to every known skill that isn't otherwise explicitly listed:

// CommandRewardTemplates/MasteryPointMilestones.json
{
  "Name": "MasteryPointMilestones",
  "Payload": {
    "15": [ { "command": "/mmocurrency give --player={player} --currency=mastery_point --amount=1" } ],
    "30": [ { "command": "/mmocurrency give --player={player} --currency=mastery_point --amount=1" } ]
  }
}

// CommandRewards/MyPack.json — fan to every skill in six lines
{
  "Name": "MyPack",
  "Payload": { "{{ALL_SKILLS}}": { "extends": "mastery_point_milestones" } }
}

The sentinel only fans to skill keys — the special TOTAL and GLOBAL_SKILL keys are never touched, and an explicit per-skill entry always wins over the fan-out. Per-skill blocks can also use levelOverrides (replace a level's rewards) and extraLevels (add new levels).

Quest & achievement templates

{
  "Name": "Pack_Daily_Kill_Goblin_T1",
  "Payload": {
    "extends": "daily_kill_template",
    "params": { "id": "pack_daily_kill_goblin_t1", "displayName": "Goblin Hunter I",
                "mobId": "Mob_Goblin_T1", "count": "10", "reward": "5" },
    "objectiveOverrides": { "kill_target": { "displayText": "Slay 10 Goblins" } }
  }
}

The easiest way to replace a template's rewards is to supply a fresh rewards: [ … ] at the top level (it overlays the whole array).

Class templates

The ClassStandard skeleton ships a three-rank ladder (Initiate / Adept / Master) with empty grants. A concrete class fills baseGrants via baseGrantsOverrides and differentiates each rank via advancementOverrides (matched by advancement id):

{
  "Name": "Warrior",
  "Payload": {
    "extends": "classstandard",
    "params": { "displayName": "Warrior", "flavor": "Front-line bruiser", "icon": "Weapon_Longsword_Iron" },
    "baseGrantsOverrides": {
      "xpMultipliers": { "SWORDS": 1.25, "MAGIC": 0.5 },
      "startingItems": { "Weapon_Longsword_Iron": 1 }
    },
    "advancementOverrides": {
      "master": { "requirements": { "skillLevels": [ { "skill": "SWORDS", "level": 50 } ] } }
    },
    "extraAdvancements": [ ]
  }
}

Remember to enable the template store in your Control file — add the matching "<Type>Templates": "add" key (e.g. "MasteryTemplates": "add") so the templates load before the content that extends them.