Skip to content

Magic System Tutorial

This tutorial guides you through building a complete magic system content pack for MAID. You will learn how to create spell components, implement mana management, handle spell effects, and integrate with the combat system.

What We're Building

The magic system will include:

  • ManaComponent: Resource pool for casting spells
  • SpellComponent: Definition of individual spells
  • SpellbookComponent: Collection of learned spells
  • MagicSystem: Spell casting, mana regeneration, spell effects
  • Commands: cast, spells, learn commands
  • Effects: Damage, healing, buffs, debuffs

Prerequisites

Before starting this tutorial, you should:

  • Have completed the Combat System Tutorial
  • Understand MAID's ECS architecture
  • Be familiar with event-driven systems

Project Structure

maid-magic-system/
    src/
        maid_magic_system/
            __init__.py
            pack.py
            components/
                __init__.py
                mana.py
                spells.py
            systems/
                __init__.py
                magic_system.py
                mana_regen_system.py
            commands/
                __init__.py
                magic_commands.py
            events/
                __init__.py
                magic_events.py
            data/
                spells.py          # Spell definitions
    tests/
        conftest.py
        test_components.py
        test_system.py
        test_commands.py
    pyproject.toml

Part 1: Mana Component

The ManaComponent tracks magical energy:

# src/maid_magic_system/components/mana.py
"""Mana component for magical energy management."""

from typing import ClassVar

from maid_engine.core.ecs import Component
from pydantic import Field


class ManaComponent(Component):
    """Component for entities that use magical energy.

    Mana is consumed when casting spells and regenerates over time.
    Some effects may also restore or drain mana.

    Attributes:
        current: Current mana points
        maximum: Maximum mana points
        regeneration_rate: Mana regenerated per second
        casting_modifier: Bonus/penalty to spell power
    """

    component_type: ClassVar[str] = "ManaComponent"

    current: int = Field(default=100, ge=0, description="Current mana")
    maximum: int = Field(default=100, ge=1, description="Maximum mana")
    regeneration_rate: float = Field(
        default=2.0, ge=0.0, description="Mana per second"
    )
    casting_modifier: int = Field(
        default=0, description="Bonus to spell power"
    )

    # Tracking
    time_since_cast: float = Field(
        default=5.0, description="Seconds since last cast"
    )
    combat_regen_multiplier: float = Field(
        default=0.5, description="Regen rate multiplier in combat"
    )

    @property
    def percentage(self) -> float:
        """Get mana as a percentage (0-100)."""
        if self.maximum <= 0:
            return 0.0
        return (self.current / self.maximum) * 100.0

    @property
    def is_empty(self) -> bool:
        """Check if mana is depleted."""
        return self.current <= 0

    @property
    def is_full(self) -> bool:
        """Check if mana is at maximum."""
        return self.current >= self.maximum

    def can_cast(self, cost: int) -> bool:
        """Check if there is enough mana to cast a spell.

        Args:
            cost: The mana cost of the spell

        Returns:
            True if there is sufficient mana.
        """
        return self.current >= cost

    def consume(self, amount: int) -> bool:
        """Consume mana for a spell cast.

        Args:
            amount: Amount of mana to consume

        Returns:
            True if mana was successfully consumed.
        """
        if not self.can_cast(amount):
            return False
        self.current -= amount
        self.time_since_cast = 0.0
        return True

    def restore(self, amount: int) -> int:
        """Restore mana.

        Args:
            amount: Amount to restore

        Returns:
            Actual amount restored.
        """
        old_value = self.current
        self.current = min(self.maximum, self.current + amount)
        return self.current - old_value

    def drain(self, amount: int) -> int:
        """Drain mana (e.g., from enemy attack).

        Args:
            amount: Amount to drain

        Returns:
            Actual amount drained.
        """
        old_value = self.current
        self.current = max(0, self.current - amount)
        return old_value - self.current

Part 2: Spell Components

Define spell data structures:

# src/maid_magic_system/components/spells.py
"""Spell-related components."""

from enum import Enum
from typing import ClassVar
from uuid import UUID

from maid_engine.core.ecs import Component
from pydantic import Field


