Add portal loading from zone TOML files

Zone TOML files can now define portals using [[portals]] sections.
Each portal specifies coordinates (x, y), a target (zone_name:x,y),
and a label. Optional aliases are supported. Portals are
automatically created and placed in the zone when it loads.
This commit is contained in:
Jared Miller 2026-02-11 22:03:08 -05:00
parent cb3ad6a547
commit b3801f780f
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 171 additions and 0 deletions

View file

@ -6,6 +6,7 @@ import logging
import tomllib
from pathlib import Path
from mudlib.portal import Portal
from mudlib.zone import Zone
log = logging.getLogger(__name__)
@ -77,6 +78,36 @@ def load_zone(path: Path) -> Zone:
spawn_y=spawn_y,
)
# Load portals
portals_data = data.get("portals", [])
for portal_dict in portals_data:
# Parse target string "zone_name:x,y"
target = portal_dict["target"]
try:
target_zone, coords = target.split(":")
target_x, target_y = map(int, coords.split(","))
except ValueError:
log.warning(
"skipping portal '%s' at (%d, %d): malformed target '%s'",
portal_dict["label"],
portal_dict["x"],
portal_dict["y"],
target,
)
continue
# Create portal (automatically added to zone._contents via Object.__post_init__)
Portal(
name=portal_dict["label"],
aliases=portal_dict.get("aliases", []),
target_zone=target_zone,
target_x=target_x,
target_y=target_y,
location=zone,
x=portal_dict["x"],
y=portal_dict["y"],
)
return zone

View file

@ -211,3 +211,143 @@ rows = [
assert zone.spawn_y == 0
finally:
temp_path.unlink()
def test_load_zone_with_portals():
"""Load a zone with portals defined."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "portal_zone"
description = "a zone with portals"
width = 10
height = 10
[terrain]
rows = [
"..........",
"..........",
"..........",
"..........",
"..........",
"..........",
"..........",
"..........",
"..........",
"..........",
]
[[portals]]
x = 5
y = 3
target = "tavern:1,1"
label = "tavern door"
[[portals]]
x = 2
y = 7
target = "forest:10,5"
label = "forest path"
""")
temp_path = pathlib.Path(f.name)
try:
zone = load_zone(temp_path)
# Find portals in zone contents
portals = [obj for obj in zone._contents if obj.__class__.__name__ == "Portal"]
assert len(portals) == 2
# Check first portal (tavern door at 5,3)
tavern_portal = [p for p in portals if p.name == "tavern door"][0]
assert tavern_portal.x == 5
assert tavern_portal.y == 3
assert tavern_portal.target_zone == "tavern"
assert tavern_portal.target_x == 1
assert tavern_portal.target_y == 1
assert tavern_portal.location == zone
# Check second portal (forest path at 2,7)
forest_portal = [p for p in portals if p.name == "forest path"][0]
assert forest_portal.x == 2
assert forest_portal.y == 7
assert forest_portal.target_zone == "forest"
assert forest_portal.target_x == 10
assert forest_portal.target_y == 5
assert forest_portal.location == zone
# Verify portals are at correct coordinates
contents_at_5_3 = zone.contents_at(5, 3)
assert tavern_portal in contents_at_5_3
contents_at_2_7 = zone.contents_at(2, 7)
assert forest_portal in contents_at_2_7
finally:
temp_path.unlink()
def test_load_zone_without_portals():
"""Load a zone without portals section works fine."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "no_portals"
description = "a zone without portals"
width = 3
height = 3
[terrain]
rows = [
"...",
"...",
"...",
]
""")
temp_path = pathlib.Path(f.name)
try:
zone = load_zone(temp_path)
# Should have no portals
portals = [obj for obj in zone._contents if obj.__class__.__name__ == "Portal"]
assert len(portals) == 0
finally:
temp_path.unlink()
def test_load_zone_portal_with_aliases():
"""Portal can have optional aliases field in TOML."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
f.write("""
name = "alias_zone"
description = "a zone with aliased portal"
width = 5
height = 5
[terrain]
rows = [
".....",
".....",
".....",
".....",
".....",
]
[[portals]]
x = 2
y = 2
target = "elsewhere:0,0"
label = "mysterious gateway"
aliases = ["gateway", "gate", "portal"]
""")
temp_path = pathlib.Path(f.name)
try:
zone = load_zone(temp_path)
portals = [obj for obj in zone._contents if obj.__class__.__name__ == "Portal"]
assert len(portals) == 1
portal = portals[0]
assert portal.name == "mysterious gateway"
assert portal.aliases == ["gateway", "gate", "portal"]
finally:
temp_path.unlink()