Add Thing templates, TOML loading, and spawning

ThingTemplate dataclass mirrors MobTemplate pattern. load_thing_template
and load_thing_templates parse TOML files from content/things/. spawn_thing
creates Thing instances from templates. Includes rock and fountain examples.
This commit is contained in:
Jared Miller 2026-02-11 20:01:15 -05:00
parent 2e79255aec
commit c43b3346ae
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
5 changed files with 221 additions and 4 deletions

View file

@ -0,0 +1,3 @@
name = "fountain"
description = "a weathered stone fountain, water trickling into a mossy basin"
portable = false

4
content/things/rock.toml Normal file
View file

@ -0,0 +1,4 @@
name = "rock"
description = "a smooth grey rock, worn by wind and rain"
portable = true
aliases = ["stone"]

View file

@ -3,13 +3,9 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from mudlib.object import Object
if TYPE_CHECKING:
from mudlib.thing import Thing
@dataclass
class Entity(Object):

62
src/mudlib/things.py Normal file
View file

@ -0,0 +1,62 @@
"""Thing template loading, registry, and spawning."""
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
from mudlib.object import Object
from mudlib.thing import Thing
@dataclass
class ThingTemplate:
"""Definition loaded from TOML — used to spawn Thing instances."""
name: str
description: str
portable: bool = True
aliases: list[str] = field(default_factory=list)
# Module-level registry
thing_templates: dict[str, ThingTemplate] = {}
def load_thing_template(path: Path) -> ThingTemplate:
"""Parse a thing TOML file into a ThingTemplate."""
with open(path, "rb") as f:
data = tomllib.load(f)
return ThingTemplate(
name=data["name"],
description=data["description"],
portable=data.get("portable", True),
aliases=data.get("aliases", []),
)
def load_thing_templates(directory: Path) -> dict[str, ThingTemplate]:
"""Load all .toml files in a directory into a dict keyed by name."""
templates: dict[str, ThingTemplate] = {}
for path in sorted(directory.glob("*.toml")):
template = load_thing_template(path)
templates[template.name] = template
return templates
def spawn_thing(
template: ThingTemplate,
location: Object | None,
*,
x: int | None = None,
y: int | None = None,
) -> Thing:
"""Create a Thing instance from a template at the given location."""
return Thing(
name=template.name,
description=template.description,
portable=template.portable,
aliases=list(template.aliases),
location=location,
x=x,
y=y,
)

View file

@ -0,0 +1,152 @@
"""Tests for thing template loading and spawning."""
import textwrap
import pytest
from mudlib.thing import Thing
from mudlib.things import (
ThingTemplate,
load_thing_template,
load_thing_templates,
spawn_thing,
)
from mudlib.zone import Zone
@pytest.fixture
def test_zone():
terrain = [["." for _ in range(10)] for _ in range(10)]
return Zone(
name="testzone",
width=10,
height=10,
terrain=terrain,
)
# --- ThingTemplate ---
def test_thing_template_creation():
"""ThingTemplate holds definition data."""
t = ThingTemplate(
name="rock",
description="a smooth grey rock",
portable=True,
aliases=["stone"],
)
assert t.name == "rock"
assert t.description == "a smooth grey rock"
assert t.portable is True
assert t.aliases == ["stone"]
def test_thing_template_defaults():
"""ThingTemplate has sensible defaults."""
t = ThingTemplate(name="rock", description="a rock")
assert t.portable is True
assert t.aliases == []
# --- load_thing_template ---
def test_load_thing_template(tmp_path):
"""Load a thing template from a TOML file."""
toml_content = textwrap.dedent("""\
name = "rusty sword"
description = "a sword covered in rust"
portable = true
aliases = ["sword", "rusty"]
""")
p = tmp_path / "sword.toml"
p.write_text(toml_content)
template = load_thing_template(p)
assert template.name == "rusty sword"
assert template.description == "a sword covered in rust"
assert template.portable is True
assert template.aliases == ["sword", "rusty"]
def test_load_thing_template_minimal(tmp_path):
"""Load thing template with only required fields."""
toml_content = textwrap.dedent("""\
name = "rock"
description = "a rock"
""")
p = tmp_path / "rock.toml"
p.write_text(toml_content)
template = load_thing_template(p)
assert template.name == "rock"
assert template.portable is True # default
assert template.aliases == [] # default
def test_load_thing_template_non_portable(tmp_path):
"""Load a non-portable thing template."""
toml_content = textwrap.dedent("""\
name = "fountain"
description = "a stone fountain"
portable = false
""")
p = tmp_path / "fountain.toml"
p.write_text(toml_content)
template = load_thing_template(p)
assert template.portable is False
# --- load_thing_templates ---
def test_load_thing_templates(tmp_path):
"""Load all thing templates from a directory."""
for name in ["rock", "sword"]:
p = tmp_path / f"{name}.toml"
p.write_text(f'name = "{name}"\ndescription = "a {name}"\n')
templates = load_thing_templates(tmp_path)
assert "rock" in templates
assert "sword" in templates
assert len(templates) == 2
def test_load_thing_templates_empty_dir(tmp_path):
"""Empty directory returns empty dict."""
templates = load_thing_templates(tmp_path)
assert templates == {}
# --- spawn_thing ---
def test_spawn_thing_in_zone(test_zone):
"""spawn_thing creates a Thing instance in a zone."""
template = ThingTemplate(
name="rock",
description="a smooth grey rock",
portable=True,
aliases=["stone"],
)
thing = spawn_thing(template, test_zone, x=3, y=7)
assert isinstance(thing, Thing)
assert thing.name == "rock"
assert thing.description == "a smooth grey rock"
assert thing.portable is True
assert thing.aliases == ["stone"]
assert thing.location is test_zone
assert thing.x == 3
assert thing.y == 7
assert thing in test_zone.contents
def test_spawn_thing_without_coordinates(test_zone):
"""spawn_thing can create a thing without position (template/inventory)."""
template = ThingTemplate(name="gem", description="a sparkling gem")
thing = spawn_thing(template, None)
assert thing.location is None
assert thing.x is None
assert thing.y is None