class SpellSchool(str, Enum):
    """Schools of magic."""
    EVOCATION = "evocation"      # Damage spells
    RESTORATION = "restoration"  # Healing spells
    ABJURATION = "abjuration"    # Protective spells
    ENCHANTMENT = "enchantment"  # Buff/debuff spells
    CONJURATION = "conjuration"  # Summoning spells
    DIVINATION = "divination"    # Information spells


class SpellTarget(str, Enum):
    """Valid spell targets."""
    SELF = "self"
    SINGLE = "single"
    AREA = "area"
    ALL_ENEMIES = "all_enemies"
    ALL_ALLIES = "all_allies"


class Spell:
    """Definition of a spell.

    This is not a component but a data class for spell definitions.
    Spells are stored in SpellbookComponent.
    """

    def __init__(
        self,
        id: str,
        name: str,
        description: str,
        school: SpellSchool,
        mana_cost: int,
        cooldown: float = 0.0,
        cast_time: float = 0.0,
        target_type: SpellTarget = SpellTarget.SINGLE,
        base_power: int = 0,
        level_requirement: int = 1,
        effects: list[dict] | None = None,
    ):
        self.id = id
        self.name = name
        self.description = description
        self.school = school
        self.mana_cost = mana_cost
        self.cooldown = cooldown
        self.cast_time = cast_time
        self.target_type = target_type
        self.base_power = base_power
        self.level_requirement = level_requirement
        self.effects = effects or []


class SpellbookComponent(Component):
    """Component that stores learned spells.

    Attributes:
        known_spells: List of spell IDs the entity knows
        spell_cooldowns: Remaining cooldown for each spell
        active_spell: Currently channeling spell (if any)
    """

    component_type: ClassVar[str] = "SpellbookComponent"

    known_spells: list[str] = Field(
        default_factory=list,
        description="IDs of known spells"
    )
    spell_cooldowns: dict[str, float] = Field(
        default_factory=dict,
        description="Remaining cooldowns"
    )
    active_spell: str | None = Field(
        default=None,
        description="Spell being channeled"
    )
    channel_progress: float = Field(
        default=0.0,
        description="Channel time accumulated"
    )

    def knows_spell(self, spell_id: str) -> bool:
        """Check if entity knows a spell."""
        return spell_id in self.known_spells

    def learn_spell(self, spell_id: str) -> bool:
        """Learn a new spell.

        Returns:
            True if spell was learned, False if already known.
        """
        if spell_id in self.known_spells:
            return False
        self.known_spells.append(spell_id)
        return True

    def forget_spell(self, spell_id: str) -> bool:
        """Forget a spell.

        Returns:
            True if spell was forgotten.
        """
        if spell_id not in self.known_spells:
            return False
        self.known_spells.remove(spell_id)
        return True

    def is_on_cooldown(self, spell_id: str) -> bool:
        """Check if a spell is on cooldown."""
        return self.spell_cooldowns.get(spell_id, 0.0) > 0.0

    def get_cooldown(self, spell_id: str) -> float:
        """Get remaining cooldown for a spell."""
        return self.spell_cooldowns.get(spell_id, 0.0)

    def start_cooldown(self, spell_id: str, duration: float) -> None:
        """Start a spell cooldown."""
        self.spell_cooldowns[spell_id] = duration

    def reduce_cooldowns(self, delta: float) -> None:
        """Reduce all cooldowns by time passed."""
        for spell_id in list(self.spell_cooldowns.keys()):
            self.spell_cooldowns[spell_id] -= delta
            if self.spell_cooldowns[spell_id] <= 0:
                del self.spell_cooldowns[spell_id]


