package world import ( "fmt" "os" "path/filepath" "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 } func New(dataDir string) *World { return &World{ dataDir: dataDir, groundItems: make(map[int][]*groundEntry), seeded: make(map[int]bool), } } 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]int) } if room.Objects == nil { room.Objects = make([]string, 0) } if room.Spawns == nil { room.Spawns = make([]SpawnDef, 0) } if room.Mobs == nil { room.Mobs = make([]string, 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() 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 { 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 } } 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 = "" } } } } }