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:

FieldMeaning
id / namelookup key and display name
elementNative / Beast / Machine / Dark — drives damage attribution
locationsarea ids this enemy spawns in
is_rare / is_bossroster flags
model_id / animation_model_idGLB under assets/enemies/<id>/; rares MAY borrow a base model's animations
combat statshp_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.

StateBehavior
IDLEWander (random heading, half speed, edge-aware) or pause. Promotes to CHASING when the target enters detection_range.
CHASINGNavigate toward the target. Walk beyond 2× attack range, run/charge inside it. Stuck-avoidance steers perpendicular when displacement stalls.
ATTACKINGStop, face target, play attack, deal damage, start cooldown. Exits to LOAFING when the animation ends.
LOAFINGBack off along a curving semicircle for 2.5–4s, then return to CHASING.
HURT0.3s stagger on any hit; cancels an in-progress attack; returns to CHASING.
DEADPlay 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):

  1. Exact clip name equal to the token.
  2. Any clip ending in _<token>.
  3. The alias table (atk → atk1, atckwat; dmg → dam — five rigs name their damage clip dam: booma, swordman, tank, orangutan, shrimp).
  4. Attack only — last-resort scan: any clip with an exact atk underscore-segment (b062_atk_pu, b052_atk_sh, m061_atk_a, b072_atk_sa_a…) qualifies, excluding clips whose final segment is st, lp, or ed — 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.
  5. 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.

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.

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:

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.

Archetype: Big-rig combo (Hildegao family) — normative

Applies to archetype: bigrig_combo (gorilla rigs — Hildegao, Hildeghana, Hildegigas). Three authored attacks plus a threat display:

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

Archetype: Two-attack melee + tech (Hypao family) — normative

Applies to archetype: two_attack (seal rigs — Hypao, Vespao). Standard ground locomotion; two attacks:

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.

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.

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.

Archetype: Hopper (Porel family) — partial

Applies to archetype: hopper (frog rigs — Porel, Pobomma, Pomarr). Authored so far (kion):

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.

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).

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.

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

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

StatusDurationEffect
freeze2simmobile, takes 1.5× damage, breaks on hit
sleep2simmobile, breaks on hit
stun3sphase 1 immobile (1s), phase 2 can move but can't attack
burn3s3% max-HP/sec DoT, −0.1 defense
poison3s5% max-HP/sec DoT
slow3s−0.3 evasion
paralysis3s50% 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:

ElementProcs
fireburn
icefreeze or slow
lightningstun or paralysis
darkpoison or devil (instant)
lightsleep

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 eventMinimap marker behavior (MUST)
Normal enemy present in loaded cella filled orange dot at its minimap-projected position
Boss (EnemyData.is_boss) present in loaded cella filled orange dot ≈8× the area of a normal-enemy dot
Enemy movesthe marker position updates to follow it
Enemy diesthe marker is removed
Enemy not in the currently loaded cellno 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

← States