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):
- Deep-clone the template payload (the cached template is never mutated).
- {{paramName}} substitution — every string value is walked and
{{key}}tokens are replaced withparams.get(key). An empty param drops the holding key entirely, so an entry can opt out of an optional template field by passing"". - Field overlay — every top-level field on the concrete entry except the reserved keys (
extends,params, the overrides/extras keys) wins over the template. - 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. - 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
| Type | Template store | Overrides | Extras | Array |
|---|---|---|---|---|
| Mastery | MasteryTemplates/ | nodeOverrides | extraNodes | nodes |
| Quest | QuestTemplates/ | objectiveOverrides | extraObjectives | objectives |
| Achievement | AchievementTemplates/ | criterionOverrides | extraCriteria | criteria |
| Class | ClassTemplates/ | advancementOverrides + baseGrantsOverrides | extraAdvancements | advancements |
| CommandReward | CommandRewardTemplates/ | levelOverrides | extraLevels | level 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.