package world import ( "strings" ) const DropDespawnTicks = 1000 const ReserveTicks = 100 type GroundItemInfo struct { ItemID string Quantity int ReservedFor string ReserveTimer int DespawnTimer int IsSpawn bool } 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 } 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, DespawnTimer: e.despawnTimer, IsSpawn: e.isSpawn, } 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) AddReservedItem(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) } // RemoveReservedItem removes up to qty of itemID from the room's ground, // refusing entries reserved for another player. It is ALL-OR-NOTHING: // when any part of the quantity is unobtainable (a reserved-for-others // entry, or too little accessible stock) nothing is taken and (0, false) // is returned. Callers must never lose already-removed items to a failed // pickup, so the removal is verified before any state is touched. func (w *World) RemoveReservedItem(roomID int, itemID string, qty int, owner string) (int, bool) { w.mu.Lock() defer w.mu.Unlock() entries := w.groundItems[roomID] // Pass 1: verify the full quantity is takable by this owner without // mutating anything, so a blocked pickup can never leave the ground // half-emptied with nothing delivered. available := 0 for _, e := range entries { if available >= qty { break } if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) { continue } if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner { continue } available += e.quantity } if available < qty { return 0, false } // Pass 2: take. Every entry reachable here is takeable, so no // mid-loop abort is possible. removed := 0 remaining := qty for _, e := range entries { if remaining <= 0 { break } if e.quantity <= 0 || !strings.EqualFold(e.itemID, itemID) { 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, true } // AccessibleGround returns the ground items in roomID this owner may take: // entries reserved for another player are excluded. Pickup flows use this // to size requests and match names against takeable stock only. func (w *World) AccessibleGround(roomID int, owner string) 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 { continue } if e.reserveTimer > 0 && e.reservedFor != "" && e.reservedFor != owner { continue } out[e.itemID] += e.quantity } return out } 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) tickGroundItems() { for _, entries := range w.groundItems { for _, e := range entries { if e.respawnTimer > 0 { e.respawnTimer-- if e.respawnTimer <= 0 { e.quantity = e.respawnQty w.mergeNearbyItems(entries, e) } } 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 = "" } } } } } func (w *World) mergeNearbyItems(entries []*groundEntry, target *groundEntry) { for _, other := range entries { if other != target && other.quantity > 0 && strings.EqualFold(other.itemID, target.itemID) && (other.reserveTimer <= 0 || other.reservedFor == "") && other.despawnTimer <= 0 { target.quantity += other.quantity other.quantity = 0 } } }