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,
)

You normally do not register these systems by hand. The ClassicRPGContentPack returns them from get_systems(), so loading the pack registers all combat systems automatically:

engine.load_content_pack(ClassicRPGContentPack())
await engine.start()  # get_systems() registers the combat systems

If you are wiring a system manually (for example in a test), use the world's system manager:

engine.world.systems.register(MeleeCombatSystem(engine.world))

Combat Components

Entities need these components for combat:

from maid_stdlib.components import HealthComponent
from maid_classic_rpg.components import (
    CharacterInfoComponent,
    CharacterStatsComponent,
    CombatComponent,
)

# Health pool
entity.add(HealthComponent(
    maximum=100,
    current=100,
))

# D&D-style ability scores
entity.add(CharacterStatsComponent(
    strength=14,
    dexterity=12,
    constitution=12,
))

# Level / class / race live on CharacterInfoComponent
entity.add(CharacterInfoComponent(
    name="Guard",
    race="human",
    character_class="warrior",
    level=5,
))

# Derived combat statistics
entity.add(CombatComponent(
    attack_power=12,
    defense=8,
    accuracy=80,
    evasion=10,
    critical_chance=5,
))

There is no single CombatStatsComponent. Ability scores live on CharacterStatsComponent, level/class/race on CharacterInfoComponent, and derived combat values (attack, defense, accuracy, evasion, crit) on the stdlib CombatComponent.


Damage Calculation

Attack Resolution

Attack resolution is handled internally by MeleeCombatSystem.resolve_attack(). The steps below are simplified pseudocode that illustrate the algorithm — the attribute names (attack_bonus, armor_class, roll_d20) are illustrative, not a public API:

# Conceptual pseudocode (not a runnable recipe)
# 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
unholy Shadow/necrotic damage Dark magic
arcane Raw magical damage Arcane spells

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_penetration=0.0,
)

Armor and Mitigation

Damage is reduced by armor. The engine calculates the reduction with calculate_armor_reduction, which weights armor by damage type and applies any armor penetration from the attack:

from maid_classic_rpg.models.combat.damage import (
    DamageType,
    calculate_armor_reduction,
)

reduction = calculate_armor_reduction(
    damage=20,
    armor_value=8,
    damage_type=DamageType.SLASHING,
    armor_penetration=0.0,  # 0.0-1.0 fraction of armor ignored
)
final_damage = max(0, 20 - reduction)

Armor effectiveness per damage type is defined by the internal ARMOR_EFFECTIVENESS table in models/combat/damage.py. DamageType.TRUE ignores armor entirely.

Not implemented: there is no Resistances component or per-type resistance multiplier map. Mitigation is armor-based (via calculate_armor_reduction) plus body-part and status-effect modifiers. Per-type resistance multipliers would need to be added to the damage model before they could be used.


Skills and Abilities

Ability Structure

from maid_classic_rpg.models.abilities import (
    AbilityDefinition,
    AbilityEffect,
    AbilityType,
    ResourceType,
)
from maid_classic_rpg.models.combat.damage import DamageType, StatusEffectType

power_strike = AbilityDefinition(
    internal_name="power_strike",
    name="Power Strike",
    description="A devastating overhead blow",
    class_type="warrior",       # required: the class that learns it
    level_required=3,
    ability_type=AbilityType.ATTACK,
    requires_target=True,
    resource_type=ResourceType.STAMINA,
    resource_cost=15,
    cooldown=10.0,              # seconds
    effects=[
        AbilityEffect(
            effect_type="damage",
            dice=2,
            sides=6,
            damage_type=DamageType.SLASHING,
        ),
        AbilityEffect(
            effect_type="status",
            status_effect=StatusEffectType.STUN,
            duration=2.0,
        ),
    ],
)

Ability Types

Type Description
ATTACK Direct damage ability
DEFENSE Defensive ability
UTILITY Non-combat ability
PASSIVE Always-on ability
TOGGLE Can be toggled on/off

Targeting

There is no TargetType enum. An ability declares whether it needs a target with the requires_target: bool flag, and each AbilityEffect names who it applies to via its target string (defaults to "target"; use "self" for self-buffs).

Skill System

Skills improve with use:

from maid_classic_rpg.models.skills import SkillCategory, SkillDefinition

melee_skill = SkillDefinition(
    internal_name="melee",
    name="Melee Combat",
    category=SkillCategory.COMBAT,
    description="Proficiency with melee weapons",
    max_level=100,
    base_difficulty=1.0,  # higher = slower to improve
)

