package world import ( "fmt" "math/rand" "os" "path/filepath" "sort" "strings" "sync" "gopkg.in/yaml.v3" ) const DropDespawnTicks = 1000 const ReserveTicks = 100 type GroundItemInfo struct { ItemID string Quantity int ReservedFor string ReserveTimer int } type groundEntry struct { itemID string quantity int isSpawn bool respawnTimer int // >0 = counting down to respawn respawnQty int respawnDelay int despawnTimer int // >0 = counting down to despawn (dropped items) reservedFor string reserveTimer int // >0 = counting down reservation } type World struct { dataDir string mu sync.Mutex groundItems map[int][]*groundEntry seeded map[int]bool objStates map[string]*ObjState objMoves []ObjMove } type ObjMove struct { DefID string Name string FromRoom int ToRoom int } type ObjState struct { Depleted bool DepleteTimer int DefID string Name string Index int RoomID int JustRespawned bool WanderRooms []int WanderInterval int WanderCounter int SharedMax int SharedTimer int } func (w *World) ObjStateKey(roomID int, defID string, index int) string { return fmt.Sprintf("%d:%s:%d", roomID, defID, index) } func (w *World) objStateKey(roomID int, defID string, index int) string { return w.ObjStateKey(roomID, defID, index) } func (w *World) EnsureObjectStates(roomID int, defIDs []string) { w.mu.Lock() defer w.mu.Unlock() if w.objStates == nil { w.objStates = make(map[string]*ObjState) } counts := make(map[string]int) for _, defID := range defIDs { idx := counts[defID] counts[defID]++ key := w.objStateKey(roomID, defID, idx) if _, exists := w.objStates[key]; !exists { w.objStates[key] = &ObjState{ DefID: defID, Name: defID, Index: idx, RoomID: roomID, } } } } func (w *World) GetObjState(roomID int, defID string, index int) *ObjState { w.mu.Lock() defer w.mu.Unlock() if w.objStates == nil { return nil } return w.objStates[w.objStateKey(roomID, defID, index)] } func (w *World) GetObjStateByKey(key string) *ObjState { w.mu.Lock() defer w.mu.Unlock() if w.objStates == nil { return nil } return w.objStates[key] } func (w *World) AllObjInstances(roomID int) []ObjState { w.mu.Lock() defer w.mu.Unlock() var out []ObjState for _, st := range w.objStates { if st.RoomID == roomID { out = append(out, *st) } } sort.Slice(out, func(i, j int) bool { if out[i].DefID != out[j].DefID { return out[i].DefID < out[j].DefID } return out[i].Index < out[j].Index }) return out } func (w *World) FindObjInstances(roomID int, name string) []ObjState { w.mu.Lock() defer w.mu.Unlock() var out []ObjState lower := strings.ToLower(name) for key, st := range w.objStates { if st.RoomID != roomID { continue } if !wordMatchesObj(lower, st.DefID, st.Name) { continue } out = append(out, ObjState{ DefID: st.DefID, Name: st.Name, Index: st.Index, RoomID: st.RoomID, Depleted: st.Depleted, DepleteTimer: st.DepleteTimer, SharedMax: st.SharedMax, SharedTimer: st.SharedTimer, }) _ = key } sort.Slice(out, func(i, j int) bool { if out[i].DefID != out[j].DefID { return out[i].DefID < out[j].DefID } return out[i].Index < out[j].Index }) return out } func wordMatchesObj(lower, defID, objName string) bool { if WordPrefixMatch(lower, objName) { return true } nameWords := strings.Fields(strings.ToLower(objName)) inputWords := strings.Fields(strings.ToLower(lower)) // Check defID as whole (bidirectional) if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) { return true } // Check each defID part (split by underscore) against each input word (bidirectional) for _, part := range strings.Split(defID, "_") { for _, iw := range inputWords { pl := strings.ToLower(part) if strings.HasPrefix(pl, iw) || strings.HasPrefix(iw, pl) { return true } } } _ = nameWords return false } func (w *World) SetObjName(roomID int, defID string, name string) { w.mu.Lock() defer w.mu.Unlock() for key, st := range w.objStates { if st.RoomID == roomID && st.DefID == defID { st.Name = name } _ = key } } func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval int) { w.mu.Lock() defer w.mu.Unlock() for key, st := range w.objStates { if st.RoomID == roomID && st.DefID == defID { st.WanderRooms = rooms st.WanderInterval = interval } _ = key } } func (w *World) SetObjSharedDeplete(roomID int, defID string, max int) { w.mu.Lock() defer w.mu.Unlock() for key, st := range w.objStates { if st.RoomID == roomID && st.DefID == defID && st.SharedMax == 0 { st.SharedMax = max st.SharedTimer = max } _ = key } } func (w *World) AllSharedObjStates() []*ObjState { w.mu.Lock() defer w.mu.Unlock() var out []*ObjState for _, st := range w.objStates { if st.SharedMax > 0 { out = append(out, st) } } return out } func (w *World) SetObjDepleted(roomID int, defID string, index int, delay int) { w.mu.Lock() defer w.mu.Unlock() key := w.objStateKey(roomID, defID, index) if st, ok := w.objStates[key]; ok { st.Depleted = true st.DepleteTimer = delay } } func New(dataDir string) *World { return &World{ dataDir: dataDir, groundItems: make(map[int][]*groundEntry), seeded: make(map[int]bool), objStates: make(map[string]*ObjState), } } func (w *World) LoadRoom(id int) (*Room, error) { path := filepath.Join(w.dataDir, "rooms", fmt.Sprintf("%d.yaml", 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.Spawns == nil { room.Spawns = 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, Up, Down: return canon } return "" } func (w *World) GroundItemsDetailed(roomID int) []GroundItemInfo { w.mu.Lock() defer w.mu.Unlock() var out []GroundItemInfo for _, e := range w.groundItems[roomID] { if e.quantity <= 0 { continue } info := GroundItemInfo{ ItemID: e.itemID, Quantity: e.quantity, } if e.reserveTimer > 0 && e.reservedFor != "" { info.ReservedFor = e.reservedFor info.ReserveTimer = e.reserveTimer } out = append(out, info) } return out } func (w *World) GroundItems(roomID int) map[string]int { w.mu.Lock() defer w.mu.Unlock() out := make(map[string]int) for _, e := range w.groundItems[roomID] { if e.quantity > 0 { out[e.itemID] += e.quantity } } return out } func (w *World) AddReservedGroundItem(roomID int, itemID string, qty int, owner string) { w.mu.Lock() defer w.mu.Unlock() e := &groundEntry{ itemID: itemID, quantity: qty, despawnTimer: DropDespawnTicks, reservedFor: owner, reserveTimer: ReserveTicks, } w.groundItems[roomID] = append(w.groundItems[roomID], e) } func (w *World) AddGroundItem(roomID int, itemID string, qty int) { w.mu.Lock() defer w.mu.Unlock() for _, e := range w.groundItems[roomID] { if e.quantity > 0 && strings.EqualFold(e.itemID, itemID) && (e.reserveTimer <= 0 || e.reservedFor == "") { e.quantity += qty e.despawnTimer = DropDespawnTicks return } } e := &groundEntry{ itemID: itemID, quantity: qty, despawnTimer: DropDespawnTicks, } w.groundItems[roomID] = append(w.groundItems[roomID], e) } func (w *World) RemoveReservedGroundItem(roomID int, itemID string, qty int, owner string) (int, bool) { w.mu.Lock() defer w.mu.Unlock() removed := 0 remaining := qty for _, e := range w.groundItems[roomID] { if remaining <= 0 { break } if !strings.EqualFold(e.itemID, itemID) { continue } if e.quantity <= 0 { continue } if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner { return removed, false } take := e.quantity if take > remaining { take = remaining } e.quantity -= take removed += take remaining -= take if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 { e.respawnTimer = e.respawnDelay } } return removed, true } func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int { w.mu.Lock() defer w.mu.Unlock() removed := 0 remaining := qty for _, e := range w.groundItems[roomID] { if remaining <= 0 { break } if !strings.EqualFold(e.itemID, itemID) { continue } if e.quantity <= 0 { continue } take := e.quantity if take > remaining { take = remaining } e.quantity -= take removed += take remaining -= take if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 { e.respawnTimer = e.respawnDelay } } return removed } 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.Spawns { merged := false for _, e := range w.groundItems[roomID] { if e.quantity > 0 && strings.EqualFold(e.itemID, s.ItemID) && (e.reserveTimer <= 0 || e.reservedFor == "") { e.quantity += s.Quantity e.respawnQty += s.Quantity e.isSpawn = true e.respawnDelay = s.RespawnTicks merged = true break } } if !merged { e := &groundEntry{ itemID: s.ItemID, quantity: s.Quantity, isSpawn: true, respawnDelay: s.RespawnTicks, respawnQty: s.Quantity, } w.groundItems[roomID] = append(w.groundItems[roomID], e) } } } func (w *World) Tick() { w.mu.Lock() defer w.mu.Unlock() for _, entries := range w.groundItems { for _, e := range entries { if e.respawnTimer > 0 { e.respawnTimer-- if e.respawnTimer <= 0 { e.quantity = e.respawnQty for _, other := range entries { if other != e && other.quantity > 0 && strings.EqualFold(other.itemID, e.itemID) && (other.reserveTimer <= 0 || other.reservedFor == "") { e.quantity += other.quantity other.quantity = 0 } } } } if e.despawnTimer > 0 { e.despawnTimer-- if e.despawnTimer <= 0 { e.quantity = 0 } } if e.reserveTimer > 0 { e.reserveTimer-- if e.reserveTimer <= 0 { e.reservedFor = "" } } } } for _, st := range w.objStates { if st.Depleted && st.DepleteTimer > 0 { st.DepleteTimer-- if st.DepleteTimer <= 0 { st.Depleted = false st.JustRespawned = true if st.SharedMax > 0 { st.SharedTimer = st.SharedMax } } } } } func (w *World) FlushObjRespawns() []ObjState { w.mu.Lock() defer w.mu.Unlock() var out []ObjState for _, st := range w.objStates { if st.JustRespawned { out = append(out, *st) st.JustRespawned = false } } return out } func (w *World) TickObjWander() { w.mu.Lock() defer w.mu.Unlock() var moves []struct { st *ObjState toRoom int } for _, st := range w.objStates { if len(st.WanderRooms) == 0 || st.WanderInterval <= 0 { continue } st.WanderCounter++ if st.WanderCounter >= st.WanderInterval { st.WanderCounter = 0 toRoom := st.WanderRooms[rand.Intn(len(st.WanderRooms))] if toRoom == st.RoomID { continue } moves = append(moves, struct { st *ObjState toRoom int }{st, toRoom}) } } for _, m := range moves { oldKey := w.objStateKey(m.st.RoomID, m.st.DefID, m.st.Index) fromRoom := m.st.RoomID m.st.RoomID = m.toRoom newKey := w.objStateKey(m.st.RoomID, m.st.DefID, m.st.Index) delete(w.objStates, oldKey) w.objStates[newKey] = m.st w.objMoves = append(w.objMoves, ObjMove{ DefID: m.st.DefID, Name: m.st.Name, FromRoom: fromRoom, ToRoom: m.toRoom, }) } } func (w *World) FlushObjMoves() []ObjMove { w.mu.Lock() defer w.mu.Unlock() out := w.objMoves w.objMoves = nil return out }