aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/room.go55
-rw-r--r--internal/world/room_test.go92
2 files changed, 144 insertions, 3 deletions
diff --git a/internal/world/room.go b/internal/world/room.go
index 8f13b82..8b8830e 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -1,8 +1,11 @@
package world
import (
+ "strings"
+
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/object"
)
type ExitDir string
@@ -125,8 +128,54 @@ func (e EnterStep) IsTimed() bool {
e.TakeItem != "" || e.Teleport != 0 || e.Heal != 0
}
+// RoomObject is either a reference to a file-backed object definition (only
+// `id`/wander fields set) or a fully inline object definition (Inline != nil).
+// An entry is treated as inline when it carries any passive content field
+// (name, description, aliases, inroom_description, color, hidden, on_look).
+// Inline objects are room-scoped: their identity (ID, used as the ObjState
+// DefID) is derived from the normalized name; `id:` is reserved for references.
type RoomObject struct {
- ID string `yaml:"id"`
- WanderRooms []int `yaml:"wander_rooms"`
- WanderInterval float64 `yaml:"wander_interval"`
+ ID string
+ WanderRooms []int
+ WanderInterval float64
+ Inline *object.ObjectDef
+}
+
+func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error {
+ type rawRef struct {
+ ID string `yaml:"id"`
+ WanderRooms []int `yaml:"wander_rooms"`
+ WanderInterval float64 `yaml:"wander_interval"`
+ }
+ var ref rawRef
+ if err := value.Decode(&ref); err != nil {
+ return err
+ }
+ ro.WanderRooms = ref.WanderRooms
+ ro.WanderInterval = ref.WanderInterval
+
+ var def object.ObjectDef
+ if err := value.Decode(&def); err != nil {
+ return err
+ }
+ inline := def.Name != "" || len(def.Description) > 0 || def.InRoomDescription != "" ||
+ len(def.Aliases) > 0 || def.Color != "" || def.Hidden || def.OnLook != nil
+ if inline {
+ // Identity derives from the name. def.ID still holds any explicit `id:`
+ // the builder wrote; it is left in place only so validation can flag it
+ // as a stray field (runtime resolution overrides the copy's ID anyway).
+ ro.ID = NormalizeObjectName(def.Name)
+ ro.Inline = &def
+ return nil
+ }
+ ro.ID = ref.ID
+ return nil
+}
+
+// NormalizeObjectName lowercases, trims, and collapses internal whitespace,
+// keeping spaces (not underscores) so the word-prefix matcher treats the name
+// as a single token in its `_`-split fallback. It is the canonical derivation
+// of an inline object's ObjState DefID from its name.
+func NormalizeObjectName(name string) string {
+ return strings.Join(strings.Fields(strings.ToLower(name)), " ")
}
diff --git a/internal/world/room_test.go b/internal/world/room_test.go
new file mode 100644
index 0000000..472fe4f
--- /dev/null
+++ b/internal/world/room_test.go
@@ -0,0 +1,92 @@
+package world
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func writeRoom(t *testing.T, dir string, body string) {
+ t.Helper()
+ rooms := filepath.Join(dir, "rooms")
+ if err := os.MkdirAll(rooms, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(rooms, "1001.yaml")
+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRoomObjectReferenceVsInline(t *testing.T) {
+ dir := t.TempDir()
+ body := `name: Test Room
+objects:
+ - id: workbench
+ - name: window
+ hidden: true
+ description: "A small window."
+ - id: control
+ name: instrument panel
+ aliases: [cockpit]
+ description: "A panel."
+`
+ // rebuild the path index so LoadRoom can find the room file
+ writeRoom(t, dir, body)
+ w := New(dir)
+
+ room, err := w.LoadRoom(1001)
+ if err != nil {
+ t.Fatalf("LoadRoom: %v", err)
+ }
+ if len(room.Objects) != 3 {
+ t.Fatalf("expected 3 objects, got %d", len(room.Objects))
+ }
+
+ // reference
+ if ref := room.Objects[0]; ref.Inline != nil || ref.ID != "workbench" {
+ t.Errorf("object 0 should be a reference to workbench, got %+v (inline=%v)", ref, ref.Inline)
+ }
+
+ // inline, id derived from the normalized name
+ in := room.Objects[1]
+ if in.Inline == nil {
+ t.Fatalf("object 1 should be inline")
+ }
+ if in.ID != "window" {
+ t.Errorf("inline id should derive from name to %q, got %q", "window", in.ID)
+ }
+ if !in.Inline.Hidden || in.Inline.Name != "window" {
+ t.Errorf("inline fields not parsed: %+v", in.Inline)
+ }
+
+ // inline with a multi-word name: id is the normalized name (spaces kept),
+ // and an explicit `id:` is NOT used as identity (it is preserved on the
+ // def only so validation can flag it).
+ ex := room.Objects[2]
+ if ex.Inline == nil {
+ t.Fatalf("object 2 should be inline")
+ }
+ if ex.ID != "instrument panel" {
+ t.Errorf("inline id should be normalized name %q, got %q", "instrument panel", ex.ID)
+ }
+ if ex.Inline.ID != "control" {
+ t.Errorf("explicit id should be preserved on def for validation, got %q", ex.Inline.ID)
+ }
+ if len(ex.Inline.Aliases) != 1 || ex.Inline.Aliases[0] != "cockpit" {
+ t.Errorf("inline aliases not parsed: %+v", ex.Inline.Aliases)
+ }
+}
+
+func TestNormalizeObjectName(t *testing.T) {
+ cases := map[string]string{
+ "window": "window",
+ "Instrument Panel": "instrument panel",
+ " Safety Card ": "safety card",
+ }
+ for in, want := range cases {
+ if got := NormalizeObjectName(in); got != want {
+ t.Errorf("NormalizeObjectName(%q) = %q, want %q", in, got, want)
+ }
+ }
+}