Enemies
The key words MUST, MUST NOT, and MAY below are used as in RFC 2119.
Every enemy is one node class — EnemyBase (a CharacterBody3D) — driven by an EnemyData resource. Stats, model, element, and spawn locations live in the data; the AI, animation, status, and SFX logic live in the shared base. New enemies SHOULD be a new EnemyData resource with no new script.
Roster & data
The runtime roster is the set of EnemyData resources under data/enemies/*.tres, loaded at boot by EnemyRegistry and keyed by id. Each entry carries:
| Field | Meaning |
|---|---|
id / name | lookup key and display name |
element | Native / Beast / Machine / Dark — drives damage attribution |
locations | area ids this enemy spawns in |
is_rare / is_boss | roster flags |
model_id / animation_model_id | GLB under assets/enemies/<id>/; rares MAY borrow a base model's animations |
| combat stats | hp_base, defense_base, evasion_base, move_speed, attack_range, detection_range, attack_cooldown, exp_reward, meseta_min/max |
Spawning
The canonical path is the field controller's _spawn_enemy(enemy_id): it resolves the id through EnemyRegistry, instantiates EnemyBase, assigns enemy_data, and adds it to the map. The base then loads its own model, hurtbox, navigation, and target on _ready. Defeated enemies emit died; the field controller (not the enemy) owns drops.
Behavior state machine
The EnemyState enum has exactly six states. Charging is not a state — it's the close-range run sub-mode within CHASING.
| State | Behavior |
|---|---|
| IDLE | Wander (random heading, half speed, edge-aware) or pause. Promotes to CHASING when the target enters detection_range. |
| CHASING | Navigate toward the target. Walk beyond 2× attack range, run/charge inside it. Stuck-avoidance steers perpendicular when displacement stalls. |
| ATTACKING | Stop, face target, play attack, deal damage, start cooldown. Exits to LOAFING when the animation ends. |
| LOAFING | Back off along a curving semicircle for 2.5–4s, then return to CHASING. |
| HURT | 0.3s stagger on any hit; cancels an in-progress attack; returns to CHASING. |
| DEAD | Play death animation, emit died, free after the animation. |
Attack recovery — no wedging (#477)
The ATTACKING → LOAFING transition is keyed on the attack animation ending, and the source models are inconsistent about what that animation is called (_atk, _atk1, misspelled _atckwat, suffix variants like _atk_pu / _atk_sh / _atk_a, or absent entirely — the big b_* rigs are the usual offenders). That naming variance MUST NOT be able to strand the state machine:
The attack-clip resolution order is normative (tool and runtime MUST resolve identically — see /mechanics/enemy-attacks):
- Exact clip name equal to the token.
- Any clip ending in
_<token>. - The alias table (
atk→atk1,atckwat;dmg→dam— five rigs name their damage clipdam: booma, swordman, tank, orangutan, shrimp). - Attack only — last-resort scan: any clip with an exact
atkunderscore-segment (b062_atk_pu,b052_atk_sh,m061_atk_a,b072_atk_sa_a…) qualifies, excluding clips whose final segment isst,lp, ored— those are segmented charge-up/loop/end pieces (m051_atk_sp_lp,b072_atk_gu_a_st) and playing one alone shows a partial attack. Among qualifiers the alphabetically first MUST be picked, so resolution is deterministic per rig. - Nothing resolves → the fixed fallback duration (below). A rig whose only attack clips are segmented pieces (boss_robot's
z003_atk_dl_lp) MUST take this path rather than play a bare segment.
- ATTACKING MUST terminate for every model, whatever its attack animation is named — including models with no resolvable attack animation at all, which MUST recover after a fixed fallback duration instead.
- Attack end MUST be detected against the resolved animation that was actually played (or by observing that it is no longer playing), MUST NOT rely on parsing a name suffix back out of the
animation_finishedsignal. - An enemy MUST NOT require a damage event (the HURT transition) to resume AI or animation. Being hit is a recovery path, never the only one.
- Player contact MUST NOT freeze an enemy: while the player stands against (or overlaps) an enemy of any size, its state machine and animation MUST keep advancing — attack, loaf, and chase cycles continue exactly as at range. Large and small enemies MUST behave identically under contact, differing only in authored stats.
What happens inside ATTACKING — attack selection, the windup → damaging-window timeline, and the arc hit shape — is defined in /mechanics/enemy-attacks.
Archetype: Bruiser (Booma family) — normative
Applies to archetype: bruiser (Booma Origin and Gigobooma Origin). Coliseum validation: stt is the emergence from the dirt, not an attack-ready stance.
- On room reveal, the bruiser MUST play its authored
fsm.spawn_clip: sttonce, in place. The uninterrupted entrance MUST finish before wandering, chasing, or attacking; the hold MUST follow the resolved clip duration and playback speed, not a fixed 0.8 seconds. - After emergence it uses
watfor idle andwlkfor approach. Its attack MUST play stationaryatkpreparation beforeruncharge travel; only charge contact can deal damage. Facing locks before preparation, so sidestepping or dodging can avoid the dash. It MUST NOT dig out again before each attack or after losing/reacquiring the target. - Rendered clip inspection shows
atk_miis a short upward recovery andatk_hia longer airborne tumble. This implementation usesatk_mias non-damaging recovery after the dash; original hit/miss selection is unverified, soatk_hiremains unused. The old “height swings” classification was inferred from names and MUST NOT be used as evidence.
Across archetypes, stt MUST NOT be a universal spawn or pre-attack alias. Emergence is selected through fsm.spawn_clip; absent that field, only explicit spawn/app/appearance clips are spawn candidates. A missing entrance clip uses the bounded spawn-effect hold and idle fallback. Gorillas, flyers, rollers, apes, mimics, and quadrupeds retain their separately defined meanings of stt.
Archetype: Stance riser (Garapython family) — normative
Only archetype: stance_riser uses stt as the pre-strike rise into wat2. It then holds that raised pose for the telegraph beat before the authored strike; neither rise nor hold deals damage. Other archetypes MUST use their own engaged idle during a generic telegraph, without borrowing stt or wat2 merely because those clips exist.
Archetype: Quadruped circler (normative)
Applies to enemies with archetype: quadruped (wolf, hyena, deer, tiger rigs — Reyhound, Grimble, Tormatible, Stagg, Kapantha). Their wlk_l / wlk_r clips have the head turned to that side, so straight-ahead walking reads visibly wrong:
- While CHASING outside the charge ring, a quadruped MUST NOT walk straight toward its target. It MUST approach on an arc (circling / spiraling in), and the walk clip MUST match the geometry:
wlk_lwhen the target is on the enemy's left,wlk_rwhen on its right — the turned head faces the target. - The straight-line approach is the
sttdash-charge: inside the charge ring (attack range × charge mult) with the attack off cooldown, the enemy MAY dash straight at the target playingstt, then attack on arrival.sttis the ONLY clip that moves a quadruped straight forward. (For this familysttis not a stand/idle clip — these rigs use their realwatfor idle.) - The arc side MAY change over time (re-picked on wander-like timers); a dash that loses its target (distance grows past ~1.5× the charge ring) SHOULD fall back to arcing.
Current Godot runtime diverges: it walks straight and fakes circling by alternating wlk_l/wlk_r on a timer (_walk_variant_timer) — that hack is retired when this contract is implemented (the enemy-attacks runtime PR). The #/enemy-room quadruped room implements this contract as the reference. Per-enemy quirks (Kapantha SHOULD prefer attacking from behind) and rare-as-leader pack behavior are open design — see the pack-behavior issue.
Archetype: Quad machine — hover kiter (normative)
Applies to archetype: quad_machine (quad rigs — Izhirak-S6, Azherowa-B2). These robots hover: four directional walks (wlk_f/wlk_b/wlk_l/wlk_r) are strafes relative to a body that keeps facing its target.
- While engaged, a quad machine's facing MUST stay on the target — it never turns its body to travel. Movement is strafing, and the clip MUST match the movement direction relative to facing:
wlk_fclosing,wlk_bretreating,wlk_l/wlk_rlateral. - It MUST hold a standoff band around
fsm.standoff_rangeto stay out of melee reach: target inside the band → retreat (wlk_b); target beyond it → close (wlk_f); at standoff → strafe laterally (side re-picked on a timer). It MUST NOT voluntarily close to melee range. - Attacks are ranged, fired from standoff when off cooldown and the target is inside an attack's range band:
atkis a projectile,atkblobs a grenade (area damage where it lands). Per the shared timing contract, ranged attacks release their projectile at the damaging-window open (/mechanics/enemy-attacks — attackkind). - Counterplay is intentional: the kiter does no pathfinding escape, so room geometry beats it — backing one into a corner collapses the standoff band and forces melee exposure. An implementation MUST NOT add wall-aware escape routing that defeats cornering.
Archetype: Big-rig combo (Hildegao family) — normative
Applies to archetype: bigrig_combo (gorilla rigs — Hildegao, Hildeghana, Hildegigas). Three authored attacks plus a threat display:
- Aggro threat: on noticing the target (IDLE → CHASING) the gorilla MUST play
stt— beating its chest — and hold position for the clip before pursuing. (For this familysttis the threat display, not a stand/idle clip; the melee archetypes' equivalent istht.) atk1punch combo (melee arc): heavy right-handed punch with a quick left uppercut, at melee reach.atk2running shoulder slam — the segmentedchargekind:atk2_stis the stationary windup, thenatk2_lploops while the gorilla charges forward along the facing locked at attack start, hitting on first contact (arc test each tick, one hit max — an i-frame dodge lets the slam pass by and keep going), ending after the hit or aftermax_rangeof travel with theatk2_edrecovery clip. The timeline fractions do not apply to this kind; the segments ARE the timeline.atk3belly flop — theleapkind: the gorilla jumps to the target's position (captured at the damaging-window open), traveling during the window and landing at its close for area damage (hit_reachradius); i-frames are tested at landing.- Attack trigger, generalized: CHASING → ATTACKING fires when the cooldown is ready and at least one attack's range band contains the target distance (the slam and flop fire from range). For enemies whose bands all end at melee reach this is identical to the old melee gate.
Archetype: Flyer combo (Pelcatraz family) — normative
Applies to archetype: flyer_combo (roc rigs — Pelcatraz, Pelcatobur). The rig's transitions are sparse (per kion: it feels disconnected, and it felt that way in the original game too — do not over-smooth what the source didn't have):
- Takeoff on aggro: idle is grounded; on noticing the target the roc MUST play
stt— taking flight — and rise to its hover height before pursuing. (Third meaning ofsttso far: quadrupeds dash, gorillas beat their chest, rocs take off — per-archetype clip semantics are the rule, not the exception.) - Shoulder-height hover is the design: while engaged the roc MUST stay airborne at roughly the player's shoulder height (
fsm.hover_height) — this is what makes it hard to hit with melee weapons, and an implementation MUST NOT ground it during combat. - Two locomotion clips:
fly(flaps to propel forward) while closing distance;tk(flaps to move around / turn) while hovering and orbiting near the target (orbit radius =fsm.standoff_range). - Attacks from the air:
atk1beak strike andatk3wing swipe are close melee arcs;atk2(flapping wings) is uncertain — possibly a gust attack (kion). Authored provisionally as a wider, weaker mid-range arc; an explicit open question until confirmed in-game.
Archetype: Two-attack melee + tech (Hypao family) — normative
Applies to archetype: two_attack (seal rigs — Hypao, Vespao). Standard ground locomotion; two attacks:
atk1tail swipe — close-range melee arc (wide sweep).atk2is a tech cast (barta-family ice). The attack'stechfield names the technique, and the Godot runtime MUST deliver it through the real technique system (element, freeze procs, tech visuals) — the room sim only approximates the delivery shape viakind.- Per-enemy divergence on a shared rig: Hypao casts Barta; Vespao (the rare) SHOULD cast the upgraded family tech — Gibarta authored; Rabarta is the open alternative (kion: "gibarta or rabarta"). First case of one rig with two different attack tables — the per-enemy table model exists exactly for this, and it feeds the rare-as-upgraded/leader theme (pack-behavior issue).
Archetype: Roller (Rohjade family) — normative
Applies to archetype: roller (armadillo rigs — Rohjade, Rohcrysta). The rig has no attack clip because the transform sequence IS the attack (sequence per kion's read of the clips — confirm in-game): trf1 rolls up → wat3 is the curled ball, which the engine MUST rotate while it travels (the clip has no motion of its own) → trf2 unrolls and falls over.
- Locomotion: walks (
wlk) at medium distance from the target (fsm.standoff_range), adjusting position until it has a straight path, then rolls. Idles:wat1standing,wat2lying down;sttis "become active" — held on aggro like the other display clips (fourthsttmeaning). - The roll is a
charge-kind attack with explicit segment clips (charge_segments). It MUST NOT stop on hitting the player (stop_on_hit: false) — the same animation plays whether it bowls the player over or hits a wall — and it travels past where the target stood at roll start (overshootmeters beyond the start distance, capped bymax_range), forcing an evade. - Knockdown denies the punish: a roll hit MUST knock the player down (
knockdown: true) — a hit player cannot capitalize on what follows. An i-frame dodge avoids the roll entirely. - The vulnerability window is the counterplay: while
trf2plays (the fall-over recovery), the roller MUST take increased damage (recovery_vulnerable_mult). Evade the roll → punish the sprawl. An implementation MUST NOT shorten or skip this window.
Archetype: Ape gunner (Froutang family) — normative
Applies to archetype: ape_gunner (orangutan rigs — Froutang, Frunaked). Design note (kion): this family is flagged as the most immersion-breaking content in the game — pistol-wielding monkeys — and is a candidate for replacement with EP2 enemies. The behavior is authored regardless, so the game plays correctly until that call is made.
- Aggro:
sttgoes from sitting to standing — held on aggro like the other display clips (fifthsttmeaning). Idlewatscratches its stomach. - Two pistol shots:
atckshtis the shoot attack;atcktukilooks like a different shoot attack (kion — uncertain, authored as a second projectile variant pending confirmation). - Charged punch is a clip sequence (kion):
atcksttwinds up →atckwatholds the charge →atckswgswings. Authored viawindup_clips: the windup clips play sequentially as pure telegraph (MUST NOT damage), then the swing clip carries the normal damaging window. The long telegraph is the trade for the charged damage. wtclp(raises hands above head — maybe intimidation) is unused pending confirmation — an open question; a taunt/display slot if it proves real.
Archetype: Box mimic (Bolix family) — normative
Applies to archetype: box_mimic (shrimp rigs — Bolix, Goldix). Hermit-crab behavior: it pretends to be a box and stays hidden until the player gets close, at which point it comes out and attacks. In Paru this is the "cleared the room but the gates won't unlock" moment.
- Dormant disguise: while idle the mimic MUST be stationary and disguised — holding the first frames of
stt(kion's read: the clip starts fully boxed). It MUST NOT wander and MUST NOT aggro atdetection_range— the disguise overrides normal detection entirely. - Reveal: when the target comes within
fsm.reveal_range(much smaller than detection range), the mimic playsstt— poking its head out of the box — as an aggro-display hold (sixthsttmeaning), then fights as a normal melee walker:tk1active idle,wlk1walk,atkstrike. - Reveal-cancel (kion's theory, authored provisionally): if the target backs out past ~1.5× the reveal range mid-reveal, the mimic plays
tk2— partially out, then retreats — and returns to the disguise. Whether it re-hides after a full reveal is an open question (authored: it stays active). - The sway tell (uncertain):
wlk2— the box sways around — MAY play occasionally while disguised as the subtle hint that a box is alive ("the game might give subtle hints that some boxes are actually moving"). Authored as a rare idle sway; confirm in-game. - Room-clear MUST count mimics: gates stay locked until disguised mimics are found and killed — that IS the Paru experience; the field controller's room-clear logic must include dormant mimics in the alive-enemy count.
Archetype: Hopper (Porel family) — partial
Applies to archetype: hopper (frog rigs — Porel, Pobomma, Pomarr). Authored so far (kion):
- Bubble projectile (definite): the frog shoots a bubble at the player from close-to-mid range — authored as a
projectileattack on theatkclip. - A second melee attack is an open question — kion is unsure whether they have bubble + melee or just bubbles; the rig has only one
atkclip, so a melee would share it. Not authored pending confirmation. - Hop locomotion pending:
jmp(leap) andtur(turn) clearly encode hopping movement, but its behavior isn't authored yet — the frogs use the baseline walk chase until those notes land.sttsemantics unconfirmed.
Archetype: Shooter (Korse family) — normative + leader dynamic
Applies to archetype: shooter (s050 rigs — Korse, and Akorse the leader variant). Modeled on PSO's Canadine / Canane: hovering shooters that fly with a leader. Whether the original PSZ actually implements the leader dynamic is an open question (kion) — it is specced here as the design.
- Normal mode: hovers at standoff (
fsm.standoff_range), holding position (wat) and adjusting withrun, firing theatk_shshooting attack as a projectile from its band. - Leader-loss berserk (the Canadine dynamic): when its leader (Akorse — the Canane analog) dies, a shooter SHOULD go berserk: it plays
atk_an— confused, spinning — once, then loopsatk_jilunging straight at the player and explodes on contact (self-destruct: one AoE hit, the shooter dies). The explosion triggers on contact regardless of i-frames; i-frames dodge the damage, not the blast. - The berserk kamikaze is authored as a
berserk_onlyattack (excluded from the normal attack gate); the room previews the sequence via a debug trigger since single-enemy rooms cannot host the leader relationship — pack wiring is the pack-behavior issue (#495), and Akorse-as-leader is its first concrete data point. atk_ji's remaining question: kion reads_jias the lunge (self-destruct dive);atk_anas the confusion spin;stt= "start" (semantics beyond that unconfirmed).
Poison Lily — normative (backported from the implementation)
Unlike every section above, this one was implemented first: scripts/3d/enemies/poison_lily.gd is already correct in Godot (kion), and this contract is backported from it. The lily keeps its own script (the sanctioned EnemyBase subclass exception).
- Stationary: the lily never moves; while awake it rotates to face its target. Model scale is
0.09(the raw GLB is ~11× too large — the tool renders with the samemodel_scale, and the runtime applies it from the table). - Sleep/wake cycle: SLEEPING (
waitclooping, closed) → withindetection_range(12) or on being hit → WAKING (wakeplays once) → IDLE_AWAKE (waitolooping, tracking the target). Target beyond detection range → back to SLEEPING. - Attacks run through the shared frame-tied machinery off the authored table (#629 — the old bespoke midpoint/MELEE_RANGE split is retired):
bite(melee arc, band 0–3) andpoison_spit(projectile, band 3–12) select by range band; both release at the damaging-window open (windup_frac 0.5— the attack clip's midpoint, keeping this contract). The spit rides the shared projectile delivery (/mechanics/enemy-attacks kindprojectile: locked facing, fsm.ts speed 10, flight = the attack'shit_reach= 18 = detection × 1.5) and adds a poison DoT: 3 damage per second for 5 seconds on contact. - Clip map (custom names, no rig prefix):
waitcsleep ·wakewake ·waitoawake idle ·attack·damege(sic) damage ·die. - The #/enemy-room sim renders scale + stationary facing + both attacks; the sleep/wake cycle itself is not simulated there (it lives in the implementation and this contract).
Helion: close claws and spin-lunge
archetype: lunging_melee applies to Helion and the shared Blaze Helion rig. Inspected clips: atk close claws (1.317 s), atkb traveling spin-lunge (0.900 s), run locomotion, stt rising display. Locomotion/display MUST NOT damage. Variant elemental behavior is unverified.
Initial gameplay tuning: claws at 0–2.5 m with contact at 30–60% of the clip; spin-lunge beyond 2.5 through 7 m with travel/contact at 15–70%. The lunge MUST lock facing and travel budget at attack start, retain stationary preparation and recovery, and resolve at most one hit or dodge. Walls/floor edges MUST stop travel; hurt/death MUST cancel it. These ranges and timings require playtesting and are not recovered DS constants.
The lunge MUST remove only Point_Spine01 planar translation from an instance-local animation copy, preserving vertical motion and rotation. Collision-driven travel keeps the rendered body with its hurtbox and prevents snapping back after the clip.
Helion pursuit tuning (October 8 playtest): on acquiring a target it MUST play stt once as a stationary, harmless aggro display. Its original trigger remains unverified. Active pursuit MUST run, including beyond the 7 m lunge band and while an attack cools down. After an attack it MUST recover for 0.35–0.65 s before pursuing again; cooldown still applies. Retreating players entering the 2.501–7 m band SHOULD provoke repeated committed atkb attacks through ordinary range selection, without reading healing inputs. Close claws and the existing dodge/wall/interruption guarantees remain unchanged. Blaze shares this tuning.
Remaining families: provisional gameplay contracts
This pass replaces generic attacks with explicit imported clip tokens. Ranges, hit windows, weights and delivery shapes below are gameplay tuning, not recovered DS constants. Existing roster/model identities are preserved. Bat confusion, Finjer elemental ailments, Mother warp destination rules, and original mine persistence are unverified; none are inferred from effect filenames.
- Batt/Bullbatt: close bite (0–2 m), traveling sonic shot (beyond 2–9 m), both use the sole atk clip. The sonic MUST be visibly distinct and dodgeable; confusion is not applied.
- Finjer R/B/G: segmented spin at 0–4 m (atk_sp_st → atk_sp_lp → atk_sp_ed); shot beyond 4–11 m (atk_sh). Preparation and recovery MUST be stationary and harmless. Movement uses run_lp. Variant shot colors do not imply statuses.
- Tank rigs (current Phobos IDs): close explosive lob (atk_mi, 0–3 m), shot (atk_sh, beyond 3–7 m), bazooka (atk_bz, beyond 7–12 m). The lob targets the release position and explodes once; it is a provisional mine approximation. Movement uses run_lp.
- Swordman rigs (current Arkzein/Zaphobos IDs): close punch (atk_pu, 0–2 m), sword sweep (atk_sw, beyond 2–4 m), thrown sword (atk_th_st, beyond 4–10 m) released at the end of the throw preparation, followed by harmless atk_th_ed recovery. The local GLBs omit the viewer's atk_th_sw clip; runtime MUST NOT silently substitute an unrelated attack.
- Mother rigs: Blade uses the two sword clips up close and a gun at distance; Shot uses a close sweep and a prepared gun shot; Force uses a close area cast and a prepared traveling cast; the combined Mother rig uses swords, gun and cast. Gun/cast sequences MUST play start → one loop → end, with no extra hits in end recovery. Spell delivery is provisional neutral damage, not an invented named technique. Idle floating pose supplies locomotion until warp rules are verified.
- Coliseum Mixed Groups MUST expose named presets for bats with Helion, all Finjers, tanks with swordmen, the three Mothers, and a combined pressure encounter. Enemies spawn apart; room-clear return MUST wait for every enemy, including dormant ones.
Optional attack recovery_clip plays once after the main clip and before loafing. It MUST block movement and new attacks, deal no new damage, and cancel on hurt/death. Optional fsm.move_clip overrides walk/run requests only; it MUST NOT replace attack clips.
Shared runtime guarantees
- Every enemy controller, including Poison Lily, MUST tick cooldowns and status durations while active and honor spawn and immobilization gates.
- Idle requests MUST resolve to an archetype's idle pose, never to a transition through a generic
wat → sttalias. Snake low idle iswat1, roller standing idlewat1, revealed mimic idletk1, and airborne flyer idletk. - The snake's
wt2wlowering MUST finish before recovery locomotion. - Roller visual rotation MUST return to its pre-charge basis on recovery, interruption, or death. Hits during
trf2MUST preserve the remaining recovery and vulnerability; lethal hits still kill normally. - Enemy Barta and Gibarta MUST use the shared ice-technique delivery profiles: ground-level Barta bolt, three Gibarta fan waves. A cast resolves damage at most once per target, with ice identity and a 20% freeze chance on a successful contact. Freeze lasts two seconds, blocks actions/movement, breaks on a later damaging hit, and cannot be applied through dodge i-frames. These proc/budget choices are gameplay tuning, not claims about DS values.
Automated combat decision scenarios
data/combat_scenarios.json defines explicit enemy, distance, condition, and allowed-attack expectations independently of the attack table. The harness MUST enter pursuit and let the ordinary AI select its action; it MUST NOT preselect an attack or force ATTACKING. Empty allowed sets mean no attack may begin within the bounded observation period. Overlapping weighted bands use allowed sets and deterministic seeds, not a hardcoded winner.
The enemy-room simulation and Godot unit runner MUST consume the same applicable cases. Freeze, dormancy, entrance and stun gates are Godot-only because the web simulation does not model them. The live Coliseum probe MUST also exercise these cases against imported rigs. Passing these cases confirms the authored conditions, not original-game authenticity or subjective feel; placeholder families remain unvalidated.
Status effects & elements
A hit carrying an element MAY proc a status. Trigger chance is 10% + 10% × element_level. The full status table (CombatManager.STATUS_EFFECTS):
| Status | Duration | Effect |
|---|---|---|
| freeze | 2s | immobile, takes 1.5× damage, breaks on hit |
| sleep | 2s | immobile, breaks on hit |
| stun | 3s | phase 1 immobile (1s), phase 2 can move but can't attack |
| burn | 3s | 3% max-HP/sec DoT, −0.1 defense |
| poison | 3s | 5% max-HP/sec DoT |
| slow | 3s | −0.3 evasion |
| paralysis | 3s | 50% action-skip chance |
Plus the instant devil effect (dark element): no timer — it drops the target to ¼ HP immediately. The same status MUST NOT stack on itself; the model tints to the most recent status's colour while any effect is active.
The element → status routing (CombatManager.ELEMENT_STATUS, the single source of truth) — a successful proc picks uniformly among the element's statuses, so every status above MUST be reachable:
| Element | Procs |
|---|---|
| fire | burn |
| ice | freeze or slow |
| lightning | stun or paralysis |
| dark | poison or devil (instant) |
| light | sleep |
Per-model SFX
EnemyBase.ENEMY_SFX is a map keyed by model_id, each entry naming a damage / death / attack / idle sound. On _ready the enemy picks up the entry for its model; a missing key resolves to silence for that enemy.
Minimap markers
The field minimap tracks live enemies (issue #422). Each alive enemy in the currently loaded cell MUST appear as a filled orange dot at its minimap-projected position, visually distinct from the player's directional arrow. Scoping mirrors the player marker exactly: loaded cell only, no marker cap (the count is simply the alive-enemy count in the cell), and off-cell enemies MUST NOT get a marker.
| Enemy event | Minimap marker behavior (MUST) |
|---|---|
| Normal enemy present in loaded cell | a filled orange dot at its minimap-projected position |
Boss (EnemyData.is_boss) present in loaded cell | a filled orange dot ≈8× the area of a normal-enemy dot |
| Enemy moves | the marker position updates to follow it |
| Enemy dies | the marker is removed |
| Enemy not in the currently loaded cell | no marker |
The marker set MUST stay in sync with the live roster via the death signals (died / defeated) — no stale dots after a kill.
Implemented by
scripts/autoloads/enemy_registry.gd(EnemyRegistry) — loads everydata/enemies/*.tresEnemyDataat boot and serves the roster keyed byid.scripts/autoloads/enemy_spawner.gd(EnemySpawner) —generate_wave()picks enemies by area/difficulty pool and rolls per-tier (common/uncommon/rare → normal/elite) stat instances.scripts/autoloads/combat_manager.gd(CombatManager) — attack/damage resolution, the element-weakness matrix, and the status-effect table (STATUS_EFFECTS).scripts/3d/enemies/enemy_base.gd(EnemyBase) — the per-enemy node: theEnemyStateAI state machine, animation, status application, and per-model SFX.scripts/3d/field/room_minimap.gd(RoomMinimap) — the live enemy dot layer:track_enemy/ death-signal untrack, per-frameupdate_enemiesprojection, boss-sized dots.