Authoring Abilities & Ability Mods
Active abilities are a fully pack-authorable content type as of 1.6.0 - the jar's own defaults are read through the same schema a pack file uses
Server/MMOSkillTree/Abilities/<Skill>/<Name>.jsonAbilities/One file per ability. The same schema reads the jar defaults, pack files, and the owner overlay at mods/mmoskilltree/ability-overrides.json. Ability improvements live at Server/MMOSkillTree/AbilityMods/<id>.json and have no owner-override layer - see Ability Mods below.
The filename is the ability id, lowercased when it is read: Arcane_Missiles.json becomes arcane_missiles. Subfolders are organizational and change nothing. The jar files its own defaults one folder per skill (Abilities/Magic/Fireball.json) purely so the roster reads well, so your pack file can sit in any folder, or none at all, and still override a jar default of the same name. Keep filenames unique across the whole folder tree: two files sharing a basename are two files claiming one id.
The asset is the runtime ability, not descriptive content beside it: what you author here is exactly what fires when a player presses the key. A pack file with the same id as a jar default replaces it entirely unless it uses Parent (see below).
The eight top-level groups
An ability file carries eight groups plus nothing else. There is no Targeting group and no separate list of effect steps: an ability's targeting and its whole effect sequence both live inside Body, composed the same way a weapon or a consumable's own interaction chain is composed.
| Field | Type | Default | Description |
|---|---|---|---|
Identity | group | optional | Display name key, description key, and icon. |
Cooldown | group | optional | Milliseconds between casts. |
Cost | group | optional | The resource bar a successful cast charges. |
XpRouting | group | optional | Which skills the cast credits, and any held-weapon exclusions. |
Casting | group | optional | What the caster must clear, plus the client-clock declaration. |
Presentation | group | optional | The cast moment: sound, particles, animation, camera, feedback. |
PassiveTrigger | group | optional | Which combat event fires the ability, and the filters that narrow it. Authoring this group is what makes an ability passive. |
Body | group | optional | The whole cast chain. Everything the ability actually does. |
Every group, and every leaf inside every group, inherits independently: a Parent-linked child that overrides only Cooldown.Ms still inherits Cost, Body, Presentation, and every other leaf from its parent.
Identity
| Field | Type | Default | Description |
|---|---|---|---|
TitleKey | string | optional | Display name localization key. |
DescriptionKey | string | optional | Description localization key. |
Icon | string | optional | An item id whose icon renders as the ability icon. |
An ability is passive because it carries a PassiveTrigger group
PassiveTrigger group fires from the combat hook that group names instead of being cast on demand: it skips Cooldown, Cost and the Casting gates, and its top-level Presentation never plays. An ability without one is castable. The old Identity.Passive boolean still decodes so an older pack file loads, but it decides nothing and /mmoconfig validate warns about it: delete the leaf and let the group speak.Cooldown
| Field | Type | Default | Description |
|---|---|---|---|
Ms | long | optional | Milliseconds between casts. |
Cost
| Field | Type | Default | Description |
|---|---|---|---|
Stat | string | optional | Resource-bar stat id charged on a successful cast, e.g. MANA, STAMINA. |
Amount | int | optional | Amount charged. |
XpRouting
| Field | Type | Default | Description |
|---|---|---|---|
Skills | string[] | optional | Skill ids this cast's XP and damage credit. Also accepts the "$HELD" token, which resolves to the held weapon's skill at cast time. |
ExcludeSkills | string[] | optional | Skill ids to exclude from held-weapon fallback routing. |
Casting
| Field | Type | Default | Description |
|---|---|---|---|
Requires | object | optional | The one shared requirements block every quest, shop entry and board uses: factor bounds, a permission, prerequisites, and the AllOf / AnyOf / Not combinators. |
SelfHp | object { Min, Max } | optional | Caster health window, as 0.0-1.0 fractions: the cast is refused unless the caster's health fraction sits inside it. Author either bound alone, e.g. { "Max": 0.5 } for an execute-yourself-at-half-health ability. |
HeldWeaponCategories | string[] | optional | The held weapon must resolve to any of these skill ids, e.g. ["BLUNT", "AXES"]. Array only: a single-category ability writes a one-element array. Omitted or empty means no weapon gate. |
ClientClock | bool | false | Declares a body the caster's own client has to run - see Client clock. |
Presentation
A shared shape used at more than one place - see Presentation below for the full field list and both scopes it appears at.
PassiveTrigger
| Field | Type | Default | Description |
|---|---|---|---|
On | string | optional | HitDealt | Kill | Finisher | DodgeArmed. |
Chance | double | optional | Legal to author, but no shipped passive uses it - every one fires at 100%. |
Filter | string | optional | Restricts which source ability triggers this passive; "any" = no restriction. |
WeaponSkills | string[] | optional | Only fires while wielding a weapon resolving to ANY of these skill ids; omitted or empty means no weapon gate. |
SelfHp | object { Min, Max } | optional | Caster health window, as 0.0-1.0 fractions. The same shape Casting.SelfHp uses. |
TargetHp | object { Min, Max } | optional | Victim health window, as 0.0-1.0 fractions: { "Max": 0.2 } is an execute that only fires on a target below one fifth health. |
ComboFinisherOnly | bool | optional | Restrict HitDealt to combo-finisher hits only. |
Bonus | object { FromAbility, Multiplier, ScalesParam } | optional | Source-ability bonus scaling: when the hit came from the ability named in FromAbility, the param named in ScalesParam (default armMultiplier) is scaled by Multiplier (default 1.0, no bonus). |
The two health windows and the bonus group are nested objects, not flat prefixed leaves:
"PassiveTrigger": {
"On": "HitDealt",
"WeaponSkills": ["SWORDS"],
"TargetHp": { "Max": 0.35 },
"Bonus": { "FromAbility": "whirlwind", "Multiplier": 1.5, "ScalesParam": "armMultiplier" }
}Body: the ability's cast chain
Body is an ordinary Hytale root interaction. The mod's own node types compose freely with native Hytale nodes inside one chain, exactly the surface a weapon or a consumable's interaction chain uses, not a separate ability-only schema. A whole ability lives in one file:
"Body": {
"Interactions": [
{ "Type": "MmoModSteps", "Anchor": "Start",
"Next": { "...": "the ability's real first node" } }
],
"RequireNewClick": false
}RequireNewClick: false is what every shipped ability authors: the chain runs on the cast it was fired by rather than waiting for a fresh click.
Body also accepts a bare string naming a real chain file under Server/Item/RootInteractions/. That form is reserved for a genuinely shared fragment rather than a whole ability body.
Node types
Every node's "Type" is either a native Hytale type (Simple, ApplyForce, ApplyEffect, RunRootInteraction, a native damage leaf, and so on) or one of the mod's own. Any of them also accepts SummaryHidden: true, which hides that node from the ability's rendered description without changing what it does.
| Field | Type | Default | Description |
|---|---|---|---|
MmoCastAbility | node | optional | Fires a real, fully gated ability cast from anywhere: a weapon proc, an NPC, a trap, a scroll, a block. See Casting from a consumable or a block. |
MmoRequireGate | node | optional | Stops the chain unless the shared requirements block passes. |
MmoGrantReward | node | optional | Grants a reward through the shared reward model, with an optional chance. |
MmoSelect | node | optional | Picks targets: a ray, a sphere, a cone, or the caster alone, with filters for allies, players and hostiles. Each target runs a fork chain; NoHit covers a total miss. |
MmoModSteps | node | optional | An anchor a granted improvement can splice into. See Anchors. |
MmoDamage | node | optional | Damage, with optional splash radius, damage cause, crit and armor-penetration knobs, weapon-derived damage, stat and skill scaling, and knockback. |
MmoDot | node | optional | Damage over time: per-tick amount, interval, duration, stacking key and cap, plus the status effect it applies. |
MmoZone | node | optional | A ground zone that pulses damage or healing, with an optional falling-meteor or arrow-rain telegraph. |
MmoBeam | node | optional | A channeled beam: per-tick amount and cost, tick count, a movement cancel distance, and a repeating visual chain. |
MmoProjectile | node | optional | Fires a projectile (or an instant raycast shot), with salvo count and interval, pierce, and an on-hit chain. |
MmoHeal | node | optional | Heals, flat or as a percentage, instantly or over time. |
MmoDash | node | optional | A momentum dash with invulnerability frames, windup, optional movement-steered direction, a sweep radius and an on-hit chain. |
MmoTeleport | node | optional | Blinks the caster forward up to a maximum distance, with its own arrival moment. |
MmoStun | node | optional | Stuns or staggers, with diminishing-returns knobs. |
MmoMark | node | optional | Marks a target so hits on it hit harder, with a detonation on the last consume. |
MmoArmNextHit | node | optional | Arms the caster's next hit with a multiplier (armMultiplier) and an optional flat bonus (armFlatBonus) in one of four slots. Momentum: true selects the pure momentum arm, the dash or charge flavor: multiplier, duration and slot only. Left absent or false, the fuller next-hit buff runs instead, with its cast-time raycast, its flat bonus, its armed per-hit chain and its optional cast-time self-AOE. |
MmoBuffAura | node | optional | A radius buff around the caster for a duration. |
MmoVanish | node | optional | Hides the caster for a duration. |
MmoTaunt | node | optional | Pulls nearby hostile attention onto the caster. |
MmoAggroSuppress | node | optional | Drops nearby hostile attention, one shot or for a window. |
MmoHookshotPull | node | optional | Reels the caster toward a hit point at a capped speed. |
MmoStatDrain | node | optional | Drains a resource stat from the victim and optionally returns part of it. |
MmoChangeStat | node | optional | Nudges a stat on the caster or the target, optionally for a duration. |
MmoSummon | node | optional | Summons temporary allies from an NPC role. |
MmoDeployable | node | optional | Plants a deployable construct such as a turret or a totem. |
MmoApplyEffect | node | optional | Applies a status effect whose duration scales with gear. A fixed-duration effect uses the plain native node instead. |
Prefer MmoSelect over a native selector for any targeting sweep leading into a number-bearing MMO node: it resolves on the server, which is what lets the same body fire identically from a hotbar slot, a scroll, a weapon proc, an NPC or a trap.
{
"Type": "MmoSelect",
"Sphere": { "Radius": { "Base": 6.0, "ParamKeys": ["radius"] }, "MaxTargets": 8 },
"Filter": { "HostileOnly": true },
"HitEntity": [ { "Interactions": [ { "Type": "MmoDamage",
"Amount": { "Base": 18.0, "ParamKeys": ["damage"] }, "Cause": "Arcane" } ],
"RequireNewClick": false } ],
"NoHit": { "Interactions": [ { "Type": "Simple", "RunTime": 0 } ], "RequireNewClick": false },
"Next": "MMO_Frag_Anchor_End"
}Shared fragments and naming
A node can reference a shared chain fragment by id instead of inlining it. Two ship with the jar, MMO_Frag_Anchor_End and MMO_Frag_Anchor_OnHit, both just anchor nodes. The rule of thumb: pull a fragment out to Server/Item/Interactions/<Name>.json and reference it by id only once three or more bodies would otherwise repeat the same nodes; one or two users stay inlined.
Name anything you author with your own prefix (MyPack_Ability_Frost_Nova) so it can never collide with another pack's fragment. The mod prefixes its own with MMO_ for the same reason.
Anchors: where a granted improvement attaches
A body should carry an anchor node with "Anchor": "Start" as its first node and one with "End" as its last, plus one with "OnHit" at the tail of any per-target or on-hit sub-chain you want to be spliceable. An ability improvement that adds a step inserts its own fragment at one of those three named points when the ability fires.
{ "Type": "MmoModSteps", "Anchor": "Start",
"Next": { "...": "the ability's real first node" } }Leaving the anchors out is not an error by itself (most abilities never receive a splice), but the content check warns about it, and it becomes an error the moment some shipped improvement actually targets that ability at an anchor the body does not have.
Numbers: Base, ParamKeys, Clamp
A number on an MMO node can be written two ways. A plain number is the shorthand for a fixed value nothing modifies:
"Amount": 12.0The object form is what you write the moment the number should be modifiable or bounded:
"Amount": { "Base": 45.0, "ParamKeys": ["damage", "damageMultiplier"], "Clamp": { "Min": 0.0 } }| Field | Type | Default | Description |
|---|---|---|---|
Base | double | 0.0 | The authored number before anything modifies it. |
ParamKeys | string[] | optional | Names the keys a mastery node, a gear improvement or a reward can target to change this number at cast time, applied left to right. A leaf with no ParamKeys is simply not modifiable. |
Clamp | object { Min, Max } | optional | Optional bounds applied after everything else. A project-wide bound for one key is authored separately under Server/MMOSkillTree/ModifierClamps/. |
Never author two keys for the same channel on one leaf
["distance", "range"] when both map to the same modifier source applies that modifier twice. One channel, one key.The paramKey vocabulary
The vocabulary is a fixed list of 17 names, in three groups by where each one folds. A modifier naming anything else is reported by /mmoconfig validate and dropped, because a key nothing resolves would otherwise be silently ignored at cast time.
| Group | Keys | Where it folds |
|---|---|---|
| Ability-level (2) | cooldownMs, cost | Patched once per cast, before the body fires. Always available: no ParamKeys declaration needed. |
| Body params (11) | damage, damageMultiplier, armMultiplier, armFlatBonus, damageRadius, radius, distance, durationMs, iframesDurationMs, pierceCount, onHitHealPercent | Folded at fire time against whichever body leaf declares the key in its own ParamKeys. A key no leaf declares does nothing for that ability. |
| Not ability-scoped (4) | lifesteal, comboFinisherBonus, lootMultiplier, schoolResist | Never touch an ability at all: they are summed at their own seams, the weapon swing and the gathering luck roll. Always available. |
armMultiplier and armFlatBonus are the MmoArmNextHit pair, deliberately kept out of damageMultiplier: a 1.5x arm scale and a +40 damage value must never share a fold bucket, or a FLAT of 2 would mean +2 damage on one and +2.0x on the other. lootMultiplier and schoolResist are PERCENT-only, and each pairs with a scope: TargetSkill for the loot roll, School for the resistance channel.
A pack can add a key of its own
Server/MMOSkillTree/ModifierClamps/<Key>.json asset and that key becomes known: one file both bounds the new key's fold and declares it, so an invented key can never ship unbounded and undeclared at once. The jar ships 19 clamp files: one per canonical key plus two reserved fold ids authoring "IsParamKey": false - HitDamage.json, which bounds the hit-side damage-percent sum, and DefenseReduction.json, which caps the victim's total percent damage reduction from defense. A reserved id is not a modifier key, so a modifier naming hitDamage or defenseReduction is flagged rather than quietly doing nothing. The owner tightens any of them in mods/mmoskilltree/ability-clamps.json.Seven ParamKeys names are also reachable from gear, through the global ability channels: distance, radius, damageRadius, durationMs, iframesDurationMs, pierceCount, and cost. An item's Armor/Weapon/Utility block grants a flat additive bonus onto whichever of those a body declares, no AbilityMod required - see Gear stats: ability channels. distance is the only reachable name for range: the older range alias was dropped, and authoring both would double-apply the same gear bonus.
The trigger and body layering rule
A body may never wait on client-reported data
The Guard Stance ability is the worked example: its body is the server half only (a status effect granting stagger immunity), while the hold-to-block mechanic, which genuinely needs a client-driven hold, lives outside the ability in its own chain, referenced from the weapon item. The consumable recipes below split the same way: the drink or read hold lives in the item's own chain, never inside a body.
Client clock
Some body nodes are run by the caster's client rather than read from it, a movement impulse being the usual case. Those are allowed in a body, and Casting.ClientClock: true declares them, so the cast is sent to the client to run instead of the server waiting for data nobody was asked to send and cancelling the cast when the wait times out.
"Casting": { "ClientClock": true }You usually do not need it: the engine works the same thing out by itself for most chains. It misses exactly two shapes, a node tucked inside a wrapper node and a chain reached only through a reference to another chain, and that is what the declaration is for. Both directions are checked at boot and by /mmoconfig validate: a chain that waits on the client without the declaration is an error, and a declaration on an ability with nothing to wait for is an error too. Declare it, never default it.
Damage schools and Cause
A damage-bearing MMO node (MmoDamage, MmoDot, MmoZone, MmoBeam, MmoMark) carries an optional Cause naming a damage-cause id.9 of those ids are the mod's damage schools. Fire, Ice, Poison and Physical ride vanilla causes (Poison as an overlay that files it under Elemental beside Fire and Ice), while Lightning, Water, Arcane, Void and Life are MMO-authored, mirroring the essence items. Life is the drain, lifesteal and radiant school, an Elemental sub-cause with its own spring-green combat-text colour. Physical is the vanilla cause promoted to a full school: it is every plain weapon hit's school and the fallback for an untyped one, so no hit is ever school-less, and the vanilla sub-causes (Slashing, Bludgeoning, Projectile) fold into it. A vanilla cause outside that taxonomy is equally valid to name. An unset or unresolvable Cause falls back to Physical quietly at runtime, and /mmoconfig validate reports the unresolvable one. See the damage-school roster for the full list.
"Cause": "Ice"A native damage node in a pack needs its own cause asset
Cause field of its own; the mod bridges its own jar abilities by reading a school tag off the node, but that bridge never scans a pack. So either use one of the MMO damage nodes above, where the cause and the credit are automatic, or author your own damage-cause asset and point the native leaf at it.{ "Parent": "PhysicalDamageCause", "Inherits": "PhysicalDamageCause" }Damage-cause assets are read before interaction assets in Hytale's own load order, so referencing a new cause id from your own damage leaf works with no special ordering.
School resistance is owner config only
skill-tree.json.Casting an ability from a consumable or a block
MmoCastAbility is the one node that fires an ability, so any native chain (a scroll, a potion, a weapon proc, an NPC, a trap, a pressure plate) can fire a real, fully gated cast. It names the ability exactly one way: Ability (a direct id), Slot (whatever the caster has bound to that slot), or Trigger (an input trigger).
| Field | Type | Default | Description |
|---|---|---|---|
Gates | object | optional | RequireUnlock, RequireClass, PayCost, CheckConditions. All default on, so a scroll that should work for a player who never opened the tree turns them off one by one. |
Cooldown | object | optional | UseAbilityPool decides whether this cast consults and stamps the ability's own cooldown; LocalMs is this node's own independent rate limit, in milliseconds. |
Modifiers | object | optional | FromPlayer, FromStack (the held item's own improvements), Inline. |
Progression | object | optional | FireObservers, AwardXp, Notify. |
Chain | object | optional | FailOnBlocked fails the chain on any refusal instead of quietly carrying on; GateOnly runs the whole gate ladder as a pure check that fires nothing. |
The two operator kill switches (the server-wide ability toggle and the per-world rules) always apply and cannot be opted out of.
A scroll casts and then consumes: the cast node sets Chain.FailOnBlocked, so a refused cast skips the consume for free and a blocked scroll read never eats the item.
{
"Type": "MmoCastAbility",
"Ability": "fireball",
"Gates": { "RequireUnlock": false, "RequireClass": false, "PayCost": false, "CheckConditions": true },
"Cooldown": { "UseAbilityPool": false },
"Progression": { "AwardXp": false },
"Chain": { "FailOnBlocked": true },
"Next": { "Type": "ModifyInventory", "AdjustHeldItemQuantity": -1 }
}A potion is the other way round, because a drink hold can be interrupted and refunded: run a GateOnly node first, so a doomed cast never starts the hold, put the drink hold next, and hang the real cast (same ability, same gates, without GateOnly) off the consume's success branch. An interrupted drink then refunds cleanly, and a completed drink can never produce a free cast.
Presentation (shared shape)
The same Presentation shape appears at two scopes: the ability's own top-level group (the cast moment, played once per successful cast, skipped entirely for a passive) and the per-node moments on the nodes that have one (MmoZone's Impact and Tick, MmoDot.Tick, MmoBeam.Hit, MmoDash.Travel, MmoProjectile.Impact, MmoTeleport.Arrive).
| Field | Type | Default | Description |
|---|---|---|---|
Sound | string | optional | A sound event id, vanilla or pack-authored. |
Particles | string or object { Id, MaxMs } | optional | The particle system for this moment. A bare string authors Id alone, which is the usual form. MaxMs caps a world-position one-shot spawn's playback in milliseconds; omit it for the shared four-second default. |
Animation | string or object { Name, Item, Slot } | optional | The animation cue (cast moment only). A bare string authors Name alone, the clip resolving against the caster's held-item set. Item names the animation set the clip resolves in instead, and Slot the hotbar slot it plays against. |
Camera | string | optional | A camera-shake id. Cast moment only. |
Feedback | string | optional | A feedback moment id. Cast moment only. |
Effect | string | optional | A cosmetic status effect that dresses the moment, such as a dash trail. A gameplay status effect is not this leaf: that stays on the node that owns the mechanic. |
Particles and Animation are nested groups with a string shorthand, so the common case stays a single word and the fuller form is there when you need it:
"Presentation": { "Sound": "SFX_Staff_Flame_Fireball_Launch", "Particles": "Fire_Charge_Soak1" }
"Presentation": {
"Particles": { "Id": "Fire_Charge_Soak1", "MaxMs": 1500 },
"Animation": { "Name": "CastOverhead", "Item": "Weapon_Staff_Crystal_Flame" }
}MaxMs only reaches a spawn planted at a bare world position, the one flavor nothing owns a lifecycle for. A model-attached emitter or a particle carried by a status effect is bounded by whatever it rides and ignores the leaf.
Each site reads only part of that list, because a zone tick has no animation to play and a dash trail is not a particle. /mmoconfig validate warns about a leaf authored where it is never read, and about a cast-moment group on a passive, which never casts.
Parent inheritance for ability variants
A pack file can set a top-level Parent pointing at any other ability id: a jar default, another pack's ability, or one of your own. Every leaf is inherited unless the child restates it, so the smallest possible variant overrides one field.
{ "Parent": "fireball", "Cooldown": { "Ms": 5000 }, "Cost": { "Stat": "MANA", "Amount": 20 } }Precedence and reload
- 1
Jar defaults
Server/MMOSkillTree/Abilities/<Skill>/<Name>.json- 97 files, filed by skill. - 2
Pack content
The same path in any installed pack, in any folder you like. A matching id fully replaces the default unless it usesParent. - 3
Owner overlay
mods/mmoskilltree/ability-overrides.jsonfolds last, leaf by leaf, with the same overlay behavior asParent. Shape:{"schemaVersion":1,"abilities":{"<id>":{...ability body...}}}.
The clamp assets fold the same three ways, on their own file. A pack ships Server/MMOSkillTree/ModifierClamps/<Key>.json to bound (or declare) a key, and the owner has the last word in mods/mmoskilltree/ability-clamps.json, one entry per paramKey merged leaf by leaf over defaults and packs. That is where a server tightens a runaway stacking cap without touching either the ability or the pack that introduced the key.
An Effects key in the owner overlay is refused at load
Effects key fails to load with a message pointing at Body. Re-author the override as a Body, or, for a small numeric tweak, target the referenced chain file instead of overriding the ability.ability-settings.json is the separate owner file that switches the whole active-ability system on and off. It does not touch individual ability tuning; that is what ability-overrides.json is for.
/mmoability reload (admin) re-folds the catalog from live pack state plus a fresh read of ability-overrides.json. It does not force a rescan of a brand-new zipped pack, which still needs a server restart - see Building the zip for how that zip is made.
abilities.json is retired (1.6.0 hard break)
mods/mmoskilltree/abilities.json is never read again. Any file found there at startup is moved to mods/mmoskilltree/backup/ with a loud warning. Re-author overrides in ability-overrides.json instead.Retired body keys
These flat leaves were removed, not deprecated. A body still carrying one loads cleanly and simply stops playing that moment, so the server names every stale key it finds in a pack file or the owner overlay at boot, one warning per file.
| Removed key | Author instead |
|---|---|
MmoZone.ImpactSound / ImpactParticle | MmoZone.Impact.Sound / Impact.Particles |
MmoZone.TickSound / TickParticle | MmoZone.Tick.Sound / Tick.Particles |
MmoDot.TickSound | MmoDot.Tick.Sound |
MmoBeam.HitSound | MmoBeam.Hit.Sound |
MmoDash.TrailEffect | MmoDash.Travel.Effect |
MmoZone.MeteorProjectileAsset / MeteorDropHeight | MmoZone.Meteor.ProjectileAsset / Meteor.DropHeight |
MmoZone.VolleyArrowProjectileAsset and friends | MmoZone.Volley.ProjectileAsset / Volley.ArrowsPerTick / Volley.SkyHeight |
MmoDash.DirectionMode | MmoDash.Direction.Movement (present = steer by movement), or omit Direction for look direction |
Minimal example
The smallest useful pack file is a variant of something that already exists:
{
"Parent": "fireball",
"Cooldown": { "Ms": 5000 }
}Full example (a real jar ability)
{
"Identity": {
"TitleKey": "ability.sentry_turret.name",
"DescriptionKey": "ability.sentry_turret.desc",
"Icon": "Weapon_Deployable_Turret"
},
"Cooldown": { "Ms": 30000 },
"Cost": { "Stat": "STAMINA", "Amount": 20 },
"XpRouting": { "Skills": ["ARCHERY"] },
"Casting": { "HeldWeaponCategories": ["ARCHERY"] },
"Presentation": {
"Sound": "SFX_Bow_T2_Shoot",
"Particles": "Bow_Signature_Launch"
},
"Body": {
"Interactions": [
{
"Type": "MmoDeployable",
"SpawnerId": "MMO_Sentry_Turret",
"SpawnFace": "UP",
"Turret": true
}
],
"RequireNewClick": false
}
}Ability Mods Since 1.6.0
A grantable, player-facing improvement to an ability the player already has: a mastery reward, a quest prize, a shop offer, or something a piece of gear grants while it is equipped.
Server/MMOSkillTree/AbilityMods/<id>.jsonAbilityMods/The filename is the id. Defaults and packs only: there is no owner-override layer for this type yet, unlike an ability's three-layer fold.
| Field | Type | Default | Description |
|---|---|---|---|
TargetAbility | string | optional | One ability id this improvement applies to. |
TargetSkill | string | optional | Or, instead, every ability that routes XP to this skill. Author one of the two: an asset with neither is refused. |
TitleKey | string | optional | Display name localization key. |
DescriptionKey | string | optional | Description localization key. |
Display | object { ValueFrom, Format } | optional | Optional tooltip hint: which number to show, and whether to show it as a percent or a flat value. |
Modifiers | array | optional | The same modifier grammar mastery nodes use - see below. A malformed entry is skipped with a warning rather than failing the whole file. |
Modifier grammar (5 shapes plus a Condition gate)
Shared with mastery-node modifiers - see Masteries for the mastery side of the same grammar. The grammar is PascalCase (Shape / ParamKey / Value); Shape is uppercase and defaults to FLAT when omitted. A field name's casing is forgiven, so paramKey still lands on ParamKey, but each field has exactly one name and there is no Key alias. Two older shape words still parse: OVERRIDE and ADD_PARAM both read as SET, since all three mean replace-the-value.
The CONDITIONAL wrapper and the flat HP bounds are gone
CONDITIONAL wrapper around an inner modifier no longer parses, and neither do the flat lower-case HP bounds a condition used to carry. Write the condition directly on the modifier it gates, with the nested SelfHp / TargetHp groups shown below. Re-author both before installing an older pack against 1.6.0: nothing rewrites them for you.FLAT
Adds a flat amount to the target number.
{ "Shape": "FLAT", "ParamKey": "damage", "Value": 4 }PERCENT
Adds a percent multiplier to the target number.
{ "Shape": "PERCENT", "ParamKey": "damage", "Value": 0.15 }SET
Replaces the target number outright (last-write-wins), or writes a brand-new number the base ability did not author.
{ "Shape": "SET", "ParamKey": "cooldownMs", "Value": 4000 }The Condition gate
Any modifier can carry a Condition so it only applies when the state passes:
{ "Shape": "PERCENT", "ParamKey": "damage", "Value": 0.25,
"Condition": { "SelfHp": { "Max": 0.5 }, "HostileOnly": true } }Condition fields: nested SelfHp / TargetHp groups (each { Min, Max } as 0-1 fractions), HostileOnly, OnCritOnly, Filter.
TRIGGER
Subscribes the player to a passive ability rather than modifying a number. TargetAbility names a passive that already exists in the catalogue, with its own PassiveTrigger group carrying the event, the filters and the HP gates; granting the improvement is what switches that passive on for the player. A TRIGGER modifier carries no ParamKey.
{ "Shape": "TRIGGER", "TargetAbility": "def_passive_punishing_dodge" }ADD_STEP
Splices an authored interaction fragment into the target ability's Body at one of its anchors. StepPayload is the id of a real fragment file you ship under Server/Item/Interactions/, never an inline block of nodes.
{ "Shape": "ADD_STEP", "InsertAnchor": "ON_HIT", "SourceId": "council_arcane_focus",
"StepPayload": "MMO_Frag_ArcaneFocusProc" }| Field | Type | Default | Description |
|---|---|---|---|
InsertAnchor | string | optional | START | END | ON_HIT, matching the Start / End / OnHit anchor nodes in the target body. |
StepPayload | string | optional | The id of the interaction fragment to splice in. |
SourceId | string | optional | A dedupe key, so the same splice granted twice still lands once. Recommended, and not allowed on a repeatable mastery node. |
A missing anchor is an error, not a silent no-op
/mmoconfig validate, as is a missing payload, a missing anchor name, or a target ability that does not exist. Author the three anchor nodes in any ability you expect packs to extend.Every shape also accepts optional scoping fields: TargetAbility, TargetSkill, School, CombatTarget.
Granting an improvement
Four routes, all content, no code:
- A reward. The shared reward model's
Mmo_Ability_Modkind, from a quest, a dialogue option, a mastery node or a shop entry, through the same one-shot pipeline every other reward kind uses, offline retry included. - A command.
/mmoability grantmodand/mmoability revokemod(admin), for testing or a manual grant. - Gear. An
ItemAbilityGrantsfile grants improvements while a matching item is held, worn or in the offhand. - A specific item stack. A stack can carry its own rolled improvements as item metadata, which is how enhanced loot works.
{ "Kind": "Mmo_Ability_Mod", "Params": { "Mod": "council_arcane_focus" } }Gear grants
{
"Items": ["Weapon_Staff_Crystal_Flame"],
"Apply": { "WhenHeld": true },
"Grants": ["mmo_gear_ember_focus", "mmo_gear_arcane_efficiency"]
}| Field | Type | Default | Description |
|---|---|---|---|
Items | string[] | optional | Exact item ids to match. |
ItemTags | string[] | optional | Or match by the item's own tag group keys, so your pack can tag its own armor or trinkets to opt in. |
Apply | object | optional | Three independent switches: WhenHeld (default on), WhenWornArmor, WhenOffhand. |
Grants | string[] | optional | The improvements to grant while the item qualifies, by id. |
Per-stack rolled improvements
A single item stack can carry its own improvements in its metadata, each a reference to an existing improvement plus a scale multiplier. The scale multiplies flat and percent modifiers only, never an override, since scaling an override would quietly rewrite authored balance.
{ "MmoAbilityMods": { "V": 1, "Mods": [ { "Mod": "mmo_gear_vortex_amplifier", "Scale": 1.0 } ] } }Two content routes hand a player pre-stamped gear, both taking that same metadata block directly:
{ "ItemId": "Utility_Leather_Quiver", "QuantityMin": 1, "QuantityMax": 1,
"Metadata": { "MmoAbilityMods": { "V": 1, "Mods": [
{ "Mod": "mmo_gear_vortex_amplifier", "Scale": 1.0 } ] } } }{ "Type": "ModifyInventory", "ItemToAdd": { "Id": "Utility_Leather_Quiver", "Quantity": 1,
"Metadata": { "MmoAbilityMods": { "V": 1, "Mods": [
{ "Mod": "mmo_gear_vortex_amplifier", "Scale": 1.0 } ] } } } }Real example
{
"TargetAbility": "arcane_missiles",
"TitleKey": "abilitymod.council_arcane_focus.name",
"DescriptionKey": "abilitymod.council_arcane_focus.desc",
"Display": { "ValueFrom": "damage", "Format": "percent" },
"Modifiers": [
{ "Shape": "PERCENT", "ParamKey": "damage", "Value": 0.1 },
{ "Shape": "ADD_STEP", "InsertAnchor": "ON_HIT", "SourceId": "council_arcane_focus",
"StepPayload": "MMO_Frag_ArcaneFocusProc" }
]
}