Enemy Action and Form Animations - UQcsse3200/2026-studio-3 GitHub Wiki

Enemy Action and Form Animations

Owner: @jianghuichen0918 · Ticket: #438 (Sprint 4) · Code side of forms: #436 (@1109TT) · PRs: #463, #485 · Last updated: 2026-10-19

Test plan: Enemy Action and Form Animations Test Plan

Why this page exists

Until Sprint 4 only the Boss had defend and cast animations. Every other enemy fell back to idle when it raised a shield or applied an effect, so it looked like it was standing still. Three mechanics also had no visual: the Riven Knight's enrage, the Tomb Guardian's taunt, and the Boss holding its silence on the player. This page lists the animations added in #438, how the game plays them, and how to rebuild the art.

What each enemy now has

Enemy (id) New regions Played when
Lesser Shade (lesser_shade) defend it defends (wraps itself in a translucent purple bubble)
Bone Crawler (bone_crawler) defend it defends (rolls up inside its bone plating)
Dark Acolyte (dark_acolyte) defend it defends (raises a golden ward)
Tomb Guardian (tomb_guardian) defend it defends (braces behind its tower shield)
cast it taunts (laughs at the player)
idle_taunt ×2, hurt_taunt taunting form, held while its taunt is on the player
Riven Knight (void_knight) idle_enraged ×2, attack_enraged, hurt_enraged enraged form; the cyan cracks in its armour glow red, and its attack shows a red slash
Boss (boss_knight) idle_silence ×2 silence form, held while its silence is on the player (PR #485)

The Riven Knight only ever attacks, so it has no defend or cast. The Boss already had defend and cast; its silence form adds no new drawing (see Boss silence form).

Region naming convention (shared with #436)

  • Actions: defend, cast (same names as the Boss).
  • Forms: <animation>_<form>, e.g. idle_enraged, attack_enraged, hurt_enraged, idle_taunt, hurt_taunt, idle_silence.
  • Several frames of one animation share a name and use index: 0, 1, … (e.g. idle_taunt ×2).

EnemyAnimationController looks regions up by name. A missing form animation falls back to the normal animation of the same name (#436), and a missing defend/cast falls back to idle, so a misspelt name never crashes; it just silently shows the wrong pose. EnemyAtlasRegionsTest checks the names above so a repack can't break them unnoticed.

How the game plays them

  • EnemyBehaviourComponent fires enemyDefend / enemyCast, and EnemyAnimationController plays defend / cast once, then returns to idle.
  • EnemyFactory registers cast and defend for every enemy (it used to be Boss only). Atlases without them are skipped with a log warning.
  • A form is entered when something triggers enemyFormChanged with the form name ("enraged", "taunt", "silence") and left again with "normal". While a form is active the controller plays <animation>_<form> instead of <animation>.

Timings (EnemyFactory)

Animation Frame time Why
Non-boss defend, cast 0.8 s, one frame a single held pose; long enough to read before returning to idle
Boss defend, cast 0.1 s per frame unchanged multi-frame versions
attack 0.2 s per frame unchanged
attack_enraged 0.5 s, one frame the red slash is a single frame; at 0.2 s it was gone before it could be seen
idle_silence 0.2 s per frame, looping two glowing cast frames alternate to show the Boss is still channelling

Who switches each form

Form Triggered by Owner
enraged EnemyBehaviourComponent, when the Riven Knight's AI decides it is enraged (below half health, or the player left itself undefended) #436
taunt BattleController, while the player carries this Tomb Guardian's taunt #436
silence BattleController, while the player carries the SILENCE status #438 (PR #485)

Boss silence form

Silence is a status on the player, not on the Boss, so the Boss cannot tell by itself when it ends. BattleController therefore listens to the player's statusEffectApplied and statusEffectRemoved events, the same way the taunt form works:

  • Only the SILENCE key matters; other statuses (e.g. Poison) are ignored.
  • When it is applied, every living enemy with the boss behaviour switches to silence.
  • When it is removed (it expires, or is cleansed), the Boss switches back to normal.
  • A Boss that is already defeated is left alone, so its death animation is not replaced.

The art reuses the two brightest frames of the Boss's existing cast animation (cast frames 4 and 5). boss_knight.atlas gains two idle_silence entries that point at those frames (xy: 256, 256 and xy: 512, 256, 256×256); boss_knight.png itself is unchanged. There are no hurt_silence frames, so a hit during silence plays the normal hurt and then returns to the silence loop.

See also Boss Status Effects and Animations for what silence does in combat.

Things other teams rely on

  • Bestiary (Team 5): the bestiary shows the default region. It still points at the first idle frame at xy: 0, 0, and the original five frames are pixel-for-pixel unchanged.
  • Status orbit and attack lunge (Team 7): the orbit is sized from the current frame's visible bounds, so it follows the new poses automatically (e.g. it grows a little around the Lesser Shade bubble while it defends). The lunge only moves the entity and is unaffected.

Sheets

New frames are added after the original five, four per row. Sheets with more than eight frames grow from 2048×1024 to 2048×2048 (tomb_guardian has 10 frames, void_knight 9). All frames are 512×512 pixel art on a 4 px grid, filter: Nearest,Nearest. The Boss sheet (boss_knight.png, 2048×1536, 256×256 frames) is not repacked.

Rebuilding the art

All tools are single-file Java under assets/enemies/tools/ (run with java <Tool>.java, JDK 11+). assets/enemies/README.md has every command and parameter.

  1. Source: the new poses are transparent PNGs in assets/enemies/<enemy>/source/ (defend.png, cast.png, idle_taunt.png).
  2. Place: PlaceFrame.java scales the pose into a 512 frame lined up with idle_0, feet on the ground line. The Tomb Guardian poses are matched by eye-to-feet distance so the body size doesn't jump.
  3. Pixelate: Pixelate.java --palette <idle_0>,<attack_0> reuses the colours of the enemy's existing pixel frames, so tones match and the existing frames are not regenerated. --soft-alpha keeps the translucency of the Lesser Shade bubble and the Dark Acolyte ward.
  4. Recolour (enrage): Recolor.java turns the Riven Knight's cyan glow red in its existing idle_0 and attack_0. No new drawing is needed.
  5. Derive: DerivePixel.java --form taunt|enraged makes the second idle frame and the hurt frame of a form.
  6. Pack: PackEnemy.java collects every <enemy>_<animation>_<index>.png and writes the sheet and .atlas.

The Boss silence form needs none of these steps: only the two idle_silence entries are added to boss_knight.atlas by hand.

Testing

Ten automated tests were added for #438, plus manual checks in a running game. The full cases, expected results and evidence are in the Enemy Action and Form Animations Test Plan.

  • EnemyAtlasRegionsTest (5): every region above exists with the right frame count, including the Boss's idle_silence, and default is still the first idle frame.
  • EnemyFactoryTest (4): non-boss enemies register defend/cast when the atlas has them and skip them cleanly when it doesn't; the Boss registers idle_silence; attack_enraged is held for 0.5 s while attack stays at 0.2 s.
  • BattleControllerTest (1): the Boss enters silence when the player is silenced and leaves it when the silence is removed; other statuses, defeated Bosses and other enemies are unaffected.
  • Manual: in a battle, press F1 and use forceintent <slot> defend|taunt|silence or setenemyhp <slot> <hp> to make an enemy act straight away (see the test plan for each case).
⚠️ **GitHub.com Fallback** ⚠️