diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/room.go | 55 | ||||
| -rw-r--r-- | internal/world/room_test.go | 92 |
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) + } + } +} |
