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}
skillslistrequired

The mechanic lines, in order. mechanics is accepted as an alias.

cooldowndurationdefault 0

Per caster. 8s, 200t, 2m, the same duration format every mechanic uses.

conditionscondition list

Checked once, against the caster, before any line runs.

The four primitives

Every line is up to four things. Only the mechanic is required.

PrimitivePrefixAnswers
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:

limitnumberdefault 0

Maximum targets after sorting. 0 is no limit of its own.

sortstringdefault none

nearest, farthest, random, threat, lowest_health or highest_health.

filtercondition list

A 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
TriggerTakes a valueFires when
~onSpawnthe mob spawns
~onFirstSpawnthe first time this mob ever spawns
~onTimerticksevery n ticks
~onTickevery polled tick
~onDamagedit takes damage
~onDamagedByPlayera player damages it
~onAttackit attacks
~onKillit kills anything
~onKillPlayerit kills a player
~onDeathit dies
~onInteracta player right-clicks it
~onPlayerNearradiusa player comes within range
~onPlayerLeaveradiusa player leaves range
~onCombatEnterit enters combat
~onCombatExitit leaves combat
~onSignalnameanother skill or the API signals it
~onPhasephaseit enters a phase
~onHealthThresholdpercenthealth first falls past a percentage
~onSummonit is summoned by another mob
~onTeleportit teleports
~onProjectileHitone of its projectiles hits
~onBlockBreakit breaks a block
~onDespawnit despawns
~onEnterArenait enters its anchor's arena
~onLeaveArenait 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.