class SpellEffectComponent(Component):
    """Component for active spell effects on an entity.

    Tracks buffs, debuffs, and damage-over-time effects.
    """

    component_type: ClassVar[str] = "SpellEffectComponent"

    effects: list[dict] = Field(
        default_factory=list,
        description="Active spell effects"
    )

    def add_effect(
        self,
        effect_type: str,
        power: int,
        duration: float,
        source_id: UUID | None = None,
        spell_id: str = "",
    ) -> None:
        """Add a spell effect."""
        self.effects.append({
            "type": effect_type,
            "power": power,
            "duration": duration,
            "remaining": duration,
            "source_id": str(source_id) if source_id else None,
            "spell_id": spell_id,
        })

    def remove_expired(self) -> list[dict]:
        """Remove and return expired effects."""
        expired = [e for e in self.effects if e["remaining"] <= 0]
        self.effects = [e for e in self.effects if e["remaining"] > 0]
        return expired

    def tick(self, delta: float) -> None:
        """Reduce all effect durations."""
        for effect in self.effects:
            effect["remaining"] -= delta

    def get_effects_by_type(self, effect_type: str) -> list[dict]:
        """Get all effects of a specific type."""
        return [e for e in self.effects if e["type"] == effect_type]

    def clear_effects(self) -> None:
        """Remove all effects."""
        self.effects.clear()

Part 3: Spell Registry

Create a spell registry with predefined spells:

# src/maid_magic_system/data/spells.py
"""Predefined spell definitions."""

from ..components.spells import Spell, SpellSchool, SpellTarget


# Evocation (Damage) Spells
FIREBALL = Spell(
    id="fireball",
    name="Fireball",
    description="Hurls a ball of fire at the target",
    school=SpellSchool.EVOCATION,
    mana_cost=25,
    cooldown=3.0,
    target_type=SpellTarget.SINGLE,
    base_power=30,
    level_requirement=1,
    effects=[
        {"type": "damage", "damage_type": "fire"},
    ],
)

LIGHTNING_BOLT = Spell(
    id="lightning_bolt",
    name="Lightning Bolt",
    description="Strikes the target with lightning",
    school=SpellSchool.EVOCATION,
    mana_cost=35,
    cooldown=5.0,
    target_type=SpellTarget.SINGLE,
    base_power=45,
    level_requirement=3,
    effects=[
        {"type": "damage", "damage_type": "lightning"},
    ],
)

ICE_STORM = Spell(
    id="ice_storm",
    name="Ice Storm",
    description="Unleashes a storm of ice on all enemies",
    school=SpellSchool.EVOCATION,
    mana_cost=50,
    cooldown=10.0,
    target_type=SpellTarget.ALL_ENEMIES,
    base_power=20,
    level_requirement=5,
    effects=[
        {"type": "damage", "damage_type": "cold"},
        {"type": "slow", "power": 30, "duration": 5.0},
    ],
)

# Restoration (Healing) Spells
HEAL = Spell(
    id="heal",
    name="Heal",
    description="Restores health to the target",
    school=SpellSchool.RESTORATION,
    mana_cost=20,
    cooldown=2.0,
    target_type=SpellTarget.SINGLE,
    base_power=25,
    level_requirement=1,
    effects=[
        {"type": "heal"},
    ],
)

GREATER_HEAL = Spell(
    id="greater_heal",
    name="Greater Heal",
    description="Powerfully restores health",
    school=SpellSchool.RESTORATION,
    mana_cost=45,
    cooldown=5.0,
    cast_time=2.0,  # Channeled spell
    target_type=SpellTarget.SINGLE,
    base_power=60,
    level_requirement=4,
    effects=[
        {"type": "heal"},
    ],
)

REGENERATION = Spell(
    id="regeneration",
    name="Regeneration",
    description="Gradually restores health over time",
    school=SpellSchool.RESTORATION,
    mana_cost=30,
    cooldown=15.0,
    target_type=SpellTarget.SINGLE,
    base_power=5,
    level_requirement=2,
    effects=[
        {"type": "heal_over_time", "duration": 10.0, "tick_rate": 1.0},
    ],
)

# Abjuration (Protection) Spells
SHIELD = Spell(
    id="shield",
    name="Shield",
    description="Creates a protective barrier",
    school=SpellSchool.ABJURATION,
    mana_cost=30,
    cooldown=20.0,
    target_type=SpellTarget.SELF,
    base_power=50,
    level_requirement=2,
    effects=[
        {"type": "absorb", "duration": 15.0},
    ],
)

