Skip to content

Combat Systems Guide

This guide covers MAID's combat systems, from basic attacks to complex boss encounters with tactical positioning.

Table of Contents


Overview

MAID's combat system is built on several interacting systems:

System Responsibility
MeleeCombatSystem Melee attack resolution
RangedCombatSystem Ranged attack resolution
DamageSystem Damage application and mitigation
BodySystem Hit location and body part damage
StatusEffectManager Buffs, debuffs, and conditions
RegenerationSystem HP/MP recovery
DeathSystem Death handling and respawn

Combat Flow

Player Command (kill, shoot)
        |
        v
    Target Resolution
        |
        v
    Attack Roll (hit/miss)
        |
        v
    Damage Calculation
        |
        v
    Damage Mitigation (armor, resistances)
        |
        v
    Damage Application
        |
        v
    Status Effects
        |
        v
    Death Check

Combat System Architecture

Core Systems

from maid_classic_rpg.systems.combat import (
    MeleeCombatSystem,
    RangedCombatSystem,
    DamageSystem,
    BodySystem,
    StatusEffectManager,
)

# Systems are registered with the world
engine.world.register_system(MeleeCombatSystem(engine.world))
engine.world.register_system(RangedCombatSystem(engine.world))
engine.world.register_system(DamageSystem(engine.world))
engine.world.register_system(BodySystem(engine.world))

Combat Components

Entities need these components for combat:

from maid_stdlib.components import HealthComponent
from maid_classic_rpg.components import CombatStatsComponent

# Health pool
entity.add(HealthComponent(
    max_hp=100,
    current_hp=100,
))

# Combat statistics
entity.add(CombatStatsComponent(
    level=5,
    strength=14,
    dexterity=12,
    constitution=12,
    attack_bonus=3,
    armor_class=15,
))

Damage Calculation

Attack Resolution

The melee combat system resolves attacks through several steps:

# 1. Roll to hit
attack_roll = roll_d20() + attacker.attack_bonus + weapon_bonus
hit = attack_roll >= target.armor_class

# 2. Check for critical hit
is_critical = attack_roll >= 20 + attacker.attack_bonus  # Natural 20

# 3. Calculate base damage
base_damage = weapon.base_damage + strength_modifier

# 4. Roll damage dice
if weapon.damage_dice:
    damage = roll_dice(weapon.damage_dice)  # e.g., "2d6"
else:
    damage = base_damage

Damage Types

MAID supports multiple damage types with different mitigation:

Type Description Common Sources
slashing Cutting damage Swords, axes
piercing Puncture damage Daggers, arrows
bludgeoning Impact damage Maces, hammers
fire Burn damage Fire spells
cold Freeze damage Ice spells
lightning Electric damage Lightning spells
poison Toxic damage Venomous attacks
holy Divine damage Holy spells
dark Shadow damage Dark magic

Damage Models

from maid_classic_rpg.models.combat.damage import (
    DamageInstance,
    DamageType,
    DamageResult,
)

# Create damage instance
damage = DamageInstance(
    amount=15,
    damage_type=DamageType.SLASHING,
    source_id=attacker.id,
    is_critical=False,
    armor_piercing=0.0,
)

Armor and Mitigation

Damage is reduced by armor and resistances:

# 1. Armor reduction (flat reduction)
armor_reduction = target.armor_value
mitigated_damage = max(0, damage - armor_reduction)

# 2. Resistance multiplier
resistance = target.resistances.get(damage_type, 1.0)
final_damage = int(mitigated_damage * resistance)

# Resistance values:
# 0.0 = immune
# 0.5 = resistant (half damage)
# 1.0 = normal
# 1.5 = vulnerable (extra damage)
# 2.0 = very vulnerable (double damage)

Configuring Resistances

from maid_classic_rpg.models.combat.damage import Resistances

# Fire elemental - immune to fire, vulnerable to cold
fire_elemental.add(Resistances({
    DamageType.FIRE: 0.0,      # Immune
    DamageType.COLD: 2.0,      # Double damage
    DamageType.BLUDGEONING: 0.5,  # Resistant
}))

Skills and Abilities

Ability Structure

from maid_classic_rpg.models.abilities import Ability, AbilityType, TargetType

