Authoring Mastery Tracks
Pack-authored mastery tracks: track and node fields, costs, respec, and the modifier grammar
Server/MMOSkillTree/Masteries/*.jsonMasteries/One file = one track. Parsed via MasteryConfig.parseTrack / parseNode. Templates live in MasteryTemplates/, see the Template Extension page.
A mastery track is a raw-Payload asset type: the envelope keys (Name, Payload) are PascalCase, and the body inside Payload is authored lowerCamelCase. The filename, lowercased, is the runtime track id - unlike Quest/Achievement there is no inner Payload.id for the track itself; each node inside nodes[] does carry its own id.
Track fields
| Field | Type | Default | Description |
|---|---|---|---|
disabled | bool | false | true parks the track without removing the file; still ships in the zip, just skipped at load (logged). |
target* | string | - | "skill:<SKILL_ID>" or "ability:<abilityId>". Missing/empty drops the whole track. Determines the mastery scope (skill-wide vs. one ability). |
titleKey | string | none | Display is key-only for a track - convention mastery.<trackId>.title. A raw displayName literal is silently ignored, not even a deprecated fallback. |
icon | string | none | Item id used as the track icon. |
refundPercent | int | inherits | Falls back to the global mastery.json respec.defaultRefundPercent (50) if unset. A node-level refundPercent overrides this. |
requirements | object | none | Track-level unlock gate, the deep PrerequisiteGroup form (skill levels, quests, other tracks' nodes, etc). |
nodes | MasteryNode[] | [] | The track's purchasable nodes, see below. |
Node fields
Each entry in a track's nodes[] array:
| Field | Type | Default | Description |
|---|---|---|---|
id* | string | - | Missing/empty drops the node. |
tier | int | 0 | Visual tier row. 9 is the shipped convention for an "Eternal" repeatable capstone. |
titleKey / descriptionKey | string | none | Key-only, same as track - convention mastery.<nodeId>.title. Do not author a node description; the effect line auto-renders from modifiers. |
icon | string | none | Item id. |
choiceGroup | string | none | Siblings sharing a choiceGroup are mutually exclusive (pick-one branching tier). |
prerequisites | string[] | [] | Other node ids in this same track that must already be owned. |
requirements | object | none | External gate (skill level, other track's node, etc), the deep PrerequisiteGroup form. |
cost | object | free | The shared Cost shape (combine/currencies/items) plus the mastery-only statSacrifice sub-object, below. |
modifiers | AbilityModifier[] | [] | The effect grammar, see the seven shapes below. |
refundPercent | int | inherits track/global | |
maxPurchases | int | null (single-purchase) | -1 = infinite ("Eternal"); >1 = capped repeat count. |
costScaling | object | null (static cost) | Only meaningful with maxPurchases != 1: { mode, multiplier, softCap }. mode is EXPONENTIAL (base * multiplier^count, default) or POLYNOMIAL (base * (count+1)^multiplier). softCap (default 0 = none) freezes scaling past that purchase count. |
Node cost.statSacrifice
A mastery-only cost component paid in a stat instead of (or alongside) currency: { "stat": "HEALTH"|"STAMINA"|"MANA", "mode": "permanent_max"|"per_cast", "amount": N } (mode defaults permanent_max).
Forbidden on a repeatable node
statSacrifice on a repeatable node (maxPurchases != 1) is invalid. MasteryConfig.validate silently drops the whole track at load if this is violated, not just the offending node, and logs a warning. permanent_max lowers the stat's max cap permanently (reversed on respec); the floor per stat is configurable in mastery.json (statSacrifice.minRemaining, default 1).Costs & respec
cost uses the shared price object: combine (all pays every listed component, any lets the player pick one route), currencies (a {currencyId: amount} map), and items (raw inventory items, drained on purchase). There is no scalar cost+currencyId form for masteries; omitting cost entirely defaults a node to free.
Respec refunds a percentage of everything spent on a track, walking node.refundPercent → track.refundPercent → the global respec.defaultRefundPercent in mods/mmoskilltree/mastery.json. A statSacrifice's permanent_max reduction is restored in full on respec regardless of refund percent (the stat cap itself is not a currency to be partially refunded).
The modifier grammar
modifiers[] uses AbilityModifier, the same JSON grammar shared with AbilityMod (the 1.6.0 ABILITY_MOD reward type) - see Authoring Abilities for the ability-side key list (damage, cooldownMs, lifesteal, comboFinisherBonus, and more) that a modifier's key resolves against via ModifierTargetTable.
| Field | Type | Default | Description |
|---|---|---|---|
shape* | enum | - | One of the seven shapes below. On a repeatable node (maxPurchases != 1), only FLAT/PERCENT are allowed - the other five are dropped at load (and, per the statSacrifice rule above, a disallowed shape on a repeatable node drops the whole track). |
key* | string | - | The paramKey. Two are mastery-only conventions with no ability-side reader: lootMultiplier (PERCENT, luck-roll bonus, needs targetSkill) and xpGain (reserved, currently unused). Every other key targets an ability leaf. |
value | number | optional | Required for FLAT/PERCENT/OVERRIDE/ADD_PARAM. FLAT: base + value. PERCENT: base * (1 + value) (e.g. 0.05 = +5%). OVERRIDE: replaces the value, last-write-wins. |
targetSkill | string | none | Scopes this modifier to abilities whose XpRouting.Skills contains this skill. |
targetAbility | string | none | Scopes to one specific ability id instead of a whole skill. |
combatTarget | string | none (ability-cast-only) | One of ALL/MELEE/RANGED/a skill id. Absent = the modifier only applies on ability casts. Set = it also applies to plain weapon swings of that combat category. An empty string via template substitution drops the key entirely - some tracks pass combatTarget: "" to opt out for non-weapon skills like Magic/Artillery. |
The seven shapes, one worked example each
FLAT - adds a flat amount to the target leaf (allowed on repeatable nodes):
{ "shape": "FLAT", "key": "damage", "value": 5, "targetAbility": "sword_slash" }PERCENT - multiplies the target leaf (allowed on repeatable nodes; the loot-multiplier convention):
{ "shape": "PERCENT", "key": "lootMultiplier", "value": 0.03, "targetSkill": "MINING" }OVERRIDE - replaces the target leaf outright (single-purchase only):
{ "shape": "OVERRIDE", "key": "cooldownMs", "value": 4000, "targetAbility": "dash" }ADD_PARAM - injects a new cast parameter that did not exist on the base ability (single-purchase only):
{ "shape": "ADD_PARAM", "key": "comboFinisherBonus", "value": 0.15, "targetAbility": "sword_slash" }CONDITIONAL - applies a nested modifier only when a runtime condition holds (single-purchase only; rare in shipped mastery content). condition is a flat set of optional gates (selfHpMin/selfHpMax/targetHpMin/targetHpMax/hostileOnly/onCritOnly/filter), not a typed object - apply is the wrapped modifier, and the outer key is redundant (the modifier inherits apply's key):
{
"shape": "CONDITIONAL",
"targetAbility": "sword_slash",
"condition": { "targetHpMax": 0.3 },
"apply": { "shape": "PERCENT", "key": "damage", "value": 0.25 }
}TRIGGER - subscribes the player to an EXISTING passive ability's own PassiveTrigger group (event/filter/weapon/HP gates), rather than injecting a new effect (single-purchase only). targetAbility names that passive ability's id - it must already exist in the ability catalog as a passive asset:
{
"shape": "TRIGGER",
"targetAbility": "bleed_on_hit_passive",
"triggerEvent": "on_hit_dealt"
}ADD_STEP - inserts a brand-new cast step into the ability's step sequence (single-purchase only). insertAnchor is one of START/END/ON_HIT only; stepPayload decodes through EffectStep's own codec, so it uses the same PascalCase field names as an authored ability step (Type, then a nested group matching that Type) - never the modifier's own lowerCamelCase grammar:
{
"shape": "ADD_STEP",
"targetAbility": "sword_slash",
"insertAnchor": "ON_HIT",
"stepPayload": { "Type": "ZONE", "Zone": { "AtVictim": true, "Radius": 2.0, "DamagePerTick": 5.0, "DurationMs": 0 } }
}EffectStep kind/leaf grammar stepPayload decodes against.Template DSL
Mastery is one of the five types supporting the shared extends/params DSL, with the {{ALL_SKILLS}} sentinel and forEachSkill fan-out available at the CommandReward/Shop layer for milestone rewards tied to a track. Full mechanics, the five-step resolver pipeline, and the override/extras field names per type live on Template Extension. A mastery entry uses nodeOverrides (deep-merge by node id) and extraNodes (append new nodes); the real Mastery Pack collapses combat tracks from ~210 lines to ~20-40 this way.
Minimal example
{
"Name": "Simple_Mastery",
"Payload": {
"target": "skill:MINING",
"titleKey": "mastery.simple_mastery.title",
"nodes": [
{
"id": "smp_t1",
"tier": 1,
"cost": { "currencies": { "mastery_point": 2 } },
"modifiers": [
{ "shape": "PERCENT", "key": "lootMultiplier", "value": 0.03, "targetSkill": "MINING" }
]
}
]
}
}Exhaustive example
Abridged from the real Acrobatics_Mastery.json track (two of its nodes):
{
"Name": "Acrobatics_Mastery",
"Payload": {
"disabled": true,
"target": "skill:ACROBATICS",
"refundPercent": 50,
"requirements": {
"mode": "AND",
"skillLevels": [ { "skill": "ACROBATICS", "level": 5 } ]
},
"nodes": [
{
"id": "acr_t1",
"tier": 1,
"cost": {
"combine": "all",
"currencies": { "mastery_point": 2, "life_essence": 500 },
"items": [ { "id": "Ingredient_Lightning_Essence", "count": 1 } ]
},
"modifiers": [
{ "shape": "PERCENT", "key": "lootMultiplier", "value": 0.03, "targetSkill": "ACROBATICS" }
]
},
{
"id": "acr_eternal",
"tier": 9,
"prerequisites": ["acr_t1"],
"maxPurchases": -1,
"costScaling": { "mode": "EXPONENTIAL", "multiplier": 1.1 },
"cost": {
"combine": "all",
"currencies": { "life_essence": 200 },
"items": [ { "id": "Ingredient_Lightning_Essence", "count": 2 } ]
},
"modifiers": [
{ "shape": "PERCENT", "key": "lootMultiplier", "value": 0.005, "targetSkill": "ACROBATICS" }
]
}
]
}
}Eternal capstone convention
9 with maxPurchases: -1 and a small per-purchase PERCENT bonus is the shipped pattern for an infinitely-repeatable capstone node - the cost climbs via costScaling so it stays a meaningful currency sink at high purchase counts.