# Enchantment (Buff/Debuff) Spells
STRENGTH = Spell(
    id="strength",
    name="Strength",
    description="Increases attack power",
    school=SpellSchool.ENCHANTMENT,
    mana_cost=25,
    cooldown=30.0,
    target_type=SpellTarget.SINGLE,
    base_power=10,
    level_requirement=2,
    effects=[
        {"type": "buff", "stat": "attack_power", "duration": 60.0},
    ],
)

WEAKEN = Spell(
    id="weaken",
    name="Weaken",
    description="Reduces target's defense",
    school=SpellSchool.ENCHANTMENT,
    mana_cost=20,
    cooldown=15.0,
    target_type=SpellTarget.SINGLE,
    base_power=5,
    level_requirement=1,
    effects=[
        {"type": "debuff", "stat": "defense", "duration": 30.0},
    ],
)


# Spell Registry
SPELL_REGISTRY: dict[str, Spell] = {
    spell.id: spell for spell in [
        FIREBALL,
        LIGHTNING_BOLT,
        ICE_STORM,
        HEAL,
        GREATER_HEAL,
        REGENERATION,
        SHIELD,
        STRENGTH,
        WEAKEN,
    ]
}


def get_spell(spell_id: str) -> Spell | None:
    """Get a spell by ID."""
    return SPELL_REGISTRY.get(spell_id)


def get_all_spells() -> list[Spell]:
    """Get all registered spells."""
    return list(SPELL_REGISTRY.values())


def get_spells_by_school(school: SpellSchool) -> list[Spell]:
    """Get all spells of a specific school."""
    return [s for s in SPELL_REGISTRY.values() if s.school == school]

Part 4: Magic System

Create the main magic system:

# src/maid_magic_system/systems/magic_system.py
"""Magic system for spell casting and effects."""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar
from uuid import UUID

from maid_engine.core.ecs import System
from maid_stdlib.components import DescriptionComponent, HealthComponent, PositionComponent

from ..components.mana import ManaComponent
from ..components.spells import SpellbookComponent, SpellEffectComponent, SpellTarget
from ..data.spells import get_spell
from ..events import SpellCastEvent, SpellEffectEvent, SpellFailedEvent

if TYPE_CHECKING:
    from maid_engine.core.ecs import Entity


