Enemy Data & AI
Enemy Data & AI
Status: Designed Author: user + game-designer Last Updated: 2026-03-28 System Index: #8 — MVP, Core Layer
Overview
The Enemy Data & AI system defines enemy data resources (stats, intents, scaling) and the AI logic that selects enemy actions each turn. Enemies are data-driven Godot Resources (.tres), similar to cards. Basic enemies use fixed action patterns (repeating sequences) for learnability. Elites and bosses use conditional logic (state-based decisions) for depth. All enemies telegraph their next action via intent icons before the player phase, giving players information to react to.
Player Fantasy
"I can read the battlefield and make smart decisions." Intent telegraphing turns combat into a puzzle — players see what's coming and choose how to respond. Basic enemies are learnable patterns you master; elites and bosses force you to adapt. In co-op, visible intents create natural coordination: "The boss is hitting for 20 — someone needs to block."
Detailed Design
Core Rules
- Every enemy type is an
EnemyDataGodot Resource (.tres) stored inassets/data/enemies/. - Enemies are spawned by the Combat System based on the current map node (encounter table).
- Each enemy has a list of actions it can perform. Actions are selected by the AI each turn.
- Before the player phase, all enemies reveal their intent — the action they will perform in the upcoming enemy phase.
- During the enemy phase, enemies execute their revealed intent in order (left to right on screen).
- Enemy HP scales with player count. Other stats do not scale.
- All enemy state and AI runs server-side only.
EnemyData Resource Schema
| Field | Type | Description |
|---|---|---|
id | StringName | Unique identifier (e.g., "slime", "goblin_shaman") |
display_name | String | Player-facing name |
base_hp | int | HP for a 2-player game (scales with player count) |
actions | Array[EnemyAction] | Available actions this enemy can perform |
ai_type | AIType enum | PATTERN or CONDITIONAL |
action_pattern | Array[int] | For PATTERN AI: indices into actions array, repeating |
conditions | Array[AICondition] | For CONDITIONAL AI: ordered conditions with action indices |
tier | EnemyTier enum | BASIC, ELITE, BOSS |
art_id | StringName | Reference to enemy art asset |
EnemyAction Resource Schema
| Field | Type | Description |
|---|---|---|
action_type | EnemyActionType enum | ATTACK, DEFEND, BUFF, DEBUFF, HEAL, SUMMON |
value | int | Damage/block/heal amount |
status_id | StringName | Status effect to apply (if BUFF/DEBUFF) |
status_stacks | int | Stacks of status to apply |
intent_icon | StringName | Icon shown during telegraphing |
target | EnemyTarget enum | ALL_PLAYERS, RANDOM_PLAYER, SELF, ALL_ENEMIES |
AICondition Resource Schema (CONDITIONAL AI only)
| Field | Type | Description |
|---|---|---|
condition_type | ConditionType enum | HP_BELOW_PERCENT, TURN_NUMBER, STATUS_ACTIVE, ALWAYS |
threshold | float | Comparison value (e.g., 0.5 for 50% HP) |
param | StringName | Optional param (e.g., status name) |
action_index | int | Index into actions array if condition is true |
Conditions are evaluated top to bottom. First match wins. Last condition should always be ALWAYS as a fallback.
Enums
- AIType:
PATTERN, CONDITIONAL - EnemyTier:
BASIC, ELITE, BOSS - EnemyActionType:
ATTACK, DEFEND, BUFF, DEBUFF, HEAL, SUMMON - EnemyTarget:
ALL_PLAYERS, RANDOM_PLAYER, SELF, ALL_ENEMIES - ConditionType:
HP_BELOW_PERCENT, TURN_NUMBER, STATUS_ACTIVE, ALWAYS
AI Selection Logic
PATTERN AI (basic enemies):
current_action_index = action_pattern[turn_number % action_pattern.size()]
next_intent = actions[current_action_index]
Example: Slime with pattern [0, 0, 1] and actions [Attack 8, Defend 6] → Attack, Attack, Defend, Attack, Attack, Defend…
CONDITIONAL AI (elites/bosses):
for condition in conditions:
if evaluate(condition, enemy_state):
next_intent = actions[condition.action_index]
break
Example: Goblin Shaman with conditions:
HP_BELOW_PERCENT 0.3→ action[2] (Heal 15)TURN_NUMBER == 0→ action[1] (Buff: +3 Strength to all enemies)ALWAYS→ action[0] (Attack 10)
HP Scaling
| Player Count | HP Multiplier |
|---|---|
| 1 | x0.75 |
| 2 | x1.0 (base) |
| 3 | x1.5 |
| 4 | x2.0 |
actual_hp = floor(base_hp * hp_multiplier[player_count])
Intent Telegraphing
Each action type has a standard intent icon:
| Action Type | Icon | Color |
|---|---|---|
| ATTACK | Sword | Red |
| DEFEND | Shield | Blue |
| BUFF | Up arrow | Green |
| DEBUFF | Down arrow | Purple |
| HEAL | Heart | Green |
| SUMMON | Plus sign | Yellow |
The intent shows the icon + value (e.g., sword icon with "12" = attacking for 12). Players can see exactly what's coming.
V1 Enemy Roster
| Enemy | Tier | HP (2p) | AI | Pattern/Conditions |
|---|---|---|---|---|
| Slime | BASIC | 30 | PATTERN | Attack 8, Attack 8, Defend 5 |
| Goblin | BASIC | 24 | PATTERN | Attack 6, Attack 10 |
| Skeleton | BASIC | 35 | PATTERN | Attack 6, Debuff (Weak 1), Attack 12 |
| Mushroom | BASIC | 28 | PATTERN | Debuff (Poison 3), Attack 5, Attack 5 |
| Shield Beetle | BASIC | 40 | PATTERN | Defend 10, Attack 7, Attack 7 |
| Goblin Shaman | ELITE | 55 | CONDITIONAL | Turn 0: Buff (+2 Str all enemies), HP<30%: Heal 15, else: Attack 12 |
| Dark Knight | ELITE | 70 | CONDITIONAL | HP<50%: Buff (+3 Str self), else: Attack 15 with Debuff (Vulnerable 2) |
| Dragon | BOSS | 120 | CONDITIONAL | Turn 0: Buff (+2 Str self), HP<30%: Attack 25 (ALL_PLAYERS), HP<60%: Attack 18, else: Attack 12 + Debuff (Weak 1) |
Encounter Tables
| Node Type | Enemies | Count |
|---|---|---|
| COMBAT | 1-3 BASIC enemies (random from pool) | Most common |
| ELITE | 1 ELITE + 0-1 BASIC enemies | Less common |
| BOSS | 1 BOSS | Final node only |
States and Transitions
Per-enemy runtime state during combat:
| Field | Type | Description |
|---|---|---|
current_hp | int | Remaining HP |
statuses | Dictionary | Active status effects and stack counts |
turn_number | int | How many turns this enemy has taken (for PATTERN index and CONDITIONAL checks) |
current_intent | EnemyAction | The telegraphed action for the current turn |
is_alive | bool | False when HP reaches 0 |
Enemy lifecycle: SPAWNED → ALIVE (selecting intents, taking actions) → DEAD (HP = 0, removed from combat)
Interactions with Other Systems
| System | Direction | Interface |
|---|---|---|
| Game State Manager | Reads | Player count for HP scaling, encounter_count for future difficulty scaling |
| Combat System | Called by | Combat spawns enemies, triggers intent selection at round start, triggers action execution in enemy phase |
| Card Effect Resolver | Called by | Resolver deals damage to enemies, applies statuses |
| Player Resources | Calls | Enemy attacks call deal_damage() on targeted players |
| Status Effect System | Bidirectional | Enemies can have statuses (Vulnerable, Weak, Strength). Status system ticks enemy statuses at phase boundaries. |
| Combat UI | Reads | UI displays enemy art, HP bar, intent icon, status icons |
Formulas
Enemy Damage Output
base_damage = action.value
if enemy has Strength: base_damage += strength_stacks
if enemy has Weak: base_damage = floor(base_damage * 0.75)
# Then passed to Player Resources damage resolution (Vulnerable check, Block, etc.)
Encounter Difficulty (V1 — simple)
encounter_value = sum(enemy.base_hp for each enemy in encounter)
Target ranges:
- COMBAT: 50-100 encounter value (2p base)
- ELITE: 80-130 encounter value
- BOSS: 120 encounter value (single enemy)
Expected Run Damage Budget
Across an 8-node run (5 combats, 1 elite, 1 rest, 1 boss):
- Basic combats deal ~15-25 damage per player per encounter (after block)
- Elite deals ~20-35 damage per player
- Boss deals ~25-40 damage per player
- Total expected damage: ~120-180 per player
- Starting HP: 60, one rest heals ~18 → budget of ~78 HP
- This means players MUST block effectively — can't just face-tank everything
Edge Cases
| Case | Resolution |
|---|---|
| Enemy has 0 HP from player cards | Mark is_alive = false. Skip this enemy's intent execution in enemy phase. Remove from targeting. |
| All enemies die mid-player-phase | Combat ends immediately. Skip enemy phase. Proceed to reward. |
| Enemy targets RANDOM_PLAYER but some players are dead | Only target living players. If all players dead, GAME_OVER. |
| Enemy buffs ALL_ENEMIES but some enemies are dead | Only buff living enemies. |
| CONDITIONAL AI: no condition matches | Should never happen if last condition is ALWAYS. If it does, default to first action. Log warning. |
| PATTERN AI: pattern array is empty | Invalid data. Log error. Default to first action in actions array. |
| Enemy with Strength buff deals DEBUFF action | Strength only modifies ATTACK actions. Non-attack actions are unaffected. |
| Enemy action targets SELF for ATTACK | Valid edge case for future self-damage mechanics. Resolve normally. |
| 1-player game HP scaling (x0.75) | floor() the result. A 30 HP slime becomes 22 HP solo. |
Dependencies
Upstream
| System | Dependency Type | Interface |
|---|---|---|
| Game State Manager | Soft | Reads player count for HP scaling |
Downstream
| System | Dependency Type | Interface |
|---|---|---|
| Combat System | Hard | Spawns enemies, triggers AI, executes intents |
| Card Effect Resolver | Hard | Targets enemies with card damage/status effects |
| Combat UI | Hard | Displays enemy sprites, HP, intents, statuses |
| Co-op Mechanics | Soft | Enemy count used by PER_ENEMY modifier |
| Map System | Soft | Map node types reference encounter tier (COMBAT/ELITE/BOSS) |
Contracts
- Enemy data resources are read-only at runtime. Runtime state (current_hp, statuses, turn_number) is held separately.
- Only the Combat System creates enemy instances. Other systems interact via the Combat System's enemy list.
- Intent is selected and revealed before the player phase. Intent cannot change during the player phase.
Tuning Knobs
| Knob | Default | Safe Range | Affects |
|---|---|---|---|
Per-enemy base_hp | Varies | 20-150 | Combat length per enemy |
Per-enemy action value | Varies | 4-25 | Damage pressure, block value |
| HP scaling multipliers | 0.75/1.0/1.5/2.0 | Adjust per tier | Co-op balance — too high = tedious, too low = trivial |
| Encounter composition (enemy count) | 1-3 basic, 1-2 elite | 1-4 | Action economy — more enemies = more incoming damage per round |
| CONDITIONAL thresholds | Varies | 0.1-0.9 | When elites/bosses change behavior |
| Status stacks on debuff actions | 1-3 | 1-5 | Debuff pressure on players |
Interaction warning: Enemy damage output × enemies per encounter × rounds per combat must be balanced against player HP + Block generation. If players generate ~15 Block per turn and face 20 damage, they lose ~5 HP per round → ~6 rounds of combat budget before dying.
Acceptance Criteria
| # | Criterion | Verification |
|---|---|---|
| 1 | All V1 enemies (5 basic, 2 elite, 1 boss) load without errors | Unit test: load each .tres, assert all fields valid |
| 2 | PATTERN AI cycles correctly | Unit test: Slime pattern [0,0,1] over 6 turns → 0,0,1,0,0,1 |
| 3 | CONDITIONAL AI evaluates top-to-bottom, first match wins | Unit test: Goblin Shaman at 25% HP → selects Heal, not Attack |
| 4 | HP scales correctly for 1-4 players | Unit test: 30 base HP → 22, 30, 45, 60 for 1-4 players |
| 5 | Intent is visible before player phase | Integration test: start combat, assert all enemies have a non-null current_intent |
| 6 | Dead enemies are skipped in enemy phase | Unit test: kill enemy, trigger enemy phase, assert no action from dead enemy |
| 7 | Enemy Strength modifies attack damage | Unit test: enemy with 2 Strength, 8 base attack → 10 damage |
| 8 | Encounter tables produce valid compositions | Unit test: generate COMBAT encounter, assert 1-3 BASIC enemies |