Authoring Abilities & Ability Mods
Active abilities are a fully pack-authorable content type as of 1.6.0 - the jar's own 91 defaults are decoded through the same codec a pack file uses
Server/MMOSkillTree/Abilities/<id>.jsonAbilities/Same codec (AbilityAsset.CODEC) decodes the jar defaults, pack files, and the owner overlay at mods/mmoskilltree/ability-overrides.json. Ability Mods live at Server/MMOSkillTree/AbilityMods/<id>.json and have no owner-override layer - see Ability Mods below.
The filename (lowercased at decode) is the ability id, e.g. Arcane_Missiles.json decodes to arcane_missiles. AbilityCatalog folds every decoded instance and both ActiveAbilityService and PassiveAbilityTriggers read it directly for cast dispatch - the asset is the runtime ability, not just descriptive content. A pack file with the same id as a jar default overrides it entirely unless it uses Parent (see below).
Top-level groups
Every group, and every leaf inside every group, is registered appendInherited: a Parent-linked child that overrides only Cooldown.Ms still inherits Cost, Effects, Presentation, and every other leaf from the parent. The one exception is Effects itself - see EffectStep.
Identity
| Field | Type | Default | Description |
|---|---|---|---|
NameKey | string | optional | Display name localization key. |
DescKey | string | optional | Description localization key. |
Icon | string | optional | An item id whose icon renders as the ability icon. |
Passive | bool | false | When true, fires from a combat hook (PassiveAbilityTriggers) instead of being cast on demand - skips Cooldown, Cost, and Casting gates, and the top-level Presentation never plays. |
Cooldown
| Field | Type | Default | Description |
|---|---|---|---|
Ms | long | optional | Milliseconds between casts. |
Cost (Java type CastCost)
| 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. |
Costs | object | optional | The unified content/cost Cost shape ({"currencies":{...},"items":[...]}), embedded for a future currency/item cast cost. Not read by any runtime path today - author it if you like, but nothing charges it yet. |
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. |
HeldExclude | string[] | optional | Skill ids to exclude from held-weapon fallback routing. |
Casting
| Field | Type | Default | Description |
|---|---|---|---|
Requires | object | optional | The unified content/gate Requirements object (features / skills / minCombatLevel / permission). |
MinHealthPercent | double (0.0-1.0) | optional | Caster's health / maxHealth must be >= this. |
MaxHealthPercent | double (0.0-1.0) | optional | Caster's health / maxHealth must be <= this. |
HeldWeaponCategory | string | optional | Caster's held weapon must resolve to this skill id, e.g. "SWORDS". |
Targeting
| Field | Type | Default | Description |
|---|---|---|---|
Mode | string | optional | e.g. SELF / PROJECTILE / RAYCAST - drives step dispatch, not a free string. |
MaxDistance | double | optional | Range. |
Radius | double | optional | AOE radius when applicable. |
Pierce | int | optional | Projectile/beam pierce count. Lives here at the ability level, not on the Projectile/Beam step leaf. |
Presentation
A shared type used at multiple call sites - see Presentation below for the full field reference and both scopes it appears at.
PassiveTrigger
| Field | Type | Default | Description |
|---|---|---|---|
On | string | optional | on_hit_dealt | on_kill | on_finisher | dodge_armed. |
Chance | double | optional | Schema-legal, but zero live users - every shipped passive fires at 100%. |
Filter | string | optional | Restricts which source ability triggers this passive; "any" = no restriction. |
RequiresWeaponSkill | string | optional | Only fires while wielding a weapon resolving to this skill id. |
SelfHpRangeMin | double (0.0-1.0) | optional | Caster HP fraction gate, lower bound. |
SelfHpRangeMax | double (0.0-1.0) | optional | Caster HP fraction gate, upper bound. |
TargetHpRangeMin | double (0.0-1.0) | optional | Target HP fraction gate, lower bound. |
TargetHpRangeMax | double (0.0-1.0) | optional | Target HP fraction gate, upper bound. |
ComboFinisherOnly | bool | optional | Restrict on_hit_dealt to combo-finisher hits only. |
BonusFromAbility | string | optional | When the source ability's id matches, scale a numeric param by BonusMultiplier. |
BonusMultiplier | double | 1.0 | Scale factor (1.0 = no bonus). |
BonusScalesParam | string | damageMultiplier | Which param BonusMultiplier scales. |
{
"Identity": { "NameKey": "ability.arch_passive_clean_kill.name", "Icon": "Weapon_Arrow_Iron", "Passive": true },
"XpRouting": { "Skills": ["$HELD"] },
"Effects": [
{ "Type": "Heal", "Presentation": { "Sound": "SFX_Health_Potion_High_Drink" },
"Heal": { "Amount": 8.0, "IntervalMs": 1000, "DurationMs": 0, "EntityEffect": "MMO_CleanseAura", "ViaLifesteal": false } }
],
"PassiveTrigger": { "On": "on_kill", "Filter": "any", "RequiresWeaponSkill": "ARCHERY" }
}Damage schools and Cause
Every damage-bearing step leaf (Damage, Dot, Zone, Beam, an OnHit sub-step, and NativeChain) carries an optional Cause string naming a DamageCause asset id. Six ids are the mod's damage schools: Fire and Ice are vanilla; Lightning, Water, Arcane, and Void are MMO-authored, each Parent-ing the vanilla Elemental cause. A non-school vanilla cause (Physical, Slashing, Bludgeoning, Poison, ...) is equally valid. An unset or unresolvable Cause falls back to Physical silently at runtime, but AbilityValidator errors an unresolvable one at /mmoconfig validate time.
"Damage": { "Amount": 45, "Cause": "Ice" }SCHOOL_RESIST is owner-config only
SCHOOL_RESIST skill-tree reward type (a skill-tree.json choices entry, combatTarget one of the six school names) targets these same schools, but a pack cannot ship skill-tree nodes at all - resistance to a school is configured by the server owner only.The EffectStep union
Each Effects[] entry is {"Type": "<Kind>", "<Kind>": {...}, "Presentation": {...}?, "SummaryHidden": true?}. Type picks exactly one matching nested group. Damage, Projectile, Dash, Beam, and ArmNextHit each carry their own OnHit: EffectStep[] - a hit-triggered sub-chain, recursive over the same EffectStep type. SummaryHidden: true marks a step whose mechanic is already described by a sibling step (tooltip-only, never changes runtime behavior).
No per-leaf Parent inheritance inside a step
EffectStep and every nested group stay plain append. The whole Effects array is inherited wholesale when a Parent child omits it entirely - it is never per-element merged. Overriding one step means restating the entire array.Amount-type fields are double unless noted; *Ms fields are long; unmarked fields are optional/nullable.
| Field | Type | Default | Description |
|---|---|---|---|
Teleport | step | optional | MaxDistance; Arrive (a Presentation for the destination arrival moment, distinct from the step's own launch-time Presentation). |
Damage | step | optional | Amount, Radius, Cause, OnHit[], Splash (bool - true = victim-centered secondary AOE excluding caster+primary; false/null = caster-centered self-AOE), CanCrit (default true), ArmorPenetration (0-1, reduces MMO defense-stat mitigation only), Scaling. |
Dot | step | optional | Amount (per tick), IntervalMs, DurationMs, Cause, MaxStacks, AllowProcs, EntityEffect, TickSound, StackKey (null disables stacking), AbilityOrigin (bool - true attributes ticks to the casting ability for XP), RayDistance (top-level-only raycast target pick), Scaling. |
ApplyEffect | step | optional | EffectAsset, DurationMs, TargetVictim (must target ctx.victim), TargetCaster (force caster target even with a victim in context). |
Heal | step | optional | Amount, Percent (percent of the healed entity's max HP, never of damage dealt), IntervalMs, DurationMs, PerTick (regen tail), EntityEffect, ViaLifesteal (true = unconditional heal for an on-hit lifesteal child, no full-HP guard/sound; false/null = the top-level self-heal path with a full-HP no-op guard). |
Zone | step | optional | Radius, IntervalMs, DurationMs, DamagePerTick, HealPerTick, DropDelayMs (telegraph window), MaxTargetDistance (default 30.0), AffectAllies, Cause, ImpactParticle/ImpactSound (first tick only), TickParticle/TickSound (every tick), MeteorProjectileAsset/MeteorDropHeight, VolleyArrowProjectileAsset/VolleyArrowsPerTick/VolleyArrowSkyHeight (all cosmetic telegraph only), AtVictim (anchors on ctx.victim instead of a ground raycast), Scaling. |
Projectile | step | optional | Asset (a ProjectileConfig id), Impact (a Presentation for the hit-location moment), OnHit[], RaycastMode (skip the physical entity, raycast instead), MaxDistance (raycast mode only, default 20.0), Count (salvo shots, default 1), IntervalMs (delay between salvo shots). Pierce is deliberately not here - see Targeting.Pierce. |
Dash | step | optional | Distance, IFrameMs, WindupMs, VelocityHorizontal (default 28.0), VelocityVertical (default 0.1), AirVelocityMultiplier (default 0.6), DirectionMode (LOOK default | MOVEMENT), InteractionAsset (a native RootInteraction fired instead of the Java velocity impulse), HitRadius (sweep sphere, default 2.0), TrailEffect, OnHit[] (OnHit[0] = the primary swept-entity Damage). |
ArmNextHit | step | optional | Multiplier, FlatBonus, DurationMs, Slot, OnHit[] (fires when the armed hit lands), CastDamage (a Damage group, cast-time self-centered AOE), CastRay ({Distance, OnHit[]}, cast-time forward raycast run before the arm), OnHitArm (bool - the on-hit-child flavor that arms the caster's next hit). |
Mark | step | optional | DurationMs, BonusMultiplier (a multiplier, not a fraction - 1.25 = +25%; <=1.0 is a no-op), MaxConsumes, RayDistance (default 20.0), DetonateDamage (final-consume burst; 0 disables), EffectAsset, Cause (inherited by the detonate burst), AtVictim (marks ctx.victim directly, no raycast). |
BuffAura | step | optional | Radius, DurationMs, Multiplier, IncludeCaster (default true), EffectAsset, Slot (default "aura", stacks with the "primary" next-hit-buff slot). |
Beam | step | optional | Range, IntervalMs, AmountPerTick, CostPerTick, TotalTicks (default 50), Radius (default 0.6), Cause, MaxMovementMeters (default 4.0; <=0 disables the channel-cancel check), VisualParticle, HitSound, OnHit[] (per-tick chain), Scaling. |
VelocityImpulse | step | optional | Forward, Up. |
AggroSuppress | step | optional | Radius, DurationMs (present = timed suppress window; omitted = one-shot aggro drop). |
Vanish | step | optional | DurationMs. |
Knockback | step | optional | Strength, Up. |
Deployable | step | optional | SpawnerId (resolved against the engine DeployableSpawner catalog), Offset ({X,Y,Z}), RotationMode, SpawnFace. |
Stun | step (OnHit child only) | optional | DurationMs, Stagger (true selects the lighter stagger variant). |
StatDrain | step (OnHit child only) | optional | Stat (uppercase cost-stat id, e.g. MANA), Amount (drained from victim), ReturnPercent (0-1, fraction refunded to caster). |
Taunt | step | optional | Radius (default 12.0), DurationMs (default 5000). Needs a server's aggro integration wired up for a gameplay effect; cosmetic-only cast notify without one. |
NativeChain | step | optional | Interaction (a RootInteraction id; blank = handler-time no-op), InteractionType (engine enum name, e.g. "Ability1", "Dodge"; blank resolves to Ability1), Cause (the damage cause the native chain's damage leaf(s) inherit - see below), SummaryArgs (cosmetic tooltip substitution, no gameplay effect). |
Summon | step | optional | RoleId (NPC role to spawn; blank = no-op), DurationMs, Count (default 1), Offset ({X,Y,Z}). |
ChangeStat | step | optional | Stat (an EntityStat id), Amount (signed delta on the current value), Target (CASTER default | VICTIM). The generic sibling of StatDrain - a plain stat nudge, no cost-stat semantics. |
Worked step examples
A caster-centered AOE fire burst on hit, from the jar-shipped Fireball ability:
{ "Type": "Projectile",
"Projectile": {
"Asset": "MMO_Fireball",
"Impact": { "Sound": "SFX_Staff_Flame_Fireball_Impact", "Particles": "Explosion_Medium" },
"OnHit": [ { "Type": "Damage", "Damage": { "Amount": 40.0, "Radius": 4.0, "Cause": "Fire" } } ],
"RaycastMode": false, "MaxDistance": 20.0, "Count": 1, "IntervalMs": 0
} }A summon step, from the jar-shipped Summon Wolf ability:
{ "Type": "Summon",
"Summon": { "RoleId": "MMO_Summoned_Wolf", "DurationMs": 30000, "Count": 1, "Offset": { "Y": 1.0 } } }Scaling
An optional nested group on Damage/Dot/Zone/Beam's per-hit/per-tick Amount. Snapshot once at cast/apply time, never re-evaluated per tick. Two composable sub-groups fold additively into one factor:
amount *= (1 + statMax * PerStat.PerPoint + skillLevel * PerSkillLevel.PerLevel)| Field | Type | Default | Description |
|---|---|---|---|
PerStat | object { Stat, PerPoint } | optional | Scales by a caster EntityStat's folded max value. |
PerSkillLevel | object { Skill, PerLevel } | optional | Scales by the caster's level in a named skill. A null/blank Skill resolves to the ability's XpRouting first skill (the same way "$HELD" resolves). |
"Damage": { "Amount": 20, "Cause": "Physical",
"Scaling": { "PerSkillLevel": { "Skill": "SWORDS", "PerLevel": 0.02 } } }Presentation (shared type)
The same Presentation shape appears at two scopes: the ability's top-level field (the cast moment, plays once per successful cast, skipped entirely for a passive ability) and each EffectStep's own Presentation (the step's own moment). Teleport.Arrive and Projectile.Impact are typed sub-moments of the same shape.
| Field | Type | Default | Description |
|---|---|---|---|
Sound | string | optional | A SoundEvent id (vanilla or pack-authored). |
Particles | string | optional | A particle-system id. |
Animation | string | optional | Cast-animation cue (ability-level cast moment only). |
AnimationItem | string | optional | Item-animation-set id, the third leg of the animation triple. |
AnimationSlot | string | optional | Hotbar slot the animation plays against. |
Camera | string | optional | A camera-shake effect id. Cast-moment only. |
Feedback | string | optional | A FeedbackService moment id. Cast-moment only. |
Camera/Feedback is schema-legal but never plays - AbilityValidator warns if one is authored. Reserve those two fields for the ability's top-level Presentation."Presentation": { "Sound": "SFX_Staff_Flame_Fireball_Launch", "Particles": "Fire_Charge_Soak1" }Parent inheritance for ability variants
A pack file can set a top-level Parent pointing at any other ability id (jar default, another pack's ability, or the owner overlay). Every top-level group leaf is inherited unless the child restates it - the smallest possible variant overrides one field:
{ "Parent": "fireball", "Cooldown": { "Ms": 5000 }, "Cost": { "Stat": "MANA", "Amount": 20 } }This is confirmed by AbilityAssetCodecTest: a child overriding only Cooldown.Ms still inherits Cost, Effects, Presentation, and every other leaf from the parent. Remember the one exception - Effects is whole-array-only (see EffectStep).
Precedence and reload
- 1
Jar defaults
src/main/resources/Server/MMOSkillTree/Abilities/*.json- 91 files. - 2
Pack content
Server/MMOSkillTree/Abilities/<id>.jsonin any installed pack. A matching id fully replaces the default unless it usesParent. - 3
Owner overlay
mods/mmoskilltree/ability-overrides.jsonfolds last, per-leaf - the same overlay semantics asParent. Shape:{"schemaVersion":1,"abilities":{"<id>":{...PascalCase AbilityAsset body...}}}.
ability-settings.json is the separate override config that gates the whole active-ability system on/off (AbilitySettingsConfig.enabled, the abilities feature-gate id) - 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 disk re-read of ability-overrides.json. It does not force-rescan a brand-new zipped pack - that still needs a server restart.
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.Native-chain damage attribution
A NativeChain step's native re-caused Damage needs its own DamageCause leaf to attribute XP/masteries/rewards through the declaring ability instead of the held-weapon fallback. For jar-shipped abilities this is fully automatic (SchoolCauseGenerator pre-scans the jar defaults plus ability-overrides.json at startup and auto-registers MMO_Cause_<AbilityPascal> per ability).
Pack-authored NativeChain abilities need a manual DamageCause
NativeChain ability does not get the automatic treatment - author the DamageCause asset yourself:{ "Parent": "PhysicalDamageCause", "Inherits": "PhysicalDamageCause" }Then reference it: "NativeChain": { "Interaction": "...", "Cause": "MMO_Cause_My_Native_Ability" }. DamageCause assets decode before Interaction assets in Hytale's own load order, so no special pack ordering is needed.
paramKey resolution for modifiers
A mastery node or skill-tree reward that targets an ability (a FLAT/PERCENT/OVERRIDE/ADD_PARAM modifier keyed by a paramKey like damage or cooldownMs) only takes effect if that key resolves to a real field on the ability's authored step chain. AbilityValidator checks every authored paramKey against the engine's binding table and errors an unresolved one (surfaced by /mmoconfig validate) - an unresolved key would otherwise silently drop the modifier at cast time.
Minimal example
{
"Identity": { "NameKey": "ability.my_spark.name", "DescKey": "ability.my_spark.desc", "Icon": "Ingredient_Lightning_Essence" },
"Cooldown": { "Ms": 6000 },
"Cost": { "Stat": "MANA", "Amount": 15 },
"XpRouting": { "Skills": ["MAGIC"] },
"Effects": [ { "Type": "Damage", "Damage": { "Amount": 18, "Cause": "Lightning" } } ]
}Full example (jar default, real)
{
"Name": "fireball",
"Identity": { "NameKey": "ability.fireball.name", "DescKey": "ability.fireball.desc", "Icon": "Weapon_Staff_Crystal_Flame" },
"Cooldown": { "Ms": 8000 },
"Cost": { "Stat": "MANA", "Amount": 30 },
"XpRouting": { "Skills": ["MAGIC"] },
"Effects": [
{ "Type": "Projectile",
"Projectile": {
"Asset": "MMO_Fireball",
"Impact": { "Sound": "SFX_Staff_Flame_Fireball_Impact", "Particles": "Explosion_Medium" },
"OnHit": [ { "Type": "Damage", "Damage": { "Amount": 40.0, "Radius": 4.0, "Cause": "Fire" } } ],
"RaycastMode": false, "MaxDistance": 20.0, "Count": 1, "IntervalMs": 0
} }
],
"Presentation": { "Sound": "SFX_Staff_Flame_Fireball_Launch", "Particles": "Fire_Charge_Soak1" }
}Ability Mods Since 1.6.0
A grantable, player-facing improvement to an existing ability - a mastery-tree reward, a quest prize, or a shop offer that permanently buffs one specific ability rather than a whole skill.
Server/MMOSkillTree/AbilityMods/<id>.jsonAbilityMods/AbilityModAsset.CODEC. Defaults < pack only - there is no owner-override layer for this type yet, unlike Abilities' three-layer fold.
| Field | Type | Default | Description |
|---|---|---|---|
TargetAbility* | string | - | Must resolve in the ability catalog (validated non-null at decode; lowercased). |
Modifiers | array | optional | The same modifier grammar mastery nodes use - see below. Parsed lazily and cached on first read; a malformed entry is skipped with a warning, not fatal to the whole asset. |
NameKey | string | optional | Display name localization key. |
DescKey | string | optional | Description localization key. |
Name | string | optional | Echo only. |
Modifier grammar (7 shapes)
Shared with mastery-node modifiers (AbilityModifierJson) - see also Masteries for the mastery-node side of the same grammar. shape is required, uppercase, defaults to FLAT if omitted.
FLAT
Adds a flat amount to the target field.
{ "shape": "FLAT", "key": "damage", "value": 4 }PERCENT
Adds a percent multiplier to the target field.
{ "shape": "PERCENT", "key": "damage", "value": 0.15 }OVERRIDE
Replaces the target field outright.
{ "shape": "OVERRIDE", "key": "cooldownMs", "value": 4000 }ADD_PARAM
Adds a brand-new numeric param the base ability did not have.
{ "shape": "ADD_PARAM", "key": "armorPenetration", "value": 0.2 }CONDITIONAL
Wraps another modifier so it only applies when a condition passes.
{ "shape": "CONDITIONAL",
"condition": { "selfHpMax": 0.5, "hostileOnly": true },
"apply": { "shape": "PERCENT", "key": "damage", "value": 0.25 } }condition fields: selfHpMin, selfHpMax, targetHpMin, targetHpMax, hostileOnly, onCritOnly, filter. apply is a nested modifier object of any other shape.
TRIGGER
Fires an event-keyed effect rather than modifying a field.
{ "shape": "TRIGGER", "triggerEvent": "on_kill", "payload": { "healPercent": "0.1" } }ADD_STEP
Splices a whole authored step into the target ability's chain - the same schema as an Effects entry.
{ "shape": "ADD_STEP", "insertAnchor": "ON_HIT", "sourceId": "council_arcane_focus",
"stepPayload": { "Type": "ApplyEffect", "ApplyEffect": { "EffectAsset": "MMO_Slam_Slow", "DurationMs": 2000 } } }| Field | Type | Default | Description |
|---|---|---|---|
insertAnchor | string | optional | START | END | ON_HIT. START/END prepend/append the top-level Effects[]; ON_HIT attaches into the target's first on-hit-attachable step (Projectile, Dash, or a non-splash Damage). |
sourceId | string | optional | Author dedupe key. |
stepPayload | object | optional | A whole EffectStep body, decoded through EffectStep.CODEC directly. |
ON_HIT is rejected, not silently no-op'd, on an incompatible target
Zone-only, Dot-only, splash-Damage-only, or native-chain-only target ability has no OnHit array to attach into. An ON_HIT anchor against one of those is rejected at content validation, so a bad ADD_STEP mod surfaces at /mmoconfig validate instead of silently doing nothing in-game.Every shape also accepts optional scoping fields: targetAbility, targetSkill, combatTarget.
Granting
Grant through the unified Reward model's ABILITY_MOD kind:
{ "type": "ABILITY_MOD", "modId": "council_arcane_focus" }from a quest reward, a dialogue Reward action, or a shop entry - the same one-shot pipeline every other reward kind uses, including the offline-retry queue. /mmoability grantmod and /mmoability revokemod (admin) do the same by console command for testing.
Real example
{
"Name": "Council_Arcane_Focus",
"TargetAbility": "arcane_missiles",
"NameKey": "abilitymod.council_arcane_focus.name",
"DescKey": "abilitymod.council_arcane_focus.desc",
"Modifiers": [
{ "shape": "FLAT", "key": "damage", "value": 4 },
{ "shape": "ADD_STEP", "insertAnchor": "ON_HIT", "sourceId": "council_arcane_focus",
"stepPayload": { "Type": "ApplyEffect", "ApplyEffect": { "EffectAsset": "MMO_Slam_Slow", "DurationMs": 2000 } } }
]
}