class MagicSystem(System):
    """System that handles spell casting and magic effects.

    Responsibilities:
    - Process spell cast requests
    - Validate mana and cooldowns
    - Calculate spell effects
    - Apply damage, healing, buffs, debuffs
    - Process active spell effects each tick
    """

    priority: ClassVar[int] = 55  # Run after combat

    async def startup(self) -> None:
        """Subscribe to spell events."""
        self.events.subscribe(SpellCastEvent, self._handle_spell_cast)

    async def update(self, delta: float) -> None:
        """Process active spell effects."""
        # Update cooldowns
        for entity in self.entities.with_components(SpellbookComponent):
            spellbook = entity.get(SpellbookComponent)
            spellbook.reduce_cooldowns(delta)

        # Process active effects
        for entity in self.entities.with_components(SpellEffectComponent):
            effects = entity.get(SpellEffectComponent)
            await self._process_effects(entity, effects, delta)

    async def _handle_spell_cast(self, event: SpellCastEvent) -> None:
        """Handle a spell cast request."""
        caster = self.entities.get(event.caster_id)
        if not caster:
            return

        # Get spell definition
        spell = get_spell(event.spell_id)
        if not spell:
            await self.events.emit(SpellFailedEvent(
                caster_id=event.caster_id,
                spell_id=event.spell_id,
                reason="Unknown spell",
            ))
            return

        # Check requirements
        fail_reason = self._check_cast_requirements(caster, spell)
        if fail_reason:
            await self.events.emit(SpellFailedEvent(
                caster_id=event.caster_id,
                spell_id=event.spell_id,
                reason=fail_reason,
            ))
            return

        # Get targets
        targets = self._get_targets(caster, event.target_id, spell.target_type)
        if not targets and spell.target_type != SpellTarget.SELF:
            await self.events.emit(SpellFailedEvent(
                caster_id=event.caster_id,
                spell_id=event.spell_id,
                reason="No valid targets",
            ))
            return

        # Consume mana
        mana = caster.get(ManaComponent)
        mana.consume(spell.mana_cost)

        # Start cooldown
        spellbook = caster.get(SpellbookComponent)
        if spell.cooldown > 0:
            spellbook.start_cooldown(spell.id, spell.cooldown)

        # Calculate power
        power = spell.base_power + mana.casting_modifier

        # Apply effects to all targets
        for target in targets:
            await self._apply_spell_effects(caster, target, spell, power)

    def _check_cast_requirements(self, caster: Entity, spell) -> str | None:
        """Check if caster meets requirements to cast a spell.

        Returns:
            Error message if requirements not met, None otherwise.
        """
        # Check mana
        mana = caster.try_get(ManaComponent)
        if not mana:
            return "You have no magical ability"
        if not mana.can_cast(spell.mana_cost):
            return "Not enough mana"

        # Check spellbook
        spellbook = caster.try_get(SpellbookComponent)
        if not spellbook:
            return "You have no spellbook"
        if not spellbook.knows_spell(spell.id):
            return "You don't know that spell"

        # Check cooldown
        if spellbook.is_on_cooldown(spell.id):
            remaining = spellbook.get_cooldown(spell.id)
            return f"Spell on cooldown ({remaining:.1f}s)"

        return None

    def _get_targets(
        self,
        caster: Entity,
        target_id: UUID | None,
        target_type: SpellTarget
    ) -> list[Entity]:
        """Get valid targets for a spell."""
        if target_type == SpellTarget.SELF:
            return [caster]

        caster_pos = caster.try_get(PositionComponent)
        if not caster_pos:
            return []

        if target_type == SpellTarget.SINGLE:
            if target_id:
                target = self.entities.get(target_id)
                if target:
                    target_pos = target.try_get(PositionComponent)
                    if target_pos and target_pos.room_id == caster_pos.room_id:
                        return [target]
            return []

        # Area/group targeting
        targets = []
        for entity in self.entities.with_components(PositionComponent):
            if entity.id == caster.id:
                continue

            pos = entity.get(PositionComponent)
            if pos.room_id != caster_pos.room_id:
                continue

            if target_type == SpellTarget.ALL_ENEMIES:
                if entity.has_tag("hostile"):
                    targets.append(entity)
            elif target_type == SpellTarget.ALL_ALLIES:
                if entity.has_tag("player") or entity.has_tag("ally"):
                    targets.append(entity)
            elif target_type == SpellTarget.AREA:
                targets.append(entity)

        return targets

    async def _apply_spell_effects(
        self,
        caster: Entity,
        target: Entity,
        spell,
        power: int
    ) -> None:
        """Apply spell effects to a target."""
        for effect in spell.effects:
            effect_type = effect["type"]

            if effect_type == "damage":
                await self._apply_damage(caster, target, power, effect)

            elif effect_type == "heal":
                await self._apply_healing(caster, target, power)

            elif effect_type == "heal_over_time":
                self._apply_hot(caster, target, power, effect, spell.id)

            elif effect_type in ("buff", "debuff"):
                self._apply_stat_modifier(caster, target, power, effect, spell.id)

            elif effect_type == "absorb":
                self._apply_shield(caster, target, power, effect, spell.id)

        # Emit spell effect event
        await self.events.emit(SpellEffectEvent(
            caster_id=caster.id,
            target_id=target.id,
            spell_id=spell.id,
            effect_type=spell.effects[0]["type"] if spell.effects else "none",
        ))

    async def _apply_damage(
        self,
        caster: Entity,
        target: Entity,
        power: int,
        effect: dict
    ) -> None:
        """Apply magical damage to a target."""
        health = target.try_get(HealthComponent)
        if not health:
            return

        damage_type = effect.get("damage_type", "magical")
        # Could apply resistances here
        actual_damage = health.damage(power)

        # Emit damage event for combat log
        from maid_combat_system.events import DamageDealtEvent
        await self.events.emit(DamageDealtEvent(
            source_id=caster.id,
            target_id=target.id,
            damage=actual_damage,
            damage_type=damage_type,
        ))

    async def _apply_healing(
        self,
        caster: Entity,
        target: Entity,
        power: int
    ) -> None:
        """Apply healing to a target."""
        health = target.try_get(HealthComponent)
        if not health:
            return

        actual_healing = health.heal(power)

    def _apply_hot(
        self,
        caster: Entity,
        target: Entity,
        power: int,
        effect: dict,
        spell_id: str
    ) -> None:
        """Apply heal-over-time effect."""
        effects = target.try_get(SpellEffectComponent)
        if not effects:
            target.add(SpellEffectComponent())
            effects = target.get(SpellEffectComponent)

        effects.add_effect(
            effect_type="heal_over_time",
            power=power,
            duration=effect.get("duration", 10.0),
            source_id=caster.id,
            spell_id=spell_id,
        )

    def _apply_stat_modifier(
        self,
        caster: Entity,
        target: Entity,
        power: int,
        effect: dict,
        spell_id: str
    ) -> None:
        """Apply buff or debuff."""
        effects = target.try_get(SpellEffectComponent)
        if not effects:
            target.add(SpellEffectComponent())
            effects = target.get(SpellEffectComponent)

        effect_power = power if effect["type"] == "buff" else -power

        effects.add_effect(
            effect_type=f"stat_{effect['stat']}",
            power=effect_power,
            duration=effect.get("duration", 30.0),
            source_id=caster.id,
            spell_id=spell_id,
        )

    def _apply_shield(
        self,
        caster: Entity,
        target: Entity,
        power: int,
        effect: dict,
        spell_id: str
    ) -> None:
        """Apply damage absorption shield."""
        effects = target.try_get(SpellEffectComponent)
        if not effects:
            target.add(SpellEffectComponent())
            effects = target.get(SpellEffectComponent)

        effects.add_effect(
            effect_type="absorb",
            power=power,
            duration=effect.get("duration", 15.0),
            source_id=caster.id,
            spell_id=spell_id,
        )

    async def _process_effects(
        self,
        entity: Entity,
        effects: SpellEffectComponent,
        delta: float
    ) -> None:
        """Process active spell effects on an entity."""
        # Process heal-over-time
        for hot in effects.get_effects_by_type("heal_over_time"):
            # Apply healing tick
            health = entity.try_get(HealthComponent)
            if health:
                tick_healing = int(hot["power"] * delta)
                if tick_healing > 0:
                    health.heal(tick_healing)

        # Tick durations
        effects.tick(delta)

        # Remove expired effects
        expired = effects.remove_expired()
        # Could emit events for expired effects

