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 |