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); Mother casts currently use provisional neutral delivery until their named techniques are verified.
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), lunge (one clip: locked-facing collision-limited travel and swept contact during its damage window, stationary preparation/recovery; max_range caps travel and hit_reach is the contact radius), 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.
recovery_clipOptional harmless end clip after the main attack. The enemy MUST remain stationary and cannot select another attack until this clip finishes, then enters loafing. Hurt or death cancels the sequence. It does not extend the main clip's damaging window.
projectile_visual / projectile_colorOptional Godot presentation: sonic ring or default orb, with RGB hex color. These MUST NOT change the contact radius or imply an elemental status. The web room uses its generic projectile marker.
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 overridesOptional spawn_clip names an entrance played once at room reveal (Booma: stt), separate from attack windup. Per-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.

Straight projectiles MUST sweep their planar contact radius across the full physics step, clamped to their remaining range. First contact consumes the shot exactly once. A contact during dodge i-frames MUST suppress both direct damage and on-hit effects (including poison); dodging MUST NOT merely defer damage into a later status tick. This swept test supersedes endpoint-only checks in both the runtime and web simulator.

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
Lunging meleeatk close claws; atkb traveling spinlion, lion_rare
Simple meleeatk, ded, dmg, stt, wat, wlk (+run/tht/atkb variants)bat, circle, vulture, lizard, rabbit (+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 (prepared dash)atk prepare → run dash → atk_mi recoverybooma (=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