Skip to content

Teleport Portal

Problem

You want a portal object that teleports players from one room to another when they interact with it — optionally across different worlds.

Solution

The Portal Component

from uuid import UUID
from maid_engine.core.ecs import Component


class PortalComponent(Component):
    """Marks an entity as a teleportation portal."""

    destination_room_id: UUID
    destination_world_id: str | None = None  # For cross-world portals
    portal_name: str = "shimmering portal"
    use_message: str = "You step through the portal..."
    arrive_message: str = "{name} steps out of a portal."
    cooldown: float = 0.0        # Seconds between uses
    last_used: float = 0.0
    one_way: bool = False        # If True, no return portal
    requires_key: str | None = None  # Item keyword needed to activate

The Enter Command

from uuid import UUID
from maid_engine.commands.decorators import command, arguments
from maid_engine.commands.arguments import ArgumentSpec, ArgumentType, ParsedArguments
from maid_engine.commands import CommandContext
from maid_engine.core.ecs import Entity
from maid_stdlib.components import DescriptionComponent


def _find_portal(ctx: CommandContext, keyword: str) -> Entity | None:
    """Find a portal entity in the player's room."""
    room_id = ctx.world.get_entity_room(ctx.player_id)
    if not room_id:
        return None
    for entity in ctx.world.entities_in_room(room_id):
        portal = entity.try_get(PortalComponent)
        if not portal:
            continue
        desc = entity.try_get(DescriptionComponent)
        if desc and desc.matches_keyword(keyword):
            return entity
        if keyword.lower() in portal.portal_name.lower():
            return entity
    return None


@command(name="enter", aliases=["portal"], category="movement",
         help_text="Enter a portal to teleport")
@arguments(
    ArgumentSpec("target", ArgumentType.STRING, required=False, default="portal"),
)
async def cmd_enter_portal(ctx: CommandContext, args: ParsedArguments) -> bool:
    """Step through a portal to teleport to another location."""
    keyword: str = args["target"]
    portal_entity = _find_portal(ctx, keyword)
    if not portal_entity:
        await ctx.session.send("There's no portal here.\n")
        return False

    portal = portal_entity.get(PortalComponent)

    # Check key requirement
    if portal.requires_key:
        player = ctx.world.get_entity(ctx.player_id)
        if not player:
            return False
        from maid_stdlib.components import InventoryComponent
        inv = player.try_get(InventoryComponent)
        has_key = False
        if inv:
            for item_id in inv.items:
                item = ctx.world.get_entity(item_id)
                if item:
                    desc = item.try_get(DescriptionComponent)
                    if desc and desc.matches_keyword(portal.requires_key):
                        has_key = True
                        break
        if not has_key:
            await ctx.session.send(
                "The portal resists your touch. You seem to need something.\n"
            )
            return False

    # Verify destination exists
    dest_room = ctx.world.get_room(portal.destination_room_id)
    if not dest_room:
        await ctx.session.send("The portal flickers but leads nowhere.\n")
        return False

    # Announce departure
    from_room_id = ctx.world.get_entity_room(ctx.player_id)
    await ctx.session.send(f"{portal.use_message}\n")

    # Teleport
    ctx.world.move_entity(ctx.player_id, portal.destination_room_id)

    # Show the new room (reuse look command pattern)
    await ctx.session.send("You arrive in a new location.\n")
    return True

Creating a Portal Pair

from uuid import UUID
from maid_engine.core.world import World
from maid_stdlib.components import DescriptionComponent


async def create_portal_pair(
    world: World,
    room_a: UUID,
    room_b: UUID,
) -> None:
    """Create a bidirectional portal between two rooms."""
    # Portal A → B
    portal_ab = world.create_entity()
    portal_ab.add(DescriptionComponent(
        name="Blue Portal",
        short_desc="a shimmering blue portal",
        long_desc="A swirling vortex of blue energy hangs in the air.",
        keywords=["blue", "portal"],
    ))
    portal_ab.add(PortalComponent(
        destination_room_id=room_b,
        portal_name="blue portal",
        use_message="You step into the blue vortex. The world spins...",
        arrive_message="{name} tumbles out of a blue portal.",
    ))
    portal_ab.add_tag("portal")
    world.place_entity_in_room(portal_ab.id, room_a)

    # Portal B → A (return portal)
    portal_ba = world.create_entity()
    portal_ba.add(DescriptionComponent(
        name="Red Portal",
        short_desc="a shimmering red portal",
        long_desc="A swirling vortex of red energy hangs in the air.",
        keywords=["red", "portal"],
    ))
    portal_ba.add(PortalComponent(
        destination_room_id=room_a,
        portal_name="red portal",
        use_message="You step into the red vortex. The world spins...",
        arrive_message="{name} tumbles out of a red portal.",
    ))
    portal_ba.add_tag("portal")
    world.place_entity_in_room(portal_ba.id, room_b)

Builder Commands

You can also create portals in-game:

@create item Blue Portal
@describe Blue Portal = A swirling vortex of blue energy.
@component Blue Portal add PortalComponent {"destination_room_id": "<target-room-uuid>", "portal_name": "blue portal"}
@teleport Blue Portal here

How It Works

  1. PortalComponent stores the destination and activation rules
  2. cmd_enter_portal finds a matching portal in the room, checks requirements, then calls world.move_entity()
  3. world.move_entity() handles updating PositionComponent and room index automatically
  4. For bidirectional travel, create two portal entities — one in each room

Variations

  • Cross-world portals: Use WorldManager to look up a destination in a different world instance
  • Timed portals: Add an expiry timestamp — a system removes the portal after N minutes
  • Random destination: Pick from a list of destination_room_ids each time
  • Level-gated portal: Add locks="level(10)" to restrict portal use by level
  • Portal network: Create a PortalNetworkSystem that links multiple portals like a fast-travel map
  • Visual effects: Subscribe to a custom PortalActivatedEvent to trigger room description changes

See Also