Enemy Action and Form Animations - UQcsse3200/2026-studio-3 GitHub Wiki
Owner:
@jianghuichen0918· Ticket: #438 (Sprint 4) · Code side of forms: #436 (@1109TT) · PRs: #463, #485 · Last updated: 2026-10-19Test plan: Enemy Action and Form Animations Test Plan
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.
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).
- 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.
-
EnemyBehaviourComponentfiresenemyDefend/enemyCast, andEnemyAnimationControllerplaysdefend/castonce, then returns toidle. -
EnemyFactoryregisterscastanddefendfor every enemy (it used to be Boss only). Atlases without them are skipped with a log warning. - A form is entered when something triggers
enemyFormChangedwith the form name ("enraged","taunt","silence") and left again with"normal". While a form is active the controller plays<animation>_<form>instead of<animation>.
| 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 |
| 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) |
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
SILENCEkey matters; other statuses (e.g. Poison) are ignored. - When it is applied, every living enemy with the
bossbehaviour switches tosilence. - 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.
-
Bestiary (Team 5): the bestiary shows the
defaultregion. It still points at the firstidleframe atxy: 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.
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.
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.
-
Source: the new poses are transparent PNGs in
assets/enemies/<enemy>/source/(defend.png,cast.png,idle_taunt.png). -
Place:
PlaceFrame.javascales the pose into a 512 frame lined up withidle_0, feet on the ground line. The Tomb Guardian poses are matched by eye-to-feet distance so the body size doesn't jump. -
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-alphakeeps the translucency of the Lesser Shade bubble and the Dark Acolyte ward. -
Recolour (enrage):
Recolor.javaturns the Riven Knight's cyan glow red in its existingidle_0andattack_0. No new drawing is needed. -
Derive:
DerivePixel.java --form taunt|enragedmakes the second idle frame and the hurt frame of a form. -
Pack:
PackEnemy.javacollects every<enemy>_<animation>_<index>.pngand 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.
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'sidle_silence, anddefaultis still the firstidleframe. -
EnemyFactoryTest(4): non-boss enemies registerdefend/castwhen the atlas has them and skip them cleanly when it doesn't; the Boss registersidle_silence;attack_enragedis held for 0.5 s whileattackstays at 0.2 s. -
BattleControllerTest(1): the Boss enterssilencewhen 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
F1and useforceintent <slot> defend|taunt|silenceorsetenemyhp <slot> <hp>to make an enemy act straight away (see the test plan for each case).