Combat System Tutorial - Part 1: Setup¶
In this tutorial series, you will build a complete combat system content pack for MAID. By the end, you will have a fully functional combat system with attack commands, damage calculation, and death handling.
What We're Building¶
Our combat system will include:
- Components: HealthComponent, AttackComponent, DefenseComponent
- Systems: CombatSystem for processing attacks and damage
- Commands:
attackcommand for initiating combat - Events: CombatDamageEvent, CombatDeathEvent for system communication
Prerequisites¶
Before starting this tutorial, you should:
- Have MAID installed (see Installation)
- Understand Core Concepts
- Have basic familiarity with Python async/await
Project Structure¶
Create a new directory for your content pack:
maid-combat-system/
src/
maid_combat_system/
__init__.py
pack.py
components/
__init__.py
combat.py
systems/
__init__.py
combat_system.py
commands/
__init__.py
combat_commands.py
events/
__init__.py
combat_events.py
tests/
__init__.py
conftest.py
test_components.py
test_system.py
test_commands.py
pyproject.toml
README.md
Step 1: Create the Project¶
Create the directory structure:
mkdir -p maid-combat-system/src/maid_combat_system/{components,systems,commands,events}
mkdir -p maid-combat-system/tests
cd maid-combat-system
Step 2: Configure pyproject.toml¶
Create pyproject.toml with all necessary dependencies:
[project]
name = "maid-combat-system"
version = "0.1.0"
description = "A combat system content pack for MAID"
readme = "README.md"
requires-python = ">=3.12"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
license = { text = "MIT" }
keywords = ["maid", "mud", "combat", "rpg"]
dependencies = [
"maid-engine>=0.1.0",
"maid-stdlib>=0.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"pytest-cov>=4.0",
"ruff>=0.3.0",
"mypy>=1.8",
]
[project.entry-points."maid.content_packs"]
combat-system = "maid_combat_system:CombatSystemPack"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/maid_combat_system"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.mypy]
python_version = "3.12"
strict = true
Step 3: Create Package Init Files¶
Create the main package __init__.py:
# src/maid_combat_system/__init__.py
"""Combat system content pack for MAID.
This pack provides a complete combat system including:
- Health, Attack, and Defense components
- Combat processing system
- Attack commands
- Combat-related events
"""
from .pack import CombatSystemPack
__all__ = ["CombatSystemPack"]
__version__ = "0.1.0"
Create init files for subpackages:
# src/maid_combat_system/components/__init__.py
"""Combat-related components."""
from .combat import AttackComponent, DefenseComponent
__all__ = ["AttackComponent", "DefenseComponent"]
# src/maid_combat_system/systems/__init__.py
"""Combat systems."""
from .combat_system import CombatSystem
__all__ = ["CombatSystem"]
# src/maid_combat_system/commands/__init__.py
"""Combat commands."""
from .combat_commands import register_commands
__all__ = ["register_commands"]
# src/maid_combat_system/events/__init__.py
"""Combat events."""
from .combat_events import AttackRequestEvent, CombatDamageEvent, CombatDeathEvent
__all__ = ["AttackRequestEvent", "CombatDamageEvent", "CombatDeathEvent"]
Step 4: Create the Content Pack Class¶
The content pack class is the entry point that ties everything together:
# src/maid_combat_system/pack.py
"""Combat system content pack."""
from __future__ import annotations
from typing import TYPE_CHECKING
from maid_engine.plugins.protocol import BaseContentPack
if TYPE_CHECKING:
from maid_engine.commands.registry import CommandRegistry, LayeredCommandRegistry
from maid_engine.core.ecs.system import System
from maid_engine.core.engine import GameEngine
from maid_engine.core.events import Event
from maid_engine.core.world import World
from maid_engine.persistence.registry import ComponentRegistry
from maid_engine.plugins.manifest import ContentPackManifest
from maid_engine.storage.document_store import DocumentStore
AnyCommandRegistry = CommandRegistry | LayeredCommandRegistry
class CombatSystemPack(BaseContentPack):
"""Combat system content pack.
Provides a complete combat system for MAID games including:
- Attack and Defense components
- Combat processing system
- Attack commands
- Combat events (damage, death)
"""
@property
def manifest(self) -> ContentPackManifest:
"""Return pack manifest with metadata."""
from maid_engine.plugins.manifest import ContentPackManifest
return ContentPackManifest(
name="combat-system",
version="0.1.0",
display_name="Combat System",
description="A complete combat system for MAID games",
dependencies={"stdlib": ">=0.1.0"},
provides=["combat", "attack", "defense"],
requires=["ecs", "event-bus"],
authors=["Your Name"],
license="MIT",
keywords=["combat", "rpg", "attack", "damage"],
)
def get_dependencies(self) -> list[str]:
"""Return list of required content pack names."""
return ["stdlib"]
def get_systems(self, world: World) -> list[System]:
"""Return systems provided by this pack."""
from .systems import CombatSystem
from .systems.combat_messages import CombatMessageSystem
# CombatMessageSystem (built in Part 4) subscribes to combat events and
# sends player feedback. It MUST be returned here — a system that is
# only defined but never registered never has startup()/update() called,
# so its event subscriptions never fire and players see no messages.
return [CombatSystem(world), CombatMessageSystem(world)]
def get_events(self) -> list[type[Event]]:
"""Return event types defined by this pack."""
from .events import AttackRequestEvent, CombatDamageEvent, CombatDeathEvent
return [AttackRequestEvent, CombatDamageEvent, CombatDeathEvent]
def register_commands(self, registry: AnyCommandRegistry) -> None:
"""Register commands provided by this pack."""
from .commands import register_commands
register_commands(registry, pack_name=self.manifest.name)
def register_document_schemas(self, store: DocumentStore) -> None:
"""Register document schemas for persistence."""
# Combat system doesn't need persistence in this basic version
pass
def register_component_types(self, registry: ComponentRegistry) -> None:
"""Register custom components so persisted entities can be restored.
AttackComponent and DefenseComponent are custom types. If they are
not registered with the persistence ComponentRegistry, any entity
that carries them will fail to deserialize on startup with an
unknown-component error.
"""
from .components import AttackComponent, DefenseComponent
registry.register(AttackComponent, pack_name=self.manifest.name)
registry.register(DefenseComponent, pack_name=self.manifest.name)
async def on_load(self, engine: GameEngine) -> None:
"""Called when the pack is loaded."""
print(f"Combat System Pack v{self.manifest.version} loaded!")
async def on_unload(self, engine: GameEngine) -> None:
"""Called when the pack is unloaded."""
print("Combat System Pack unloaded!")
Step 5: Set Up Testing Infrastructure¶
Create the pytest configuration file:
# tests/conftest.py
"""Pytest configuration and fixtures for combat system tests."""
import os
# Settings validation requires MAID_DEBUG=true to allow the default insecure
# secret key during tests. Set this before importing Settings.
os.environ.setdefault("MAID_DEBUG", "true")
from dataclasses import dataclass, field
from uuid import UUID, uuid4
import pytest
from maid_engine.config.settings import Settings
from maid_engine.core.world import World
from maid_stdlib.components import DescriptionComponent, HealthComponent, PositionComponent
@pytest.fixture
def settings() -> Settings:
"""Test settings (MAID_DEBUG enables the default insecure key)."""
return Settings()
@pytest.fixture
def world(settings: Settings) -> World:
"""Create a fresh World for testing."""
return World(settings)
@pytest.fixture
def room_id() -> UUID:
"""A room UUID for testing."""
return uuid4()
@pytest.fixture
def player_entity(world: World, room_id: UUID):
"""Create a player entity with standard components."""
player = world.entities.create()
player.add(PositionComponent(room_id=room_id))
player.add(HealthComponent(current=100, maximum=100))
player.add(DescriptionComponent(name="Hero", short_desc="A brave hero"))
player.add_tag("player")
return player
@pytest.fixture
def enemy_entity(world: World, room_id: UUID):
"""Create an enemy entity for combat testing."""
enemy = world.entities.create()
enemy.add(PositionComponent(room_id=room_id))
enemy.add(HealthComponent(current=50, maximum=50))
enemy.add(DescriptionComponent(name="Goblin", short_desc="A sneaky goblin"))
enemy.add_tag("hostile")
return enemy
@dataclass
class MockSession:
"""Mock session for testing commands."""
messages: list[str] = field(default_factory=list)
player_id: UUID | None = None
async def send(self, message: str) -> None:
"""Record a message sent to the session."""
self.messages.append(message)
def clear(self) -> None:
"""Clear recorded messages."""
self.messages.clear()
def get_last_message(self) -> str | None:
"""Get the most recent message."""
return self.messages[-1] if self.messages else None
@pytest.fixture
def mock_session(player_entity) -> MockSession:
"""Create a mock session for command testing."""
return MockSession(player_id=player_entity.id)
Step 6: Verify Installation¶
Install your package in development mode:
Verify the structure is correct:
# Check package can be imported
python -c "from maid_combat_system import CombatSystemPack; print('OK')"
Configuration Options¶
Your combat system can be configured through environment variables or a config file. Here are some settings you might want to support:
# Example configuration (optional)
COMBAT_SETTINGS = {
"base_hit_chance": 80, # Base percentage to hit
"critical_multiplier": 2.0, # Critical hit damage multiplier
"default_critical_chance": 5, # Default crit chance percentage
"death_message_enabled": True, # Show death messages
"combat_log_enabled": True, # Log combat events
}
Summary¶
In this part, you:
- Created the project structure for a combat system content pack
- Configured
pyproject.tomlwith dependencies and entry points - Created the main
CombatSystemPackclass - Set up testing infrastructure with pytest fixtures
- Verified the installation works
Next Steps¶
In Part 2: Components, you will create the combat-related components:
AttackComponentfor offensive statsDefenseComponentfor defensive stats- Methods for damage calculation
Continue to Part 2: Components