# Skills are a standalone progression/check system. SkillSystem provides:
#   - learn_skill / get_skill_level / add_experience / train_skill
#   - skill_check(...) and opposed_check(...) — generic d20-style checks against
#     a difficulty, optionally modified by a character stat
#
# They are NOT wired into combat: melee/ranged attack resolution (DamageSystem,
# RangedCombatSystem) does not read SkillsComponent, so skill level does not
# currently affect hit chance or damage, and skills do not unlock abilities.
# Use skill_check()/opposed_check() for non-combat actions (lockpicking,
# persuasion, etc.). Feeding skills into combat would require custom code.

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

Positioning affects accuracy and reachability, not flat per-row damage or defense percentages. get_position_accuracy_modifier(attacker_pos, target_pos) returns an accuracy modifier in the range −0.10 … +0.15:

Situation Accuracy modifier
Attacking a FRONT-row target from the REAR row +15% (backstab)
Attacking a CENTER-column target from a side column (LEFT/RIGHT) +5% (flank)
Attacking a REAR-row target from the FRONT row −10% (awkward angle)
Otherwise 0%

Reachability is governed by weapon reach and combat zone (can_reach_position, CombatZone), and some weapons get a position-specific bonus via get_weapon_position_bonus (e.g. reach weapons). There is no implemented table that scales raw damage or defense by row alone.

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 the side or behind grants an accuracy bonus (not a damage bonus). Use get_attack_angle to classify the attack relative to the target's facing, then get_attack_angle_accuracy_bonus for the modifier:

from maid_classic_rpg.models.combat.facing import (
    AttackAngle,
    get_attack_angle,
    get_attack_angle_accuracy_bonus,
)
from maid_classic_rpg.models.combat.grid_position import FacingDirection

# Attacker at (0, 0); target at (0, 1) facing north (attacker is behind it)
angle = get_attack_angle(
    attacker_pos=(0, 0),
    target_pos=(0, 1),
    target_facing=FacingDirection.NORTH,
)
accuracy_bonus = get_attack_angle_accuracy_bonus(angle)

# Attack-angle accuracy bonuses (hit chance, not damage):
# - AttackAngle.FRONT: +0%
# - AttackAngle.SIDE:  +10%  (flanking)
# - AttackAngle.REAR:  +15%  (backstab)

There is no calculate_flanking helper; use get_attack_angle + get_attack_angle_accuracy_bonus. FacingDirection uses the 8 compass directions (NORTH, NORTHEAST, …), and lives in models.combat.grid_position.

Limitation — facing is not wired into normal attacks. The attack-angle bonus is only applied inside MeleeCombatSystem.resolve_attack() when both attacker_grid_state and target_grid_state (carrying .facing) are passed in. The built-in kill command does not supply grid state — it calls resolve_attack with only row/column CombatPosition and zone — so the face command and the flanking/backstab accuracy bonus currently have no effect in normal play. get_attack_angle/get_attack_angle_accuracy_bonus are usable as library helpers if you write a command or system that supplies the grid state yourself.


Ranged Combat

Ranged Weapon Types

from maid_classic_rpg.models.combat.ranged import (
    RangedCombatComponent,
    RangedWeaponType,
)

# RangedWeaponType is an enum of weapon categories:
#   THROWN_LIGHT, THROWN_HEAVY, SHORTBOW, LONGBOW, CROSSBOW, HEAVY_CROSSBOW,
#   PISTOL, RIFLE, SHOTGUN, SPELL, WAND, STAFF

# RangedCombatComponent is a defined model with these fields, but see the
# limitation note below — the RangedCombatSystem does NOT read it.
shortbow = RangedCombatComponent(
    weapon_type=RangedWeaponType.SHORTBOW,
    range_rooms=3,
    accuracy_falloff=0.2,   # accuracy penalty per room of distance
    reload_time=0.0,        # no reload
)

crossbow = RangedCombatComponent(
    weapon_type=RangedWeaponType.CROSSBOW,
    range_rooms=4,
    reload_time=3.0,        # must reload after each shot
)

Limitation — RangedCombatComponent is inert. The class (and its create_ranged_component() factory) exists, but nothing in the shipped code attaches it to entities and RangedCombatSystem.resolve_attack() never reads it. The system takes the weapon, distance, and line-of-sight as call parameters and tracks reload state in an in-memory dict keyed by entity ID (self._reload_timers). It does not consult a component on the attacker. Ammunition is likewise unenforced: _has_ammo() always returns True and _consume_ammo() is a no-op, so the ammo_count/max_ammo fields have no effect. Adding RangedCombatComponent to an entity changes nothing; treat it as a data model awaiting integration, not a working capability toggle.

Range and Penalties

Ranged accuracy is a probability (0–1) reduced by a fractional distance penalty, not a flat to-hit number. calculate_ranged_accuracy(base_accuracy, distance, weapon_type, ...) computes:

distance_penalty = accuracy_falloff * (distance - 1)   # 0 at the same/1st room
accuracy = base_accuracy - distance_penalty - (obstruction * 0.5)
accuracy += elevation_bonus * 0.15
accuracy = clamp(accuracy, 0.0, 1.0)

