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¶
level(n)lock function: Built-in — checksCharacterInfoComponent.level >= non the player- Lock expressions run automatically before command execution; if they return
False, the command is denied - Event cancellation:
event.cancel()in an event handler stops the event from propagating to further handlers - Custom lock functions: Registered via
registry.register_lock_function()inregister_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¶
- Locked Door — Item-based access gating
- Command System Guide — Lock expressions reference
- Hidden Room — Another access pattern