Skip to content

Locked Door

Problem

You want a door that only opens when the player carries a specific key item. The door should block movement until unlocked.

Solution

This uses MAID's built-in lock expressions and ExitMetadataComponent — no custom system needed for the basic case.

Setting Up the Locked Exit (Builder Commands)

The fastest way is to use in-game builder commands:

@dig north = Throne Room
@set here/exits/north/locked = true
@set here/exits/north/door = true
@set here/exits/north/key_id = iron_key
@set here/exits/north/door_name = iron-bound door

Creating the Key Item

@create item Iron Key
@set Iron Key/item_type = key
@set Iron Key/keywords = ["iron", "key", "iron key"]
@describe Iron Key = A heavy iron key with ornate teeth. It looks like it fits a large door.

Programmatic Setup

To create the locked door in code (e.g., during on_load):

from uuid import UUID
from maid_engine.core.world import World
from maid_stdlib.components import (
    DescriptionComponent,
    ExitMetadataComponent,
    ExitInfo,
    ItemComponent,
)


async def create_locked_door_area(world: World) -> None:
    """Create two rooms connected by a locked door."""
    # Create the rooms
    hallway = world.create_entity()
    hallway.add(DescriptionComponent(
        name="Grand Hallway",
        long_desc="A long stone hallway stretches before you. "
                  "An iron-bound door blocks the way north.",
        keywords=["hallway"],
    ))
    hallway.add_tag("room")
    world.register_room(hallway.id, {"name": "Grand Hallway"})

    throne_room = world.create_entity()
    throne_room.add(DescriptionComponent(
        name="Throne Room",
        long_desc="A magnificent throne sits atop a raised dais.",
        keywords=["throne", "room"],
    ))
    throne_room.add_tag("room")
    world.register_room(throne_room.id, {"name": "Throne Room"})

    # Lock the north exit from the hallway
    hallway.add(ExitMetadataComponent(
        exits={
            "north": ExitInfo(
                door=True,
                locked=True,
                key_id="iron_key",
                door_name="iron-bound door",
            ),
        },
    ))

    # Wire the actual exit route so movement works once unlocked
    hallway_data = world.get_room(hallway.id)
    if hallway_data is not None:
        if isinstance(hallway_data, dict):
            hallway_data.setdefault("exits", {})["north"] = throne_room.id
        elif hasattr(hallway_data, "exits"):
            hallway_data.exits["north"] = throne_room.id

    # Create the key item
    key = world.create_entity()
    key.add(DescriptionComponent(
        name="Iron Key",
        short_desc="a heavy iron key",
        long_desc="A heavy iron key with ornate teeth.",
        keywords=["iron", "key", "iron key"],
    ))
    key.add(ItemComponent(
        item_type="key",
        weight=0.5,
        value=0,
    ))
    key.add_tag("item")

Gating a Command with a Lock Expression

If you want a command that only works when the player has the key:

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


@command(
    name="unlock",
    category="interaction",
    help_text="Unlock a door with the right key",
    locks="has_item(iron_key)",
)
async def cmd_unlock(ctx: CommandContext) -> bool:
    """Unlock a door if the player has the key."""
    room_id = ctx.world.get_entity_room(ctx.player_id)
    if not room_id:
        return False

    room = ctx.world.get_entity(room_id) if room_id else None
    if not room:
        return False

    exit_meta = room.try_get(ExitMetadataComponent)
    if not exit_meta:
        await ctx.session.send("There's nothing to unlock here.\n")
        return False

    direction = ctx.args[0] if ctx.args else "north"
    exit_info = exit_meta.exits.get(direction)
    if not exit_info or not exit_info.locked:
        await ctx.session.send(f"There's no locked door to the {direction}.\n")
        return False

    exit_info.locked = False
    exit_info.door_open = True
    exit_meta.notify_mutation()
    await ctx.session.send(
        f"You unlock the {exit_info.door_name or 'door'} with your iron key.\n"
    )
    return True

How It Works

  1. ExitMetadataComponent stores per-direction exit state: door, locked, hidden, key_id
  2. The movement system checks ExitInfo.locked before allowing passage
  3. Lock expressions like has_item(iron_key) are evaluated against the player's inventory at command time
  4. notify_mutation() marks the entity dirty so persistence picks up the change

Variations

  • Multiple keys for one door: Use a custom lock function instead of the built-in has_item
  • One-use key: Remove the key from inventory after unlocking with inventory.remove_item(key.id, weight)
  • Lockpicking: Add a skill check — locks="has_item(iron_key) OR has_skill(lockpick, 5)"
  • Timed relock: Subscribe to a timer event to set locked = True again after N seconds

See Also