Part 5: Magic Commands

Create the cast command:

# src/maid_magic_system/commands/magic_commands.py
"""Commands for the magic system."""

from maid_engine.commands.registry import AccessLevel, CommandContext, LayeredCommandRegistry
from maid_stdlib.components import DescriptionComponent, PositionComponent

from ..components.mana import ManaComponent
from ..components.spells import SpellbookComponent
from ..data.spells import get_spell, get_all_spells
from ..events import SpellCastEvent


async def cast_command(ctx: CommandContext) -> bool:
    """Cast a spell.

    Usage:
        cast <spell> [target]
        cast fireball goblin
        cast heal self
    """
    if not ctx.args:
        await ctx.session.send("Cast what spell?")
        await ctx.session.send("Usage: cast <spell> [target]")
        return True

    spell_name = ctx.args[0].lower()
    target_name = ctx.args[1].lower() if len(ctx.args) > 1 else None

    # Get player
    player = ctx.world.entities.get(ctx.player_id)
    if not player:
        return False

    # Check player has magic ability
    mana = player.try_get(ManaComponent)
    if not mana:
        await ctx.session.send("You have no magical ability.")
        return True

    spellbook = player.try_get(SpellbookComponent)
    if not spellbook:
        await ctx.session.send("You don't have a spellbook.")
        return True

    # Find spell
    spell = get_spell(spell_name)
    if not spell:
        # Try to find by partial name
        for s in get_all_spells():
            if spell_name in s.name.lower():
                spell = s
                break

    if not spell:
        await ctx.session.send(f"Unknown spell: {spell_name}")
        return True

    # Check requirements
    if not spellbook.knows_spell(spell.id):
        await ctx.session.send(f"You don't know {spell.name}.")
        return True

    if not mana.can_cast(spell.mana_cost):
        await ctx.session.send(
            f"Not enough mana. Need {spell.mana_cost}, have {mana.current}."
        )
        return True

    if spellbook.is_on_cooldown(spell.id):
        remaining = spellbook.get_cooldown(spell.id)
        await ctx.session.send(f"{spell.name} is on cooldown ({remaining:.1f}s).")
        return True

    # Find target
    target_id = None
    if target_name and target_name != "self":
        target = await find_target(ctx, target_name)
        if not target:
            await ctx.session.send(f"You don't see '{target_name}' here.")
            return True
        target_id = target.id

    # Cast the spell
    await ctx.session.send(f"You cast {spell.name}!")

    await ctx.world.events.emit(SpellCastEvent(
        caster_id=ctx.player_id,
        spell_id=spell.id,
        target_id=target_id,
    ))

    return True


