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 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
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