Combat Systems Guide¶
This guide covers MAID's combat systems, from basic attacks to complex boss encounters with tactical positioning.
Table of Contents¶
- Overview
- Combat System Architecture
- Damage Calculation
- Skills and Abilities
- Tactical Positioning
- Ranged Combat
- Status Effects
- Creating Combat Abilities
- Boss Encounters
- Combat Commands Reference
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:
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 onCharacterStatsComponent, level/class/race onCharacterInfoComponent, and derived combat values (attack, defense, accuracy, evasion, crit) on the stdlibCombatComponent.
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
Resistancescomponent or per-type resistance multiplier map. Mitigation is armor-based (viacalculate_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_flankinghelper; useget_attack_angle+get_attack_angle_accuracy_bonus.FacingDirectionuses the 8 compass directions (NORTH,NORTHEAST, …), and lives inmodels.combat.grid_position.Limitation — facing is not wired into normal attacks. The attack-angle bonus is only applied inside
MeleeCombatSystem.resolve_attack()when bothattacker_grid_stateandtarget_grid_state(carrying.facing) are passed in. The built-inkillcommand does not supply grid state — it callsresolve_attackwith only row/columnCombatPositionand zone — so thefacecommand and the flanking/backstab accuracy bonus currently have no effect in normal play.get_attack_angle/get_attack_angle_accuracy_bonusare 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 —
RangedCombatComponentis inert. The class (and itscreate_ranged_component()factory) exists, but nothing in the shipped code attaches it to entities andRangedCombatSystem.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 returnsTrueand_consume_ammo()is a no-op, so theammo_count/max_ammofields have no effect. AddingRangedCombatComponentto 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:
StatusEffectManageractively applies only the damage-over-time effects —bleed,poison,burn, anddisease— which tick damage each update via theDamageSystem. Control/debuff effects such asstun,slow,freeze,blind,daze,silence, andimmobilizeare tracked (you canadd_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 checkshas_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/AbilityEffectdescribe 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_handlerdecorator,AbilityContext,AbilityResult, or ability-use command (usehandles consumable items only, not abilities), andAbilityLoaderis not wired into the pack by default. To make abilities do something you must implement the execution yourself — typically aSystem/command that, on activation, applies eachAbilityEffectthrough theDamageSystemandStatusEffectManagershown 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_abilitydecorator,BossAbilityResult, orBossAction. 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, andCombatComponent. - A custom
System(subclassmaid_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 theStatusEffectManagerandDamageSystemshown 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 |
Related Documentation¶
- Building Commands Reference - Creating combat areas
- Creating NPCs Guide - NPC combat configuration
- Command System Guide - Adding custom combat commands