package world import ( "strings" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/object" ) type ExitDir string const ( North ExitDir = "north" South ExitDir = "south" East ExitDir = "east" West ExitDir = "west" Up ExitDir = "up" Down ExitDir = "down" ) var ExitAliases = map[string]ExitDir{ "n": North, "s": South, "e": East, "w": West, "u": Up, "d": Down, } var OppositeExit = map[ExitDir]ExitDir{ North: South, South: North, East: West, West: East, Up: Down, Down: Up, } var ExitOrder = []ExitDir{ North, South, East, West, Up, Down, } type SpawnDef struct { ID string `yaml:"id"` Quantity int `yaml:"quantity"` RespawnTicks float64 `yaml:"respawn_ticks"` } type ExitDef struct { Room int `yaml:"room"` Condition *behavior.Condition `yaml:"condition"` BlockedMessage string `yaml:"blocked_message"` SetFlags map[string]any `yaml:"set_flags"` SetPlayerFlags map[string]any `yaml:"set_player_flags"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { if value.Kind == yaml.ScalarNode { var n int if err := value.Decode(&n); err != nil { return err } e.Room = n return nil } type raw ExitDef return value.Decode((*raw)(e)) } type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` Color string `yaml:"color"` Description behavior.DescList `yaml:"description"` Exits map[ExitDir]ExitDef `yaml:"exits"` Objects []RoomObject `yaml:"objects"` ItemSpawns []SpawnDef `yaml:"item_spawns"` Mobs []RoomMob `yaml:"mobs"` OnEnter []EnterStep `yaml:"on_enter"` Hazard string `yaml:"hazard"` BlockTransport bool `yaml:"block_transport"` Triggers []TriggerDef `yaml:"triggers"` } type RoomMob struct { ID string `yaml:"id"` WanderRooms []int `yaml:"wander_rooms"` WanderInterval float64 `yaml:"wander_interval"` } func (rm *RoomMob) UnmarshalYAML(value *yaml.Node) error { if value.Kind == yaml.ScalarNode { var s string if err := value.Decode(&s); err != nil { return err } rm.ID = s return nil } type raw RoomMob return value.Decode((*raw)(rm)) } type EnterStep struct { Message string `yaml:"message"` Condition *behavior.Condition `yaml:"condition"` Delay int `yaml:"delay"` SetFlags map[string]any `yaml:"set_flags"` SetPlayerFlags map[string]any `yaml:"set_player_flags"` Broadcast string `yaml:"broadcast"` BroadcastGlobal string `yaml:"broadcast_global"` SpawnMob *SpawnMobConfig `yaml:"spawn_mob"` DespawnMob string `yaml:"despawn_mob"` GiveItem string `yaml:"give_item"` TakeItem string `yaml:"take_item"` Teleport int `yaml:"teleport"` Heal int `yaml:"heal"` } // IsTimed reports whether the step carries enter-sequence semantics (any // non-zero field means it needs per-tick scheduling rather than synchronous // printing). func (e EnterStep) IsTimed() bool { return e.Delay > 0 || len(e.SetFlags) > 0 || len(e.SetPlayerFlags) > 0 || e.Message != "" || e.Broadcast != "" || e.BroadcastGlobal != "" || e.SpawnMob != nil || e.DespawnMob != "" || e.GiveItem != "" || 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 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)), " ") }