package world import ( "fmt" "os" "path/filepath" "strings" "sync" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" ) type World struct { dataDir string mu sync.Mutex roomPathIndex map[int]string groundItems map[int][]*groundEntry seeded map[int]bool objStates map[string]*ObjState objMoves []ObjMove hazardMu sync.Mutex hazardPathIndex map[string]string hazardDefs map[string]*HazardDef } func New(dataDir string) *World { return &World{ dataDir: dataDir, roomPathIndex: behavior.BuildRoomIndex(filepath.Join(dataDir, "rooms")), groundItems: make(map[int][]*groundEntry), seeded: make(map[int]bool), objStates: make(map[string]*ObjState), hazardDefs: make(map[string]*HazardDef), } } func (w *World) LoadRoom(id int) (*Room, error) { w.mu.Lock() path, ok := w.roomPathIndex[id] w.mu.Unlock() if !ok { return nil, fmt.Errorf("read room %d: no such room", id) } data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read room %d: %w", id, err) } var room Room if err := yaml.Unmarshal(data, &room); err != nil { return nil, fmt.Errorf("parse room %d: %w", id, err) } room.ID = id if room.Exits == nil { room.Exits = make(map[ExitDir]ExitDef) } if room.ItemSpawns == nil { room.ItemSpawns = make([]SpawnDef, 0) } if room.Mobs == nil { room.Mobs = make([]RoomMob, 0) } return &room, nil } func (w *World) ResolveExit(input string) ExitDir { if dir, ok := ExitAliases[strings.ToLower(input)]; ok { return dir } canon := ExitDir(strings.ToLower(input)) switch canon { case North, South, East, West, Northeast, Northwest, Southeast, Southwest, Up, Down: return canon } return "" } func (w *World) SeedGroundItems(roomID int) { w.mu.Lock() if w.seeded[roomID] { w.mu.Unlock() return } w.seeded[roomID] = true w.mu.Unlock() room, err := w.LoadRoom(roomID) if err != nil { return } w.mu.Lock() defer w.mu.Unlock() for _, s := range room.ItemSpawns { merged := false for _, e := range w.groundItems[roomID] { if e.quantity > 0 && strings.EqualFold(e.itemID, s.ID) && (e.reserveTimer <= 0 || e.reservedFor == "") && e.despawnTimer <= 0 { e.quantity += s.Quantity e.respawnQty += s.Quantity e.isSpawn = true e.respawnDelay = int(s.RespawnTicks) merged = true break } } if !merged { e := &groundEntry{ itemID: s.ID, quantity: s.Quantity, isSpawn: true, respawnDelay: int(s.RespawnTicks), respawnQty: s.Quantity, } w.groundItems[roomID] = append(w.groundItems[roomID], e) } } } func (w *World) RoomIndex() map[int]bool { w.mu.Lock() defer w.mu.Unlock() ids := make(map[int]bool, len(w.roomPathIndex)) for id := range w.roomPathIndex { ids[id] = true } return ids } func (w *World) RebuildRoomIndex(dataDir string) { w.mu.Lock() defer w.mu.Unlock() w.roomPathIndex = behavior.BuildRoomIndex(filepath.Join(dataDir, "rooms")) } func (w *World) AddRoomPath(id int, path string) { w.mu.Lock() defer w.mu.Unlock() w.roomPathIndex[id] = path } func (w *World) GetRoomPath(id int) (string, bool) { w.mu.Lock() defer w.mu.Unlock() path, ok := w.roomPathIndex[id] return path, ok } func (w *World) ClearHazardCache() { w.hazardMu.Lock() defer w.hazardMu.Unlock() w.hazardPathIndex = nil w.hazardDefs = make(map[string]*HazardDef) } func (w *World) Tick() { w.mu.Lock() defer w.mu.Unlock() w.tickGroundItems() w.tickObjStatesLocked() } func (w *World) ClearRoomState(roomID int) { w.mu.Lock() defer w.mu.Unlock() delete(w.groundItems, roomID) delete(w.seeded, roomID) prefix := fmt.Sprintf("%d:", roomID) for k := range w.objStates { if strings.HasPrefix(k, prefix) { delete(w.objStates, k) } } }