Authoring Mastery Tracks

Pack-authored mastery tracks: the structured schema, Parent reuse, generators, costs, and the modifier grammar

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

One file = one track (the filename, lowercased, is the track id). A whole family of tracks is one MasteryGenerators/ file, below.

defaults<pack<owner

Prefer a content pack: author one file per track under Server/MMOSkillTree/Masteries/ inside a pack folder, zip it, and drop the .zip into your server Mods/ folder. Building the zip.

A mastery track is one structured file with PascalCase fields at the top level - there is no Name/Payload wrapper. The filename, lowercased, is the runtime track id; Nodes is a map keyed by node id, in display order. It is the same schema wherever a track body appears: a pack file, a generator's Child, or an entry in the owner's mastery.json overlay.

Server/MMOSkillTree/Masteries/Swordplay_Focus.json
{
  "Parent": "mastery_base",
  "Target": "skill:SWORDS",
  "Requires": { "Factors": [ { "Factor": "hytale:stat", "Param": "MMO_Level_SWORDS", "Min": 16 } ] },
  "Nodes": {
    "swp_t1": {
      "Tier": 1,
      "Cost": { "Currencies": { "mastery_point": 3 },
                "Items": [ { "Item": "Ingredient_Lightning_Essence", "Count": 1 } ] },
      "Modifiers": [ { "Shape": "PERCENT", "ParamKey": "damage", "Value": 0.08,
                       "TargetSkill": "SWORDS", "CombatTarget": "SWORDS" } ]
    }
  }
}

Track fields

