Enemy Attacks

The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.

Enemy attacks share one data model: the per-enemy attack table in data/enemy_attacks.json, tuned in the #/enemy-room web tool (the enemy-side sibling of #/combat-room). The tool's simulation, its overlays, and the Godot runtime all read the same table — they MUST NOT use different shapes or timings. The behavior state machine (IDLE → CHASING → ATTACKING → LOAFING → HURT) stays owned by /states/enemies; this page defines what happens inside ATTACKING and how an attack is chosen.

Data model — data/enemy_attacks.json

Keyed by enemy id (the data/enemies/*.tres id). Seeded from the .tres registry by scripts/tools/gen_enemy_attacks.py (merge-preserving — re-runs keep hand-tuned rows); the stats block mirrors .tres values. Resolved (#509 Godot PR): .tres is authoritative for stats (attack_base, ranges, cooldown) and the JSON is authoritative for the attack tables + fsm params only — the runtime (EnemyAttackRegistry) reads attack defs from the JSON and stats from EnemyData, and ignores the JSON stats block (it stays purely a mirror for the web sim). No two masters.

FieldMeaning
clipAnimation-name token as the runtime resolver consumes it (atk, atk2, atk_hi…) — NOT a full clip name. Resolution = exact match, *_<token> suffix, the ANIM_ALIASES table, then (attack only) the last-resort atk-segment scan excluding st/lp/ed segment pieces — the normative order lives in /states/enemies and MUST be identical in tool and runtime.
windup_frac / damage_end_fracFractions of the resolved clip's duration. Telegraph = [0, windup_frac), damaging window = [windup_frac, damage_end_frac] — windup end and damage start are definitionally the same instant.
hit_half_angle_deg / hit_reachThe arc hit shape (below).
min_range / max_rangeSelection band on the distance to the target at the moment ATTACKING is entered.
techOptional: the attack casts a named game technique (barta, gibarta…). The Godot runtime MUST deliver it through the real technique system — element, status procs (barta family freezes), tech visuals; the room sim only approximates the delivery shape via kind. First users: Hypao (barta) / Vespao (gibarta); the mother casters' tec_* clips will use it too.
kindHit delivery: melee_arc (default — the arc test below), projectile (straight projectile released at window open; hits on contact, i-frames at impact), lob (grenade released at window open toward the target's position, landing for area damage; i-frames at landing), charge (segmented _st windup → _lp loop while moving forward along the locked facing, hitting on first contact, → _ed recovery; travel capped at max_range; timeline fractions do not apply — the segments are the timeline), or leap (the enemy itself travels to the target's window-open position during the window, landing at its close for area damage; i-frames at landing). All kinds resolve at most once per attack.
windup_clipsOptional list of clip tokens played sequentially before the attack clip as pure telegraph — they MUST NOT damage. The timeline fractions apply to the attack clip itself, offset past the windup total. First user: the ape gunner's charged punch (atckstt wind-up → atckwat charged hold → atckswg swing).
charge extras: charge_segments / overshoot / stop_on_hitFor kind: charge. charge_segments names the st/lp/ed clips explicitly when the rig doesn't use _st/_lp/_ed suffixes (the roller's trf1/wat3/trf2); the engine MUST rotate loop clips that have no motion of their own (wat3 curled ball). overshoot makes the travel target = start distance + overshoot (capped by max_range) instead of always max_range. stop_on_hit: false lets the charge roll through the target instead of ending on contact.
berserk_onlyExcluded from the normal attack gate; used only by a berserk mode (the shooter's leader-loss kamikaze — spec /states/enemies §shooter). Its hit_reach is the contact-explosion AoE radius and damage_mult the blast damage.
knockdown / recovery_vulnerable_multknockdown: true — a hit MUST knock the player down (denies the follow-up punish; the runtime maps this to the player knockdown reaction). recovery_vulnerable_mult — while the attack's recovery clip plays, the enemy MUST take this damage multiplier (the roller's fall-over punish window); the room surfaces it in the log/overlay.
weightRelative selection weight among in-band candidates.
damage_multDamage dealt = stats.attack_base × damage_mult (replaces the legacy fixed 10).
fsm overridesPer-enemy walk/charge multipliers, loaf durations, hurt duration, attack_fallback_duration; plus archetype params — standoff_range (kiter distance / flyer & roller orbit), hover_height (flyers), reveal_range (box mimic: the disguise-break distance that replaces detection). Defaults = today's enemy_base.gd constants; SHOULD-level tuning freedom.
clip_notesOptional, documentational (MAY): authored clip semantics keyed by token — what each clip on the rig is (idle vs attack-ready vs transition). Shown in the tool next to the clip picker; the runtime ignores it. First worked example: garapython.
model_scale / idle_clip / fsm.stationaryEntry-level render scale for oversized source GLBs (poison lily: 0.09 — the runtime applies the same factor); the engaged-idle clip token for rooted enemies; and the rooted flag itself (never moves, faces the target, attacks from its bands).
archetypeBehavior-archetype id from the table below, stamped by the seeder from model_id (classification lives in gen_enemy_attacks.py MODEL_ARCHETYPES, not in hand edits). Every enemy MUST carry a known archetype — an unclassified model fails the data test, forcing a decision when new rigs land. Selects the enemy's room at #/enemy-room/<archetype> and, later, the runtime behavior module.

Every non-boss enemy MUST have an entry with ≥1 attack. An enemy missing from the file MUST fall back to the defaults block plus one default atk attack — it never crashes and never refuses to attack.

Attack selection

Attack timeline — frame-tied damage

This section AMENDS the current runtime, which deals a fixed 10 damage instantly at attack start (enemy_base.gd _start_attack) — untelegraphed and undodgeable. Per the working agreement this contract lands first; the Godot implementation follows in the next PR.

Hit shape — the arc

A flat arc: apex at the enemy's origin, aimed along the facing locked at attack start. A target is inside iff its XZ distance ≤ hit_reach + the target's collision radius AND the XZ angle from the locked facing ≤ hit_half_angle_deg. Deliberately simpler than the player weapon cone (targeting): no apex pull-back, no vertical bound. The tool's overlay and the runtime hit test MUST be this same shape.

Dodge interaction

A target that is invincible (dodge i-frames — the first 0.2 s of a dodge, /mechanics/dodge) at its first arc-test pass is dodged: the attack consumes its one resolution on it and MUST NOT re-test that target for the rest of the window. I-frame timing beats the window by design — a well-timed dodge through the arc is safe even if the dodge ends inside the window.

Recovery & missing clips (#477 layering)

The attack-recovery MUSTs in /states/enemies apply unchanged: attack end is keyed on the resolved clip (or the fallback timer), never on a damage event. Layered on top:

Difficulty scaling — aggression & timing (#522)

Enemies scale their aggression and timing by the session difficulty (SessionManager.get_session().difficulty, one of normal / hard / super-hard). Normal is the identity baseline — the current passive feel is preserved exactly. Higher tiers widen aggro and tighten cadence/telegraph; this is deliberately the only knob difficulty turns here — HP and damage scaling stay with damage math (#157) / the attack model, not this section.

Tierdetectioncadencereaction
normal1.01.01.0
hard1.250.800.85
super-hard1.50.650.70

The table is SHOULD-level starting calibration (EnemyBase.AGGRO_SCALING) — tuned by play-test, not normative to the decimal. What is normative: Normal MUST be identity, higher tiers MUST NOT reduce aggro/cadence pressure, and the scaling MUST apply to aggro/timing only (never HP/damage from this path).

Pre-strike telegraph & stance model (#491)

Resolved (playtest #491): striking straight from locomotion read as "walk up, freeze, cheap hit" — no anticipation, unreadable. So ATTACKING MUST open with a telegraph sub-phase before the damage window: the enemy holds a readable attack-ready pose, facing the target, long enough to react (run, evade, or commit). The telegraph deals no damage; it is not a 7th FSM state — it is the head of ATTACKING (the six states in /states/enemies stand).

The deeper stance sub-state question (persistent low/raised modes with per-enemy transition tables beyond attack telegraphs) remains open under #491; this contract covers the attack-time telegraph, which is what the feel needs first.

Rig groups & behavior archetypes

PSZ enemies behave differently by type and by interaction — the single chase → attack → loaf melee loop this page's FSM parameters tune is only one archetype. The rigs themselves define the minimum behavior set (generated by scripts/tools/enemy_anim_groups.py, which parses every enemy GLB's clip names; re-run it and reconcile this table when rigs change):

ArchetypeVocabulary signatureModels
Simple meleeatk, ded, dmg, stt, wat, wlk (+run/tht/atkb variants)bat, circle, vulture, lizard, rabbit, lion (+recolors/rares)
Quadruped circler (specced — /states/enemies §quadruped)wlk_l/wlk_r head-turned arc walks + stt straight dash-charge + stpwolf, hyena, deer, tiger
Quad machine (specced — /states/enemies §quad-machine)hover kiter: 4-dir strafes wlk_b/f/l/r, atk projectile + atkb grenade lobquad
Bruiser (height swings)atk / atk_hi / atk_mibooma (=jigobooma)
Big-rig combo (specced — /states/enemies §big-rig)atk1 punch combo, atk2_st/lp/ed running shoulder slam (charge), atk3 belly-flop leap, stt chest-beat threatgorilla
Flyer combo (specced — /states/enemies §flyer)stt takeoff on aggro, shoulder-height hover, fly approach / tk orbit, atk1 beak / atk2 gust? / atk3 wing swiperoc
Two-attack melee + tech (specced — /states/enemies §two-attack)atk1 tail swipe, atk2 barta-family tech cast (Hypao: barta · Vespao: gibarta/rabarta)seal
Stance riser (#491)wat1/wat2 + stt rise / wt2w lowersnake (authored); shinowa's wat2/backstep/transform is adjacent
Roller (specced — /states/enemies §roller)rolls into a ball: trf1 roll-up → wat3 engine-rotated ball travel (through the player, knockdown) → trf2 fall-over vulnerability windowarmadillo (Rohjade, Rohcrysta)
Ape gunner (specced — /states/enemies §ape-gunner; EP2-replacement candidate)pistol shots atcksht + atcktuki(?), charged punch sequence atckstt → atckwat → atckswg, stt sit-to-stand aggro, wtclp intimidation(?)orangutan (Froutang, Frunaked)
Box mimic (specced — /states/enemies §box-mimic)dormant disguise (holds stt first frames; wlk2 sway tell?), reveal_range pop-out, tk2 peek-retreat cancel(?), then tk1/wlk1/atk meleeshrimp (Bolix, Goldix)
Hopper (partial — /states/enemies §hopper)bubble projectile from close-to-mid range (definite); melee second attack an open question; jmp/tur hop locomotion pendingfrog (Porel, Pobomma, Pomarr)
Rappythe rare bonus enemies — not actually tricksters (kion), just rare; dual ded1/ded2 + stt1/stt2 suggest the classic fall-down-then-fleerappy (Rappy, Ar Rappy, Rab Rappy)
Shooter (specced — /states/enemies §shooter; PSO Canadine/Canane)standoff hover + atk_sh projectile; leader-loss berserk: atk_an confusion spin → atk_ji kamikaze loop → contact explosion (Akorse is the leader — #495)shooter (Korse, Akorse)
Missile tankfires missiles at the player: atk_bz / atk_mi / atk_sh, segmented run_st/lp/edtank (Phobos, Phobos Dyna)
Boarderrobots riding a surfboard — highly mobile, hard to track down: atk_sh, atk_sp_st/lp/ed, run_st/lp/edboard (Finjer R/B/G)
Shadethe Dark Shrine enemies — look like the heartless from Kingdom Hearts (kion); grouped as one category pending behavior notesleg/lower + swordman (Derreo, Zerreo, Eulid, Eulidveil, Arkzein[s], Zaphobos[es])
Mother castersegmented atk_gu_*, tec_* casts, warp_st/ed teleportmother ×4
Uniquesmole (grd01–03 digs), shinowa; poison_lily backported from its implementation (/states/enemies §poison-lily: rooted, sleep/wake, bite-or-poison-spit at the clip midpoint, scale 0.09)

EnemyData.behavior already enumerates MELEE / RANGED / CHARGER / SWARM / TANK / BOSS but is largely unused by the runtime; as archetype rows get specced, that enum (or a successor keyed to this table) becomes the runtime dispatch. Bosses are explicitly out of scope for #/enemy-room: their five vocabularies are each unique and bound to their specific arena (movement anchors, phase geometry). That dedicated tool now exists at #/boss-room (#493): one room per boss, loading the boss rig inside its own arena stage (mapping in data/boss_arenas.json, seeded from the quests' boss segments), with a walkable player capsule, clip playback, and first-class st/lp/ed segment-chain playback. It is the observation instrument — draft boss phase/attack tables now live in boss_arenas.json (schema_version: 2) under the contract at /states/bosses, which reuses this page's attack timeline model (chains generalize windup_clips; unset fields take this page's defaults) and extends the kind set with boss-only kinds. boss_robot's full 26-clip kit is imported (patched apicula — the rig carries one extra unanimated root object apicula's exact-count match rejected; see /states/bosses §chaos-mobius).

Tooling & data flow

Implemented by (next PR)

← Mechanics