async def spells_command(ctx: CommandContext) -> bool:
    """List known spells.

    Usage:
        spells
    """
    player = ctx.world.entities.get(ctx.player_id)
    if not player:
        return False

    spellbook = player.try_get(SpellbookComponent)
    if not spellbook:
        await ctx.session.send("You don't have a spellbook.")
        return True

    mana = player.try_get(ManaComponent)

    if not spellbook.known_spells:
        await ctx.session.send("You don't know any spells.")
        return True

    await ctx.session.send("Known Spells:")
    await ctx.session.send("-" * 40)

    for spell_id in spellbook.known_spells:
        spell = get_spell(spell_id)
        if not spell:
            continue

        # Build spell line
        status = ""
        if spellbook.is_on_cooldown(spell_id):
            cd = spellbook.get_cooldown(spell_id)
            status = f" [CD: {cd:.1f}s]"
        elif mana and not mana.can_cast(spell.mana_cost):
            status = " [No mana]"

        await ctx.session.send(
            f"  {spell.name} ({spell.mana_cost} mana) - {spell.description}{status}"
        )

    if mana:
        await ctx.session.send("-" * 40)
        await ctx.session.send(f"Mana: {mana.current}/{mana.maximum}")

    return True


async def find_target(ctx: CommandContext, name: str):
    """Find a target by name in the player's room."""
    player = ctx.world.entities.get(ctx.player_id)
    if not player:
        return None

    player_pos = player.try_get(PositionComponent)
    if not player_pos:
        return None

    for entity in ctx.world.entities.with_components(PositionComponent, DescriptionComponent):
        pos = entity.get(PositionComponent)
        if pos.room_id != player_pos.room_id:
            continue

        desc = entity.get(DescriptionComponent)
        if name in str(desc.name).lower():
            return entity

    return None


def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
    """Register magic commands."""
    registry.register(
        name="cast",
        handler=cast_command,
        pack_name=pack_name,
        aliases=["c"],
        category="magic",
        description="Cast a spell",
        usage="cast <spell> [target]",
        access_level=AccessLevel.PLAYER,
    )

    registry.register(
        name="spells",
        handler=spells_command,
        pack_name=pack_name,
        aliases=["spellbook", "sb"],
        category="magic",
        description="List known spells",
        usage="spells",
        access_level=AccessLevel.PLAYER,
    )

Summary

This tutorial covered:

  1. Mana Component: Resource management for spell casting
  2. Spell Components: Spell definitions and spellbooks
  3. Spell Registry: Predefined spell data
  4. Magic System: Spell casting, targeting, and effect processing
  5. Commands: Player interface for casting spells

Integration with Combat

The magic system integrates with the combat system by:

  • Emitting DamageDealtEvent for damage spells
  • Using the same targeting system
  • Sharing components like HealthComponent

Next Steps

  • Add more spell effects (stun, teleport, summon)
  • Implement spell combos
  • Create a learning/leveling system for spells
  • Add equipment that boosts magic

Continue to the Complete Game Tutorial to see how all systems work together.