power_strike = Ability(
    id="power_strike",
    name="Power Strike",
    description="A devastating overhead blow",
    ability_type=AbilityType.ACTIVE,
    target_type=TargetType.ENEMY,
    cooldown=10.0,  # seconds
    stamina_cost=15,
    mana_cost=0,
    requirements={
        "level": 3,
        "strength": 12,
        "skill:melee": 5,
    },
    effects=[
        {"type": "damage", "multiplier": 1.5, "damage_type": "slashing"},
        {"type": "stun", "duration": 2.0, "chance": 0.3},
    ],
)

Ability Types

Type Description
ACTIVE Must be manually activated
PASSIVE Always active when learned
TOGGLE Can be turned on/off
REACTIVE Triggers on specific conditions

Target Types

Type Description
SELF Affects user only
ENEMY Single enemy target
ALLY Single allied target
AREA_ENEMY All enemies in area
AREA_ALLY All allies in area
AREA_ALL Everyone in area

Skill System

Skills improve with use:

from maid_classic_rpg.models.skills import Skill, SkillCategory

melee_skill = Skill(
    id="melee",
    name="Melee Combat",
    category=SkillCategory.COMBAT,
    description="Proficiency with melee weapons",
    max_level=100,
    xp_per_level=100,  # XP needed per level
)

# Skills affect combat:
# - Higher skill = better hit chance
# - Unlock abilities at skill thresholds
# - Damage bonuses

Tactical Positioning

MAID supports tactical combat with positioning on a 3x3 grid.

Position Grid

      FRONT
   [F] [F] [F]    <- Melee range, high damage, high risk
   [M] [M] [M]    <- Middle, balanced
   [R] [R] [R]    <- Rear, ranged/support, protected
      REAR

Position Components

from maid_classic_rpg.models.combat.position import (
    CombatPosition,
    PositionRow,
    PositionColumn,
)

position = CombatPosition(
    row=PositionRow.FRONT,
    column=PositionColumn.CENTER,
)

Position Effects

Position Melee Damage Ranged Damage Defense
Front +10% Normal -10%
Middle Normal Normal Normal
Rear -20% +10% +10%

Position Commands

position front     - Move to front row
position center    - Move to middle row
position back      - Move to rear row
face north         - Face a direction (flanking bonuses)

Flanking

Attacking from behind grants bonuses:

from maid_classic_rpg.models.combat.facing import FacingDirection, calculate_flanking

flanking = calculate_flanking(
    attacker_facing=FacingDirection.SOUTH,
    target_facing=FacingDirection.NORTH,
    attacker_position=attacker_pos,
    target_position=target_pos,
)

# Flanking bonuses:
# - Side attack: +2 attack, +10% damage
# - Rear attack: +4 attack, +25% damage

Ranged Combat

Ranged Weapon Types

from maid_classic_rpg.models.combat.ranged import RangedWeaponType

# Weapon types with different characteristics
SHORTBOW = RangedWeaponType(
    name="Shortbow",
    range_rooms=3,
    reload_time=0.0,  # No reload
    base_damage=6,
)

CROSSBOW = RangedWeaponType(
    name="Crossbow",
    range_rooms=4,
    reload_time=3.0,  # Must reload after each shot
    base_damage=10,
)

Range and Penalties

Distance Accuracy
Same room Normal
1 room -2
2 rooms -4
3+ rooms -6

Reload Mechanics

# Check if reloading
if ranged_system.is_reloading(character.id):
    remaining = ranged_system.get_reload_remaining(character.id)
    print(f"Reloading... {remaining:.1f}s remaining")

# Force reload
ranged_system.force_reload(character.id, weapon_type)

Ranged Commands

shoot <target>     - Fire at target
reload             - Reload weapon
aim <target> [location]  - Aim at specific body part

Status Effects

Effect Types

Effect Description
stun Cannot act
slow Reduced action speed
bleed Damage over time
poison Damage over time (can stack)
burn Fire damage over time
freeze Slowed + reduced defense
blind Reduced accuracy
silence Cannot cast spells

Creating Status Effects

from maid_classic_rpg.systems.combat.effects import (
    StatusEffect,
    StatusEffectType,
    StatusEffectManager,
)

# Create a bleed effect
bleed = StatusEffect(
    effect_type=StatusEffectType.BLEED,
    source_id=attacker.id,
    duration=10.0,  # seconds
    damage_per_tick=5,
    tick_interval=2.0,  # Damage every 2 seconds
    stacks=1,
    max_stacks=5,
)

# Apply effect
effect_manager = engine.world.systems.get(StatusEffectManager)
effect_manager.apply_effect(target.id, bleed)

Effect Management

# Check for effect
if effect_manager.has_effect(target.id, StatusEffectType.STUN):
    print("Target is stunned!")

