Skip to content

Level Gating

Problem

You want to restrict access to a dangerous area so that only players at or above a minimum level can enter.

Solution

MAID's lock expressions make this trivial. The built-in level(n) lock function checks the player's character level.

Using Lock Expressions on a Command

Gate a movement command or area entrance:

from maid_engine.commands.decorators import command
from maid_engine.commands import CommandContext


@command(
    name="enter_dungeon",
    category="movement",
    help_text="Enter the Abyssal Depths (requires level 10)",
    locks="level(10)",
)
async def cmd_enter_dungeon(ctx: CommandContext) -> bool:
    """Enter the level-gated dungeon."""
    dungeon_entrance_id = ctx.world.get_data("dungeons", "abyssal_depths")
    if not dungeon_entrance_id:
        await ctx.session.send("The dungeon entrance is sealed.\n")
        return False

    ctx.world.move_entity(ctx.player_id, dungeon_entrance_id)
    await ctx.session.send(
        "You descend into the Abyssal Depths. The air grows cold...\n"
    )
    return True

If the player's level is below 10, the command is automatically blocked and the player receives a permission error.

Event-Based Gate (Movement Check)

For blocking standard movement without a custom command, use a pre-hook or event handler:

from uuid import UUID
from dataclasses import dataclass

from maid_engine.core.ecs import System
from maid_engine.core.events import Event, RoomEnterEvent


GATED_ROOMS: dict[UUID, int] = {}  # room_id -> minimum_level


class LevelGateSystem(System):
    """Prevents under-leveled players from entering gated rooms."""

    priority = 5  # Run very early, before other movement processing

    async def startup(self) -> None:
        self.events.subscribe(
            RoomEnterEvent, self._check_level_gate
        )

    async def _check_level_gate(self, event: RoomEnterEvent) -> None:
        """Cancel room entry if the player is under-leveled."""
        min_level = GATED_ROOMS.get(event.room_id)
        if min_level is None:
            return  # Not a gated room

        entity = self.world.get_entity(event.entity_id)
        if not entity or not entity.has_tag("player"):
            return  # Only gate players, not NPCs

        # Check character level via CharacterInfoComponent
        # (from maid-classic-rpg or your own level component)
        from maid_classic_rpg.components import CharacterInfoComponent
        info = entity.try_get(CharacterInfoComponent)
        player_level = info.level if info else 0

        if player_level < min_level:
            event.cancel()
            # Move them back
            if event.from_room_id:
                self.world.move_entity(event.entity_id, event.from_room_id)

Using a Custom Lock Function

Register a custom lock function for more complex gating:

from maid_engine.commands.locks import LockContext


def lock_area_level(ctx: LockContext, args: list[str]) -> bool:
    """Check if the player meets the area's level requirement.

    Usage in lock expression: area_level()
    """
    if not ctx.player_entity_id:
        return False

    entity = ctx.world.get_entity(ctx.player_entity_id)
    if not entity:
        return False

    # Get the player's current room's minimum level
    room_id = ctx.world.get_entity_room(ctx.player_entity_id)
    if not room_id:
        return True  # No room = no restriction

    min_level = GATED_ROOMS.get(room_id, 0)
    if min_level == 0:
        return True

    from maid_classic_rpg.components import CharacterInfoComponent
    info = entity.try_get(CharacterInfoComponent)
    return (info.level if info else 0) >= min_level


# Register inside your ContentPack's register_commands method:
def register_commands(self, registry: LayeredCommandRegistry) -> None:
    registry.register_lock_function("area_level", lock_area_level)

Then use it:

@command(
    name="explore",
    locks="area_level()",
    help_text="Explore the current area",
)
async def cmd_explore(ctx: CommandContext) -> bool:
    await ctx.session.send("You explore the area and discover hidden treasures!\n")
    return True

Builder Setup

Tag rooms with level requirements in-game:

@set here/min_level = 10
@attribute here add level_gate
@describe here = The entrance to the Abyssal Depths. A sign reads: "DANGER - Level 10 required."

How It Works

  1. level(n) lock function: Built-in — checks CharacterInfoComponent.level >= n on the player
  2. Lock expressions run automatically before command execution; if they return False, the command is denied
  3. Event cancellation: event.cancel() in an event handler stops the event from propagating to further handlers
  4. Custom lock functions: Registered via registry.register_lock_function() in register_commands() for domain-specific checks

Variations

  • Soft gate: Instead of blocking, warn the player and apply a debuff (reduced stats in the area)
  • Quest gate: Use locks="has_flag(completed_trial)" — require completing a quest instead of a level
  • Combined: locks="level(10) AND has_item(dungeon_pass)" — both level and an item required
  • Class restriction: Register a custom is_class(warrior) lock function
  • Progressive unlocking: Gate each floor of a dungeon at incrementally higher levels (10, 15, 20, etc.)

See Also