Authoring Mastery Tracks

Pack-authored mastery tracks: track and node fields, costs, respec, and the modifier grammar

Content pack assetServer/MMOSkillTree/Masteries/*.jsonMasteries/

One file = one track. Parsed via MasteryConfig.parseTrack / parseNode. Templates live in MasteryTemplates/, see the Template Extension page.

defaults<pack<owner

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

FieldTypeDefaultDescription
disabledboolfalsetrue 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).
titleKeystringnoneDisplay is key-only for a track - convention mastery.<trackId>.title. A raw displayName literal is silently ignored, not even a deprecated fallback.
iconstringnoneItem id used as the track icon.
refundPercentintinheritsFalls back to the global mastery.json respec.defaultRefundPercent (50) if unset. A node-level refundPercent overrides this.
requirementsobjectnoneTrack-level unlock gate, the deep PrerequisiteGroup form (skill levels, quests, other tracks' nodes, etc).
nodesMasteryNode[][]The track's purchasable nodes, see below.
* required

Node fields

Each entry in a track's nodes[] array:

FieldTypeDefaultDescription
id*string-Missing/empty drops the node.
tierint0Visual tier row. 9 is the shipped convention for an "Eternal" repeatable capstone.
titleKey / descriptionKeystringnoneKey-only, same as track - convention mastery.<nodeId>.title. Do not author a node description; the effect line auto-renders from modifiers.
iconstringnoneItem id.
choiceGroupstringnoneSiblings sharing a choiceGroup are mutually exclusive (pick-one branching tier).
prerequisitesstring[][]Other node ids in this same track that must already be owned.
requirementsobjectnoneExternal gate (skill level, other track's node, etc), the deep PrerequisiteGroup form.
costobjectfreeThe shared Cost shape (combine/currencies/items) plus the mastery-only statSacrifice sub-object, below.
modifiersAbilityModifier[][]The effect grammar, see the seven shapes below.
refundPercentintinherits track/global
maxPurchasesintnull (single-purchase)-1 = infinite ("Eternal"); >1 = capped repeat count.
costScalingobjectnull (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.
* required

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

A node with 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.

FieldTypeDefaultDescription
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.
valuenumberoptionalRequired 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.
targetSkillstringnoneScopes this modifier to abilities whose XpRouting.Skills contains this skill.
targetAbilitystringnoneScopes to one specific ability id instead of a whole skill.
combatTargetstringnone (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.
* required

The seven shapes, one worked example each

FLAT - adds a flat amount to the target leaf (allowed on repeatable nodes):

json
{ "shape": "FLAT", "key": "damage", "value": 5, "targetAbility": "sword_slash" }

PERCENT - multiplies the target leaf (allowed on repeatable nodes; the loot-multiplier convention):

json
{ "shape": "PERCENT", "key": "lootMultiplier", "value": 0.03, "targetSkill": "MINING" }

OVERRIDE - replaces the target leaf outright (single-purchase only):

json
{ "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):

json
{ "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):

json
{
  "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:

json
{
  "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:

json
{
  "shape": "ADD_STEP",
  "targetAbility": "sword_slash",
  "insertAnchor": "ON_HIT",
  "stepPayload": { "Type": "ZONE", "Zone": { "AtVictim": true, "Radius": 2.0, "DamagePerTick": 5.0, "DurationMs": 0 } }
}
CONDITIONAL/TRIGGER/ADD_STEP are rare in shipped mastery content and single-purchase-only. See the ability step engine reference on Authoring Abilities for the full 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

Server/MMOSkillTree/Masteries/Simple_Mastery.json
{
  "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):

Server/MMOSkillTree/Masteries/Acrobatics_Mastery.json
{
  "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

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