accuracy_falloff comes from the weapon (per-RangedWeaponType, default 0.2). So, for a weapon with accuracy_falloff=0.2 and base_accuracy=0.8: same/first room → 0.8, 2 rooms → 0.6, 3 rooms → 0.4. can_attack_at_range(weapon_type, distance, has_los) rejects shots beyond the weapon's maximum range or without line of sight.

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

What is actually enforced: StatusEffectManager actively applies only the damage-over-time effects — bleed, poison, burn, and disease — which tick damage each update via the DamageSystem. Control/debuff effects such as stun, slow, freeze, blind, daze, silence, and immobilize are tracked (you can add_effect, has_effect, get_effects, remove_effect), but no shipped system reads them to prevent actions, slow movement, or modify accuracy. The "Description" column below is the intended meaning of each effect; enforcing the non-DOT effects requires custom code that checks has_effect(...) in your own command/combat logic.

Effect Types

Effect Description Enforced by the pack?
bleed Damage over time Yes (DOT tick)
poison Damage over time (can stack) Yes (DOT tick)
burn Fire damage over time Yes (DOT tick)
disease Damage over time / debuff Yes (DOT tick)
stun Cannot act No — tracked only
slow Reduced action speed No — tracked only
freeze Slowed + reduced defense No — tracked only
blind Reduced accuracy No — tracked only
silence Cannot cast spells No — tracked only

Creating Status Effects

from maid_classic_rpg.systems.combat import StatusEffectManager
from maid_classic_rpg.models.combat.damage import StatusEffectType

# The manager builds and tracks the StatusEffect for you via add_effect().
# `target` is a Character domain object (the combat systems resolve these from
# entity IDs); source_id is the attacker's entity UUID. Per-effect defaults for
# duration and damage-per-tick come from the internal effect tables.
effect_manager = engine.world.systems.get(StatusEffectManager)

bleed = effect_manager.add_effect(
    target,                     # Character
    StatusEffectType.BLEED,
    source_id=attacker_id,      # UUID of the source entity
    duration=10.0,              # seconds (omit to use the effect's default)
    stacks=1,
)

Effect Management

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

# Inspect active effects
for effect in effect_manager.get_effects(target):
    if effect.effect_type == StatusEffectType.STUN:
        print(f"Stunned for {effect.duration:.1f}s")

# Remove a single effect type
effect_manager.remove_effect(target, StatusEffectType.POISON)

# Remove all effects
effect_manager.remove_all_effects(target)

Creating Combat Abilities

Abilities in the Classic RPG pack are data, not decorated handler functions. You define an AbilityDefinition (see Skills and Abilities) with a list of AbilityEffects. Characters track what they know through AbilitiesComponent and LearnedAbility (quest rewards can grant abilities into that component).

No runtime executor ships in the pack. AbilityDefinition/AbilityEffect describe what an ability would do, but the Classic RPG pack does not currently register a system or command that reads a learned ability and applies its effects. There is no @ability_handler decorator, AbilityContext, AbilityResult, or ability-use command (use handles consumable items only, not abilities), and AbilityLoader is not wired into the pack by default. To make abilities do something you must implement the execution yourself — typically a System/command that, on activation, applies each AbilityEffect through the DamageSystem and StatusEffectManager shown earlier in this guide. Treat the definitions below as data schema, not working behavior.

Example: a self-heal ability expressed as data:

from maid_classic_rpg.models.abilities import (
    AbilityDefinition,
    AbilityEffect,
    AbilityType,
    ResourceType,
)

heal = AbilityDefinition(
    internal_name="minor_heal",
    name="Minor Heal",
    description="Restore a small amount of health",
    class_type="cleric",
    ability_type=AbilityType.DEFENSE,
    requires_target=True,
    resource_type=ResourceType.MANA,
    resource_cost=10,
    effects=[
        AbilityEffect(
            effect_type="heal",
            dice=2,
            sides=6,          # 2d6 healing
            target="target",
        ),
    ],
)

Boss Encounters

Design proposal — not implemented. The engine does not ship a boss framework. There is no BossComponent, BossSystem, BossPhase, @boss_ability decorator, BossAbilityResult, or BossAction. The material below describes a pattern you could build on top of the existing systems; it is not runnable against the current API.

A phase-based boss can be assembled from primitives that do exist:

  • A regular entity with HealthComponent, CharacterStatsComponent, and CombatComponent.
  • A custom System (subclass maid_engine.core.ecs.system.System) that reads the boss's HP each tick and changes behaviour at HP thresholds.
  • AbilityDefinitions for the boss's special moves, applied through the StatusEffectManager and DamageSystem shown earlier in this guide.

Implementing such a system is left as an exercise; treat any BossSystem-style API as aspirational until a boss framework is added to the pack.

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