FieldTypeDefaultDescription
ParentstringnoneAnother track (or an Abstract base) to inherit from; see Reuse.
Abstractboolfalsetrue marks a shared base that never becomes a track of its own. Abstract itself never inherits, so a child of a base is a real track.
Enabledbooltruefalse parks the track without removing the file - in its own file, a later pack's same-id file, or an owner's mastery.json entry; the same leaf switches a shipped-parked track back ON.
Target*string-"skill:<SKILL_ID>", "ability:<abilityId>", or the bare word "global" (the width a school track wants; see School-scoped modifiers).
TitleKeystringnoneDisplay is key-only - convention mastery.<trackId>.title. Raw display literals are not read.
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.
RequiresobjectnoneTrack-level unlock gate, written as the one shared gate block (skill levels, quests, other tracks' nodes, and so on).
Nodesmap{}The purchasable nodes, keyed by node id, in display order. A node id is what a player's purchases are saved under, so renaming one orphans everybody who bought it.
* required

Node fields

Each entry under a track's Nodes map:

FieldTypeDefaultDescription
Tierint0Visual tier row. 9 is the shipped convention for an "Eternal" repeatable capstone.
TitleKey / DescriptionKeystringnoneKey-only, same as the track - convention mastery.<nodeId>.title. Without a DescriptionKey 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.
RequiresobjectnoneExternal gate (a skill level, another track's node, a completed quest), written as the one shared gate block.
CostobjectfreeThe shared price group: Currencies (a {currencyId: amount} map), Items ([{Item, Count}]), and Combine (All charges every component, the default; Any charges exactly one route).
StatSacrificeobjectnoneA permanent max-stat drain paid BESIDE the price: { "Stat": "HEALTH"|"STAMINA"|"MANA", "Mode": "permanent_max", "Amount": N }. Single-purchase nodes only; see the callout below.
Modifiersmodifier[][]The effect grammar, see the shapes below.
MaxPurchasesintnull (single-purchase)-1 = infinite ("Eternal"); >1 = capped repeat count. A repeatable node takes FLAT and PERCENT modifiers only.
CostScalingobjectnull (static cost)Only meaningful with MaxPurchases != 1: { Curve, Multiplier, SoftCap }. Curve is EXPONENTIAL (base * multiplier^count, default) or POLYNOMIAL (base * (count+1)^multiplier). SoftCap (default 0 = none) freezes scaling past that purchase count.
RefundPercentintinherits track/global
* required

Track and node gates

A track's or a node's Requires is the same gate block a quest, a shop entry, a board band or an NPC placement uses, so one spelling covers every kind of condition and a mastery gate can read anything the rest of the server can.

  • A skill level is a bound on that skill's level reading: factor hytale:stat, param MMO_Level_<SKILL>.
  • Another mastery node is factor mmoskilltree:mastery_node, param "<track>:<node>", minimum 1.
  • AnyOf opens the node behind either of two earlier picks; AllOf and Not compose the rest.
json
"Requires": {
  "Factors": [ { "Factor": "hytale:stat", "Param": "MMO_Level_MINING", "Min": 40 } ],
  "AnyOf": [
    { "Factors": [ { "Factor": "mmoskilltree:mastery_node",
                     "Param": "fireball_mastery:fb_t2_radius", "Min": 1 } ] },
    { "Factors": [ { "Factor": "mmoskilltree:mastery_node",
                     "Param": "fireball_mastery:fb_t2_pierce", "Min": 1 } ] } ]
}

A locked track or node lists every requirement the player has not met, not just the first one, so a well-authored gate reads as a shopping list in game. The mastery audit domain (/mmoconfig validate) checks every ability id, skill id, paramKey and cross-node gate reference a track makes, so a dangling one is reported instead of silently inert.

Node StatSacrifice

A mastery-only cost component paid in a stat, sitting on the node BESIDE the Cost group: { "Stat": "HEALTH"|"STAMINA"|"MANA", "Mode": "permanent_max", "Amount": N }.

Forbidden on a repeatable node

A StatSacrifice on a repeatable node (MaxPurchases != 1) is invalid. The load check drops the whole track, 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 is the shared price group every other priced thing on the server speaks: Currencies, Items as [{Item, Count}], and Combine where Any charges exactly one route. Omitting Cost entirely defaults a node to free.

Respec refunds a percentage of everything spent on a track, walking node RefundPercent to track RefundPercent to 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 entries are the same grammar an ability improvement uses (the grantable Mmo_Ability_Mod reward) - see Authoring Abilities for the shapes and the paramKey vocabulary for the 17 names a modifier's ParamKey has to resolve against, plus how a pack declares one of its own.

FieldTypeDefaultDescription
ShapeenumFLATFLAT / PERCENT / SET / TRIGGER / ADD_STEP, below. On a repeatable node (MaxPurchases != 1) only FLAT/PERCENT are allowed, and a Condition is not - N purchases would splice N copies; a violation drops the whole track at load.
ParamKey*string-The parameter the value lands on. Four never touch an ability at all: lootMultiplier (PERCENT, luck-roll bonus, needs TargetSkill) and schoolResist (PERCENT only, needs School - see School-scoped modifiers), plus lifesteal and comboFinisherBonus, which are summed at the weapon swing. Every other key lands on an ability leaf that declares it.
ValuenumberoptionalRequired for FLAT/PERCENT/SET. FLAT: base + value. PERCENT: base * (1 + value) (e.g. 0.05 = +5%). SET: 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.
SchoolstringnoneOne of the nine damage schools. Legal only on the damage and schoolResist keys, and mutually exclusive with the other scope fields - a school-scoped value pays out on every hit of that school, whatever produced it. See School-scoped modifiers.
ConditionobjectnoneGates any modifier on runtime state (single-purchase nodes only): nested SelfHp/TargetHp groups (each {Min, Max} as 0-1 fractions) plus HostileOnly/OnCritOnly/Filter.
* required

School-scoped modifiers

A damage or schoolResist value can be narrowed to one of the nine damage schools instead of a skill or an ability, so a node reads "+5% Fire damage" or "+4% Void resistance" rather than naming any particular attack. damage mirrors onto the attacker-side MMO_Bonus_<School>/MMO_BonusFlat_<School> pair (FLAT or PERCENT); schoolResist mirrors onto the victim-side MMO_SchoolResist_<School> channel (PERCENT only) - see Gear stats: per-school stats for the same channels gear reaches.

A school bonus is decided by the hit's damage school, not by which skill threw it, so a school track wants "Target": "global": a skill-scoped track only emits for abilities inside its own skill, and the abilities that carry one school are scattered across several skills.

+5% Fire damage on every hit of that school, whatever produced it:

json
{ "Shape": "PERCENT", "ParamKey": "damage", "School": "Fire", "Value": 0.05 }

+4% Void resistance, worn on the victim side:

json
{ "Shape": "PERCENT", "ParamKey": "schoolResist", "School": "Void", "Value": 0.04 }

A school-scoped modifier is refused at load if it...

  • names a school nobody recognizes - the error lists the nine legal ids
  • carries a Condition (a school value reaches a hit only through its own stat channel, which no per-hit condition can write)
  • combines School with TargetAbility, TargetSkill, or CombatTarget - one value takes one route
  • lands on any key besides damage or schoolResist - no other key has a per-school channel
  • is a school-scoped damage in a shape besides FLAT or PERCENT
  • is schoolResist with no School, or in a shape besides PERCENT
A refused modifier drops the WHOLE track, the same as an illegal StatSacrifice.

The five shapes, one worked example each

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

json
{ "Shape": "FLAT", "ParamKey": "damage", "Value": 5, "TargetAbility": "sword_slash" }

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

json
{ "Shape": "PERCENT", "ParamKey": "lootMultiplier", "Value": 0.03, "TargetSkill": "MINING" }

SET - replaces the target leaf outright, or injects a cast parameter the base ability did not author (single-purchase only):

json
{ "Shape": "SET", "ParamKey": "cooldownMs", "Value": 4000, "TargetAbility": "dash" }

A conditioned modifier is any of the shapes above carrying a Condition (single-purchase only) - "+25% damage to targets under 30% health" is one PERCENT line:

json
{ "Shape": "PERCENT", "ParamKey": "damage", "Value": 0.25,
  "TargetAbility": "sword_slash",
  "Condition": { "TargetHp": { "Max": 0.3 } } }

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": "HitDealt"
}

ADD_STEP - splices an extra piece of chain into the target ability's Body (single-purchase only). InsertAnchor is one of START, END or ON_HIT, matching the anchor nodes the target body authors, and StepPayload is the id of an interaction fragment your pack ships, not an inline block of steps:

json
{
  "Shape": "ADD_STEP",
  "TargetAbility": "sword_slash",
  "InsertAnchor": "ON_HIT",
  "StepPayload": "MyPack_Frag_Bleed_Pulse"
}
TRIGGER and ADD_STEP are rare in shipped mastery content and single-purchase only. A splice naming an anchor the target ability does not have is reported at /mmoconfig validate rather than quietly doing nothing. See Authoring Abilities for the anchors and how to author the fragment it points at.

Reuse: Parent and Abstract

Reuse is native Parent inheritance, the same mechanism quests and abilities use. A file marked "Abstract": true is a shared base that never becomes a track of its own; a child names it with "Parent": "<id>", retunes any leaf, and inherits the rest. Nodes merges per node id, and inside a node every group merges per leaf, so a child can retune one price and keep the whole tree.

An Abstract base and a child that retunes one leaf
// Server/MMOSkillTree/Masteries/Mastery_Base.json
{ "Abstract": true, "RefundPercent": 50 }

// Server/MMOSkillTree/Masteries/Axes_Focus.json
{ "Parent": "mastery_base", "Target": "skill:AXES",
  "Nodes": { "axe_t1": { "Tier": 1, "Cost": { "Currencies": { "mastery_point": 3 } },
                         "Modifiers": [ { "Shape": "PERCENT", "ParamKey": "damage",
                                          "Value": 0.08, "TargetSkill": "AXES" } ] } } }

Generators: one file, many tracks

Server/MMOSkillTree/MasteryGenerators/<Id>.json writes a FAMILY of tracks - "the same track, once per skill" - exactly the way quest generators do. Substitution is textual and applies everywhere in Child: every string value, every object KEY (which is how each member gets its own node ids), and IdPattern. A value that is exactly one token keeps that token's type, so "Count": "{count}" lands as a number.

Server/MMOSkillTree/MasteryGenerators/Combat_Masteries.json
{
  "Base": "mastery_base",
  "IdPattern": "{skillLower}_mastery",
  "ForEach": [ { "Values": [
      { "skill": "SWORDS", "skillLower": "swords", "prefix": "swo" },
      { "skill": "AXES",   "skillLower": "axes",   "prefix": "axe" } ] } ],
  "Child": {
    "Target": "skill:{skill}",
    "Nodes": { "{prefix}_t1": { "Tier": 1,
                                "Modifiers": [ { "Shape": "PERCENT", "ParamKey": "damage",
                                                 "Value": 0.08, "TargetSkill": "{skill}" } ] } }
  }
}

An axis may name a Source some mod enumerates ("mmoskilltree:skills", with an optional "Filter": {"Category": "COMBAT"}) instead of listing Values, so a family grows with the skill roster. Each combination becomes an ordinary child of Base - the same decode a hand-written child gets - and an authored Masteries/ file of the same id always wins, which is how one member of a family is made special.

The owner overlay: mastery.json

A server owner retunes a shipped track from mods/mmoskilltree/mastery.json (schema v3): each entry under tracks is a partial body in this same schema, merged per leaf over the loaded track of that id, so one price can change and everything else stays the pack's.

mods/mmoskilltree/mastery.json
{
  "schemaVersion": 3,
  "tracks": {
    "swords_mastery": {
      "Nodes": { "swo_t1_dmg": { "Cost": { "Currencies": { "mastery_point": 5 } } } }
    },
    "parked_track": { "Enabled": false }
  }
}

Upgrading an older mastery.json

A pre-1.6.0 mastery.json (the lower-case target/nodes format, or the retired extends/params DSL) is not read: the server moves it to a timestamped backup/ copy on first start, with one notice. The Migration Converter rewrites an old file into this v3 overlay for you.

Minimal example

Server/MMOSkillTree/Masteries/Simple_Mastery.json
{
  "Target": "skill:MINING",
  "TitleKey": "mastery.simple_mastery.title",
  "Nodes": {
    "smp_t1": {
      "Tier": 1,
      "Cost": { "Currencies": { "mastery_point": 2 } },
      "Modifiers": [
        { "Shape": "PERCENT", "ParamKey": "lootMultiplier", "Value": 0.03, "TargetSkill": "MINING" }
      ]
    }
  }
}

Exhaustive example

A parked two-node track exercising most leaves, an Eternal capstone included:

Server/MMOSkillTree/Masteries/Acrobat_Focus.json
{
  "Enabled": false,
  "Target": "skill:ACROBATICS",
  "RefundPercent": 50,
  "Requires": {
    "Factors": [ { "Factor": "hytale:stat", "Param": "MMO_Level_ACROBATICS", "Min": 5 } ]
  },
  "Nodes": {
    "acr_t1": {
      "Tier": 1,
      "Cost": {
        "Currencies": { "mastery_point": 2, "life_essence": 500 },
        "Items": [ { "Item": "Ingredient_Lightning_Essence", "Count": 1 } ]
      },
      "Modifiers": [
        { "Shape": "PERCENT", "ParamKey": "lootMultiplier", "Value": 0.03, "TargetSkill": "ACROBATICS" }
      ]
    },
    "acr_eternal": {
      "Tier": 9,
      "Prerequisites": ["acr_t1"],
      "MaxPurchases": -1,
      "CostScaling": { "Curve": "EXPONENTIAL", "Multiplier": 1.1 },
      "Cost": {
        "Currencies": { "life_essence": 200 },
        "Items": [ { "Item": "Ingredient_Lightning_Essence", "Count": 2 } ]
      },
      "Modifiers": [
        { "Shape": "PERCENT", "ParamKey": "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.