# Get effect details
stun = effect_manager.get_effect(target.id, StatusEffectType.STUN)
if stun:
    print(f"Stunned for {stun.remaining_duration:.1f}s")

# Remove effect
effect_manager.remove_effect(target.id, StatusEffectType.POISON)

# Clear all effects
effect_manager.clear_effects(target.id)

Creating Combat Abilities

Example: Power Strike

from maid_classic_rpg.models.abilities import Ability, AbilityType, TargetType

@ability_handler("power_strike")
async def handle_power_strike(
    ctx: AbilityContext,
    ability: Ability,
    target: Entity,
) -> AbilityResult:
    """Execute the Power Strike ability."""
    # Check stamina
    if ctx.user.stamina < ability.stamina_cost:
        return AbilityResult.failure("Not enough stamina!")

    # Deduct cost
    ctx.user.stamina -= ability.stamina_cost

    # Calculate damage (1.5x multiplier)
    melee_system = ctx.world.systems.get(MeleeCombatSystem)
    result = await melee_system.resolve_attack(
        attacker=ctx.user,
        target=target,
        weapon=ctx.weapon,
        damage_multiplier=1.5,
    )

    # Apply bonus stun effect
    if result.hit and random.random() < 0.3:  # 30% chance
        effect_manager = ctx.world.systems.get(StatusEffectManager)
        effect_manager.apply_effect(target.id, StatusEffect(
            effect_type=StatusEffectType.STUN,
            source_id=ctx.user.id,
            duration=2.0,
        ))
        result.add_message(f"{target.name} is stunned!")

    return AbilityResult.success(result)

Example: Healing Spell

@ability_handler("heal")
async def handle_heal(
    ctx: AbilityContext,
    ability: Ability,
    target: Entity,
) -> AbilityResult:
    """Cast a healing spell."""
    # Check mana
    if ctx.user.mana < ability.mana_cost:
        return AbilityResult.failure("Not enough mana!")

    # Deduct cost
    ctx.user.mana -= ability.mana_cost

    # Calculate healing
    base_heal = 20
    intelligence_bonus = (ctx.user.intelligence - 10) // 2
    heal_amount = base_heal + intelligence_bonus + roll_dice("2d6")

    # Apply healing
    target_health = target.get(HealthComponent)
    old_hp = target_health.current_hp
    target_health.current_hp = min(
        target_health.max_hp,
        target_health.current_hp + heal_amount,
    )
    actual_heal = target_health.current_hp - old_hp

    return AbilityResult.success(
        message=f"You heal {target.name} for {actual_heal} HP!",
        effects=[("heal", actual_heal)],
    )

Example: Area Attack

@ability_handler("whirlwind")
async def handle_whirlwind(
    ctx: AbilityContext,
    ability: Ability,
    targets: list[Entity],  # Area abilities get multiple targets
) -> AbilityResult:
    """Spin attack hitting all adjacent enemies."""
    if ctx.user.stamina < ability.stamina_cost:
        return AbilityResult.failure("Not enough stamina!")

    ctx.user.stamina -= ability.stamina_cost

    melee_system = ctx.world.systems.get(MeleeCombatSystem)
    results = []

    # Attack each target with reduced damage
    for target in targets:
        result = await melee_system.resolve_attack(
            attacker=ctx.user,
            target=target,
            weapon=ctx.weapon,
            damage_multiplier=0.7,  # 70% damage per target
        )
        results.append(result)

    total_damage = sum(r.damage_dealt for r in results if r.hit)
    return AbilityResult.success(
        message=f"Whirlwind hits {len([r for r in results if r.hit])} targets for {total_damage} total damage!",
        sub_results=results,
    )

Boss Encounters

Boss Entity Setup

# Create boss entity
dragon = world.create_entity()

dragon.add(DescriptionComponent(
    name="Scorax the Fire Drake",
    short_desc="A massive red dragon with scales like burning coals",
    keywords=["dragon", "scorax", "drake"],
))

dragon.add(HealthComponent(
    max_hp=5000,
    current_hp=5000,
))

dragon.add(CombatStatsComponent(
    level=20,
    strength=24,
    dexterity=14,
    constitution=22,
    attack_bonus=15,
    armor_class=22,
))

dragon.add(Resistances({
    DamageType.FIRE: 0.0,      # Immune
    DamageType.COLD: 1.5,      # Vulnerable
}))

