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