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:
parent
2e79255aec
commit
c43b3346ae
5 changed files with 221 additions and 4 deletions
3
content/things/fountain.toml
Normal file
3
content/things/fountain.toml
Normal 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
4
content/things/rock.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
name = "rock"
|
||||||
|
description = "a smooth grey rock, worn by wind and rain"
|
||||||
|
portable = true
|
||||||
|
aliases = ["stone"]
|
||||||
|
|
@ -3,13 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from mudlib.object import Object
|
from mudlib.object import Object
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Entity(Object):
|
class Entity(Object):
|
||||||
|
|
|
||||||
62
src/mudlib/things.py
Normal file
62
src/mudlib/things.py
Normal 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,
|
||||||
|
)
|
||||||
152
tests/test_thing_templates.py
Normal file
152
tests/test_thing_templates.py
Normal 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
|
||||||
Loading…
Reference in a new issue