Enemy Hierarchy
The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.
Today every enemy is one class — EnemyBase (scripts/3d/enemies/enemy_base.gd) — driven by an EnemyData resource (/states/enemies). Beta keeps that as the default and adds justified behavior subclasses. This page is the contract both sides honor.
What the base owns (subclasses inherit, MUST NOT reimplement)
| Responsibility | Surface |
|---|---|
| AI state machine shell | EnemyState enum (IDLE / CHASING / ATTACKING / LOAFING / HURT / DEAD) + the per-state tick dispatch. Subclasses override state handlers, not the dispatch. |
| Damage intake | take_damage → HP mutation → HURT/DEAD transitions, damage numbers, hit SFX. |
| Status effects | Application via CombatManager.roll_element_status / apply_status_effect, non-stacking rule, DoT ticking, model tint (STATUS_COLORS). |
| Death & rewards | Drop rolls, EXP grant (#343), death animation/cleanup, session kill accounting. |
| Presentation plumbing | Model load + texture application (MeshUtils), per-model SFX map, reticle, hurtbox construction. |
| Floor-edge movement guard | _can_move_to all-3-probe rule — deliberately stricter than the player's 2-of-3 (#215-C6: intentional divergence; do not "unify"). |
When to subclass — and when NOT to
- New enemy, same behavior shape → a new
EnemyDataresource. No script. This stays the common case. - Movement/attack pattern divergence (flier, burrower, ranged keep-away — the
WolfEnemy/SmallEnemytier) → subclass overriding the specific state handlers; the enum and dispatch stay the base's. - Bosses (#62) → subclass with its own phase machine layered over the base intake/status/death contract, following the PSOBB FSM taxonomy (
headers/bosses.h): Architecture A — per-boss FSM, subtype-indexed, variants share one FSM; Architecture B — multi-actor outer phase sequencer coordinating actor FSMs (the finale shape). - Stat/aura variants (#108) → NOT a subclass: a multiplier set + visual flag on the spawn roll, BattleParams-style.
Invariants every leaf preserves
- Exactly one
EnemyStateactive; transitions go through the base's transition path so signals/animation stay coherent. take_damageis the only damage door — bosses MAY scale or gate it (armor phases) by overriding a hook, never by bypassing it.- Status non-stacking and the element→status routing (spec table) hold for every enemy; a leaf MAY declare status immunities as data, not by reimplementing application.
- Death always reaches the base's reward path — a boss that skips drops/EXP is a bug, not a feature flag.
Testing (two layers, per /engineering)
Each new subclass ships a seeded unit test of its divergent handlers (state-transition table, the test_player_states pattern) and a post-build probe (a matrix phase or kill_all room exercising it under autopilot).