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 (atkatk1, atckwat; dmgdam — 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: 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).

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