Skip to content

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:

  1. Components: HealthComponent, AttackComponent, DefenseComponent
  2. Systems: CombatSystem for processing attacks and damage
  3. Commands: attack command for initiating combat
  4. Events: DamageDealtEvent, EntityDeathEvent for system communication

Prerequisites

Before starting this tutorial, you should:

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, DamageDealtEvent, EntityDeathEvent

__all__ = ["AttackRequestEvent", "DamageDealtEvent", "EntityDeathEvent"]

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.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

        return [CombatSystem(world)]

    def get_events(self) -> list[type[Event]]:
        """Return event types defined by this pack."""
        from .events import AttackRequestEvent, DamageDealtEvent, EntityDeathEvent

        return [AttackRequestEvent, DamageDealtEvent, EntityDeathEvent]

    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

    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."""

from dataclasses import dataclass, field
from uuid import UUID, uuid4

import pytest

from maid_engine.core.world import World
from maid_stdlib.components import DescriptionComponent, HealthComponent, PositionComponent


@pytest.fixture
def world() -> World:
    """Create a fresh World for testing."""
    return World()


@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:

# From the maid-combat-system directory
uv sync

# Or with pip
pip install -e ".[dev]"

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:

  1. Created the project structure for a combat system content pack
  2. Configured pyproject.toml with dependencies and entry points
  3. Created the main CombatSystemPack class
  4. Set up testing infrastructure with pytest fixtures
  5. Verified the installation works

Next Steps

In Part 2: Components, you will create the combat-related components:

  • AttackComponent for offensive stats
  • DefenseComponent for defensive stats
  • Methods for damage calculation

Continue to Part 2: Components