dragon.add(BossComponent(
    phase_thresholds=[0.75, 0.50, 0.25],  # HP percentages
    enrage_threshold=0.10,
    special_abilities=["fire_breath", "tail_sweep", "wing_buffet"],
))

Boss Phases

from maid_classic_rpg.systems.boss import BossSystem, BossPhase

# Define phase behaviors
phases = {
    BossPhase.PHASE_1: {  # 100% - 75% HP
        "abilities": ["fire_breath", "claw_attack"],
        "ability_cooldowns": {"fire_breath": 30.0},
        "attack_speed": 1.0,
    },
    BossPhase.PHASE_2: {  # 75% - 50% HP
        "abilities": ["fire_breath", "tail_sweep", "summon_dragonlings"],
        "ability_cooldowns": {"fire_breath": 20.0},
        "attack_speed": 1.2,
    },
    BossPhase.PHASE_3: {  # 50% - 25% HP
        "abilities": ["fire_breath", "wing_buffet", "flame_aura"],
        "ability_cooldowns": {"fire_breath": 15.0},
        "attack_speed": 1.5,
    },
    BossPhase.ENRAGED: {  # Below 25% HP
        "abilities": ["fire_breath", "meteor_swarm", "berserk"],
        "ability_cooldowns": {"fire_breath": 10.0},
        "attack_speed": 2.0,
        "damage_multiplier": 1.5,
    },
}

Boss Special Abilities

@boss_ability("fire_breath")
async def fire_breath(boss: Entity, targets: list[Entity]) -> BossAbilityResult:
    """Breathe fire in a cone, hitting multiple targets."""
    messages = []
    effect_manager = world.systems.get(StatusEffectManager)

    for target in targets:
        # Calculate damage based on distance from center
        damage = roll_dice("6d8")

        # Apply fire damage
        damage_system.apply_damage(target.id, DamageInstance(
            amount=damage,
            damage_type=DamageType.FIRE,
            source_id=boss.id,
        ))

        # Apply burn effect
        effect_manager.apply_effect(target.id, StatusEffect(
            effect_type=StatusEffectType.BURN,
            source_id=boss.id,
            duration=10.0,
            damage_per_tick=10,
        ))

        messages.append(f"{target.name} is engulfed in flames for {damage} damage!")

    return BossAbilityResult(
        success=True,
        messages=messages,
        cooldown=20.0,
    )

Boss AI

class DragonBossAI:
    """AI controller for the dragon boss."""

    def __init__(self, boss_entity: Entity):
        self.boss = boss_entity
        self.phase = BossPhase.PHASE_1
        self.ability_cooldowns = {}

    async def think(self, combatants: list[Entity]) -> BossAction:
        """Determine the boss's next action."""
        # Update phase based on HP
        self._update_phase()

        # Check for available special abilities
        available = self._get_available_abilities()

        # Prioritize abilities based on situation
        if self._should_use_ability("fire_breath", combatants):
            return BossAction.use_ability("fire_breath", self._get_clustered_targets())

        if self.phase >= BossPhase.PHASE_2 and len(combatants) > 3:
            if "tail_sweep" in available:
                return BossAction.use_ability("tail_sweep", combatants)

        if self.phase == BossPhase.ENRAGED:
            if "berserk" in available:
                return BossAction.use_ability("berserk", [self.boss])

        # Default: attack highest threat
        target = self._get_highest_threat_target(combatants)
        return BossAction.attack(target)

    def _update_phase(self):
        hp_percent = self.boss.health.current_hp / self.boss.health.max_hp
        if hp_percent <= 0.10:
            self.phase = BossPhase.ENRAGED
        elif hp_percent <= 0.25:
            self.phase = BossPhase.PHASE_3
        elif hp_percent <= 0.50:
            self.phase = BossPhase.PHASE_2
        else:
            self.phase = BossPhase.PHASE_1

Combat Commands Reference

Basic Combat

Command Description
kill <target> Attack target (aliases: attack, hit, k)
flee Attempt to escape combat (alias: fl)
consider <target> Evaluate target difficulty (alias: con)

Tactical

Command Description
position <front/center/back> Set combat position
face <direction> Set facing direction
aim <target> [location] Aim at body part

Ranged

Command Description
shoot <target> Fire ranged weapon (alias: fire)
reload Reload ranged weapon

Support

Command Description
rescue <ally> Pull aggro from ally
rest Sit down for faster regen (alias: sit)
sleep Maximum regen (vulnerable)
wake Stand up (alias: stand)

Death

Command Description
respawn Return to life at bind point
retrieve [item] Loot your corpse