Skills
A skill is what something does. Files under skills/ nest as deeply as you like and merge into one flat namespace, so an id is just its key.
frost_nova:
cooldown: 6s
skills:
- damage{amount=6} @playersInRadius{r=5}
skillslistrequiredThe mechanic lines, in order.
mechanicsis accepted as an alias.cooldowndurationdefault0Per caster.
8s,200t,2m, the same duration format every mechanic uses.conditionscondition listChecked once, against the caster, before any line runs.
The four primitives
Every line is up to four things. Only the mechanic is required.
| Primitive | Prefix | Answers |
|---|---|---|
| Mechanic | (none) | what happens |
| Targeter | @ | to whom, or where |
| Condition | ? | only if |
| Trigger | ~ | when, in mob files only |
Two forms, one tree
- type: damage
amount: 9
ignore_armor: true
targeter: { type: players_in_radius, radius: 7 }
conditions:
- { type: on_ground }
- damage{amount=9;ignoreArmor=true} @playersInRadius{r=7} ?onGround
These parse to the identical tree through the same parser, so there is one validator and one set of error messages. Mix them freely, even within one list.
Structured YAML is the canonical form. In a structured mechanic map, type, mechanic, targeter, target, conditions, trigger, skills and children are reserved for the line itself; every other key is a parameter of the mechanic.
Keys normalise by lowercasing and stripping underscores, so ignore_armor, ignoreArmor and IgnoreArmor are one key.
Shorthand grammar
line := mechanic targeter? trigger? condition*
mechanic := name args?
targeter := "@" name args? (" of " targeter)?
trigger := "~on" name (":" value)?
condition := "?" "!"? name args?
args := "{" pair (";" pair)* "}"
pair := key "=" value
value := bare-token | quoted-string | args
Args nest, and a nested block builds exactly the map the equivalent YAML would:
- particle{shape={type=ring;radius=6;points=48};p=sweep_attack} @selfLocation
Targeters chain with of, which reads right to left, so the targeter after of resolves first:
- damage{amount=4} @playersInRadius{r=6} of @nearestPlayer{r=20}
Mechanics
140 built in. A mechanic declares its parameters and their aliases, so amount and a can mean different things on different mechanics without the parser knowing anything about either.
That declaration is also why a misspelled parameter is a load-time warning naming the parameters that do exist, rather than a silent default.
Flow mechanics
skill, repeat, random_skill and delay are mechanics like any other, which is what makes a skill a tree rather than a list.
- type: repeat
times: 3
interval: 10
skills:
- damage{amount=2} @target
An inline block needs the structured form, because a shorthand line has nowhere to hang children. Its children can still be shorthand, as above. Where a flow mechanic just calls a named skill, shorthand is fine:
- repeat{times=3;interval=10;s=frost_nova}
None of them schedules anything itself. delay tells the execution to pause and the executor reschedules the remainder at the caster, so a delayed skill on Folia resumes on the correct region thread, and one whose caster died during the pause is dropped rather than resumed against a stale entity.
An inline skills: block becomes a real skill under a synthetic id, so it goes through the same executor path as a named one and /bestiary info can show it.
Targeters
39 built in, producing either entities or locations. Every targeter also accepts three parameters the engine applies rather than the targeter, so a third-party targeter gets them free and cannot forget the max_targets cap:
limitnumberdefault0Maximum targets after sorting.
0is no limit of its own.sortstringdefaultnonenearest,farthest,random,threat,lowest_healthorhighest_health.filtercondition listA condition every target must pass.
A targeter that produces locations cannot feed a mechanic that needs an entity. That mismatch is a load-time error rather than a runtime surprise, because finding out at runtime means a boss that quietly targets nothing.
Conditions
52 built in. Numeric conditions take an optional comparator prefix (<=, >=, <, >, =, !=) followed by an expression. A bare number means =.
- { type: health_percent, amount: "<= 50" }
Prefix a condition with ! to negate it: ?!onGround.
Triggers
25 built in. Triggers answer when, and are read only in a mob file's skills: list: a skill file defines what happens, a mob decides when.
skills:
- skill{s=example_shockwave} ~onTimer:160 ?phase{is=ground}
- skill{s=example_enrage} ~onHealthThreshold:25
| Trigger | Takes a value | Fires when |
|---|---|---|
~onSpawn | the mob spawns | |
~onFirstSpawn | the first time this mob ever spawns | |
~onTimer | ticks | every n ticks |
~onTick | every polled tick | |
~onDamaged | it takes damage | |
~onDamagedByPlayer | a player damages it | |
~onAttack | it attacks | |
~onKill | it kills anything | |
~onKillPlayer | it kills a player | |
~onDeath | it dies | |
~onInteract | a player right-clicks it | |
~onPlayerNear | radius | a player comes within range |
~onPlayerLeave | radius | a player leaves range |
~onCombatEnter | it enters combat | |
~onCombatExit | it leaves combat | |
~onSignal | name | another skill or the API signals it |
~onPhase | phase | it enters a phase |
~onHealthThreshold | percent | health first falls past a percentage |
~onSummon | it is summoned by another mob | |
~onTeleport | it teleports | |
~onProjectileHit | one of its projectiles hits | |
~onBlockBreak | it breaks a block | |
~onDespawn | it despawns | |
~onEnterArena | it enters its anchor's arena | |
~onLeaveArena | it leaves its anchor's arena |
A worked example
example_shockwave:
cooldown: 8s
skills:
- type: sound
sound: entity.generic.explode
targeter: { type: self }
- type: particle
shape: { type: ring, radius: 6, points: 48 }
particle: sweep_attack
targeter: { type: self_location }
- type: damage
amount: 9
ignore_armor: true
targeter:
type: players_in_radius
radius: 7
conditions:
- { type: on_ground }
- type: velocity
mode: away_from_origin
strength: 0.8
vertical: 0.35
targeter: { type: players_in_radius, radius: 7 }
The same thing, in shorthand:
example_shockwave:
cooldown: 8s
skills:
- sound{s=entity.generic.explode} @self
- particle{shape={type=ring;radius=6;points=48};p=sweep_attack} @selfLocation
- damage{amount=9;ignoreArmor=true} @playersInRadius{r=7} ?onGround
- velocity{mode=awayFromOrigin;strength=0.8;vertical=0.35} @playersInRadius{r=7}
Testing one
/bestiary cast example_shockwave
Runs the skill from you with a live trace of every targeter resolution and condition result, which is usually enough to see which node is at fault. /bestiary debug <mob> attaches the same trace to a running mob.
The full mechanic, targeter and condition references are generated from the metadata used by the loader, including aliases, defaults and target kinds. When shorthand is rejected, use the accepted parameters printed in the load warning instead of assuming an alias from another mob engine.