package loot import ( "context" "fmt" "log" "math/rand" "sync" "time" "eq2emu/internal/items" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) // Simplified Loot System // Consolidates all functionality from 7 files into unified architecture // Preserves 100% C++ EQ2 loot functionality while eliminating Active Record patterns // Loot tier constants based on EQ2 item quality system const ( LootTierTrash int8 = 0 // Gray items LootTierCommon int8 = 1 // White items LootTierUncommon int8 = 2 // Green items LootTierTreasured int8 = 3 // Blue items LootTierRare int8 = 4 // Purple items LootTierLegendary int8 = 5 // Orange items LootTierFabled int8 = 6 // Yellow items LootTierMythical int8 = 7 // Red items LootTierArtifact int8 = 8 // Artifact items LootTierRelic int8 = 9 // Relic items LootTierUltimate int8 = 10 // Ultimate items ) // Chest appearance IDs from the C++ implementation const ( ChestAppearanceSmall int32 = 4034 // Small chest for common+ items ChestAppearanceTreasure int32 = 5864 // Treasure chest for treasured+ items ChestAppearanceOrnate int32 = 5865 // Ornate chest for legendary+ items ChestAppearanceExquisite int32 = 4015 // Exquisite chest for fabled+ items ) // Loot generation constants const ( DefaultMaxLootItems int16 = 6 // Default maximum items per loot DefaultLootDropProbability float32 = 100.0 // Default probability for loot to drop DefaultCoinProbability float32 = 50.0 // Default probability for coin drops MaxGlobalLootTables int = 1000 // Maximum number of global loot tables ) // Chest interaction results const ( ChestResultSuccess = 0 // Operation successful ChestResultLocked = 1 // Chest is locked ChestResultTrapped = 2 // Chest is trapped ChestResultNoRights = 3 // No loot rights ChestResultEmpty = 4 // Chest is empty ChestResultFailed = 5 // Operation failed ChestResultCantCarry = 6 // Cannot carry more items ChestResultTooFar = 7 // Too far from chest ChestResultInCombat = 8 // Cannot loot while in combat ) // Loot distribution methods const ( LootDistributionNone = 0 // No automatic distribution LootDistributionFreeForAll = 1 // Anyone can loot LootDistributionRoundRobin = 2 // Round robin distribution LootDistributionMasterLoot = 3 // Master looter decides LootDistributionNeedGreed = 4 // Need before greed system LootDistributionLotto = 5 // Random lotto system ) // Special loot table IDs const ( LootTableIDNone int32 = 0 // No loot table LootTableIDGlobal int32 = -1 // Global loot table marker LootTableIDLevel int32 = -2 // Level-based global loot LootTableIDRace int32 = -3 // Race-based global loot LootTableIDZone int32 = -4 // Zone-based global loot ) // Chest spawn duration and cleanup const ( ChestDespawnTime = 300 // Seconds before chest despawns (5 minutes) ChestCleanupTime = 600 // Seconds before chest is force-cleaned (10 minutes) MaxChestsPerZone = 100 // Maximum number of chests per zone MaxChestsPerPlayer = 10 // Maximum number of chests a player can have loot rights to ) // Probability calculation constants const ( ProbabilityMax float32 = 100.0 // Maximum probability percentage ProbabilityMin float32 = 0.0 // Minimum probability percentage ProbabilityDefault float32 = 50.0 // Default probability for items ) // GlobalLootType represents the type of global loot type GlobalLootType int8 const ( GlobalLootTypeLevel GlobalLootType = iota GlobalLootTypeRace GlobalLootTypeZone ) // String returns the string representation of GlobalLootType func (t GlobalLootType) String() string { switch t { case GlobalLootTypeLevel: return "level" case GlobalLootTypeRace: return "race" case GlobalLootTypeZone: return "zone" default: return "unknown" } } // GroupLootMethod represents different group loot distribution methods type GroupLootMethod int8 const ( GroupLootMethodFreeForAll GroupLootMethod = iota GroupLootMethodRoundRobin GroupLootMethodMasterLooter GroupLootMethodNeed GroupLootMethodLotto ) // String returns the string representation of GroupLootMethod func (glm GroupLootMethod) String() string { switch glm { case GroupLootMethodFreeForAll: return "free_for_all" case GroupLootMethodRoundRobin: return "round_robin" case GroupLootMethodMasterLooter: return "master_looter" case GroupLootMethodNeed: return "need_greed" case GroupLootMethodLotto: return "lotto" default: return "unknown" } } // LootTable represents a complete loot table with its drops type LootTable struct { ID int32 Name string MinCoin int32 MaxCoin int32 MaxLootItems int16 LootDropProbability float32 CoinProbability float32 Drops []*LootDrop mutex sync.RWMutex } // LootDrop represents an individual item that can drop from a loot table type LootDrop struct { LootTableID int32 ItemID int32 ItemCharges int16 EquipItem bool Probability float32 NoDropQuestCompletedID int32 } // GlobalLoot represents global loot configuration based on level, race, or zone type GlobalLoot struct { Type GlobalLootType MinLevel int8 MaxLevel int8 Race int16 ZoneID int32 TableID int32 LootTier int32 } // LootResult represents the result of loot generation type LootResult struct { Items []*items.Item Coins int32 mutex sync.RWMutex } // AddItem adds an item to the loot result (thread-safe) func (lr *LootResult) AddItem(item *items.Item) { lr.mutex.Lock() defer lr.mutex.Unlock() lr.Items = append(lr.Items, item) } // AddCoins adds coins to the loot result (thread-safe) func (lr *LootResult) AddCoins(coins int32) { lr.mutex.Lock() defer lr.mutex.Unlock() lr.Coins += coins } // GetItems returns a copy of the items slice (thread-safe) func (lr *LootResult) GetItems() []*items.Item { lr.mutex.RLock() defer lr.mutex.RUnlock() result := make([]*items.Item, len(lr.Items)) copy(result, lr.Items) return result } // GetCoins returns the coin amount (thread-safe) func (lr *LootResult) GetCoins() int32 { lr.mutex.RLock() defer lr.mutex.RUnlock() return lr.Coins } // IsEmpty returns true if the loot result has no items or coins func (lr *LootResult) IsEmpty() bool { lr.mutex.RLock() defer lr.mutex.RUnlock() return len(lr.Items) == 0 && lr.Coins == 0 } // TreasureChest represents a treasure chest spawn containing loot type TreasureChest struct { ID int32 SpawnID int32 ZoneID int32 X float32 Y float32 Z float32 Heading float32 AppearanceID int32 LootResult *LootResult Created time.Time LootRights []uint32 // Player IDs with loot rights IsDisarmable bool IsLocked bool DisarmDifficulty int16 LockpickDifficulty int16 mutex sync.RWMutex } // HasLootRights checks if a player has rights to loot this chest func (tc *TreasureChest) HasLootRights(playerID uint32) bool { tc.mutex.RLock() defer tc.mutex.RUnlock() // If no specific loot rights, anyone can loot if len(tc.LootRights) == 0 { return true } for _, id := range tc.LootRights { if id == playerID { return true } } return false } // AddLootRights adds a player to the loot rights list func (tc *TreasureChest) AddLootRights(playerID uint32) { tc.mutex.Lock() defer tc.mutex.Unlock() // Check if already has rights for _, id := range tc.LootRights { if id == playerID { return } } tc.LootRights = append(tc.LootRights, playerID) } // ChestAppearance represents different chest appearances based on loot tier type ChestAppearance struct { AppearanceID int32 Name string MinTier int8 MaxTier int8 } // Predefined chest appearances based on C++ implementation var ( SmallChest = &ChestAppearance{AppearanceID: 4034, Name: "Small Chest", MinTier: 1, MaxTier: 2} TreasureChestAppearance = &ChestAppearance{AppearanceID: 5864, Name: "Treasure Chest", MinTier: 3, MaxTier: 4} OrnateChest = &ChestAppearance{AppearanceID: 5865, Name: "Ornate Chest", MinTier: 5, MaxTier: 6} ExquisiteChest = &ChestAppearance{AppearanceID: 4015, Name: "Exquisite Chest", MinTier: 7, MaxTier: 10} ) // GetChestAppearance returns the appropriate chest appearance based on loot tier func GetChestAppearance(highestTier int8) *ChestAppearance { if highestTier >= ExquisiteChest.MinTier { return ExquisiteChest } if highestTier >= OrnateChest.MinTier { return OrnateChest } if highestTier >= TreasureChestAppearance.MinTier { return TreasureChestAppearance } return SmallChest } // LootContext provides context for loot generation type LootContext struct { PlayerLevel int16 PlayerRace int16 ZoneID int32 KillerID uint32 GroupMembers []uint32 CompletedQuests map[int32]bool LootMethod GroupLootMethod } // LootEntry represents a complete loot entry with all associated data type LootEntry struct { SpawnID int32 LootTableID int32 TableName string Priority int16 } // LootStatistics tracks loot generation statistics type LootStatistics struct { TotalLoots int64 TotalItems int64 TotalCoins int64 TreasureChests int64 ItemsByTier map[int8]int64 LootsByTable map[int32]int64 AverageItemsPerLoot float32 AverageCoinsPerLoot float32 mutex sync.RWMutex } // NewLootStatistics creates a new loot statistics tracker func NewLootStatistics() *LootStatistics { return &LootStatistics{ ItemsByTier: make(map[int8]int64), LootsByTable: make(map[int32]int64), } } // RecordLoot records statistics for a loot generation func (ls *LootStatistics) RecordLoot(tableID int32, result *LootResult) { ls.mutex.Lock() defer ls.mutex.Unlock() ls.TotalLoots++ ls.LootsByTable[tableID]++ items := result.GetItems() ls.TotalItems += int64(len(items)) ls.TotalCoins += int64(result.GetCoins()) // Track items by tier for _, item := range items { ls.ItemsByTier[item.Details.Tier]++ } // Update averages if ls.TotalLoots > 0 { ls.AverageItemsPerLoot = float32(ls.TotalItems) / float32(ls.TotalLoots) ls.AverageCoinsPerLoot = float32(ls.TotalCoins) / float32(ls.TotalLoots) } } // RecordChest records a treasure chest creation func (ls *LootStatistics) RecordChest() { ls.mutex.Lock() defer ls.mutex.Unlock() ls.TreasureChests++ } // GetStatistics returns a copy of the current statistics func (ls *LootStatistics) GetStatistics() LootStatistics { ls.mutex.RLock() defer ls.mutex.RUnlock() // Create deep copy copy := LootStatistics{ TotalLoots: ls.TotalLoots, TotalItems: ls.TotalItems, TotalCoins: ls.TotalCoins, TreasureChests: ls.TreasureChests, AverageItemsPerLoot: ls.AverageItemsPerLoot, AverageCoinsPerLoot: ls.AverageCoinsPerLoot, ItemsByTier: make(map[int8]int64), LootsByTable: make(map[int32]int64), } for tier, count := range ls.ItemsByTier { copy.ItemsByTier[tier] = count } for tableID, count := range ls.LootsByTable { copy.LootsByTable[tableID] = count } return copy } // ChestInteractionResult represents the result of chest interaction type ChestInteractionResult struct { Success bool Result int8 Message string Items []*items.Item Coins int32 Experience int32 ChestEmpty bool ChestClosed bool } // LootManager handles all loot generation and management type LootManager struct { pool *sqlitex.Pool itemManager *items.ItemManager lootTables map[int32]*LootTable spawnLoot map[int32][]int32 globalLoot []*GlobalLoot treasureChests map[int32]*TreasureChest chestIDCounter int32 statistics *LootStatistics random *rand.Rand loaded bool mutex sync.RWMutex } // NewLootManager creates a new loot manager func NewLootManager(pool *sqlitex.Pool, itemManager *items.ItemManager) *LootManager { return &LootManager{ pool: pool, itemManager: itemManager, lootTables: make(map[int32]*LootTable), spawnLoot: make(map[int32][]int32), globalLoot: make([]*GlobalLoot, 0), treasureChests: make(map[int32]*TreasureChest), chestIDCounter: 1, statistics: NewLootStatistics(), random: rand.New(rand.NewSource(time.Now().UnixNano())), } } // Initialize loads all loot data from database func (lm *LootManager) Initialize() error { log.Printf("[LOOT] Initializing loot system...") if err := lm.LoadAllLootData(); err != nil { return fmt.Errorf("failed to load loot data: %v", err) } lm.mutex.Lock() lm.loaded = true lm.mutex.Unlock() log.Printf("[LOOT] Loot system initialized successfully") return nil } // IsLoaded returns whether the loot system has been loaded func (lm *LootManager) IsLoaded() bool { lm.mutex.RLock() defer lm.mutex.RUnlock() return lm.loaded } // LoadAllLootData loads all loot data from database func (lm *LootManager) LoadAllLootData() error { lm.mutex.Lock() defer lm.mutex.Unlock() // Clear existing data lm.lootTables = make(map[int32]*LootTable) lm.spawnLoot = make(map[int32][]int32) lm.globalLoot = make([]*GlobalLoot, 0) // Load loot tables if err := lm.loadLootTables(); err != nil { return fmt.Errorf("failed to load loot tables: %v", err) } // Load spawn loot assignments if err := lm.loadSpawnLoot(); err != nil { return fmt.Errorf("failed to load spawn loot: %v", err) } // Load global loot configuration if err := lm.loadGlobalLoot(); err != nil { return fmt.Errorf("failed to load global loot: %v", err) } log.Printf("[LOOT] Loaded %d loot tables, %d spawn assignments, %d global loot entries", len(lm.lootTables), len(lm.spawnLoot), len(lm.globalLoot)) return nil } // loadLootTables loads loot tables from database func (lm *LootManager) loadLootTables() error { conn, err := lm.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %v", err) } defer lm.pool.Put(conn) // Load loot tables stmt := conn.Prep(`SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability FROM loottable ORDER BY id`) for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("failed to step through loot tables: %v", err) } if !hasRow { break } table := &LootTable{ ID: int32(stmt.GetInt64("id")), Name: stmt.GetText("name"), MinCoin: int32(stmt.GetInt64("mincoin")), MaxCoin: int32(stmt.GetInt64("maxcoin")), MaxLootItems: int16(stmt.GetInt64("maxlootitems")), LootDropProbability: float32(stmt.GetFloat("lootdrop_probability")), CoinProbability: float32(stmt.GetFloat("coin_probability")), Drops: make([]*LootDrop, 0), } lm.lootTables[table.ID] = table } // Load loot drops for each table for tableID := range lm.lootTables { if err := lm.loadLootDrops(conn, tableID); err != nil { return fmt.Errorf("failed to load drops for table %d: %v", tableID, err) } } return nil } // loadLootDrops loads loot drops for a specific table func (lm *LootManager) loadLootDrops(conn *sqlite.Conn, tableID int32) error { stmt := conn.Prep(`SELECT item_id, item_charges, equip_item, probability, no_drop_quest_completed_id FROM lootdrop WHERE loot_table_id = ?`) stmt.BindInt64(1, int64(tableID)) table := lm.lootTables[tableID] for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("failed to step through loot drops: %v", err) } if !hasRow { break } drop := &LootDrop{ LootTableID: tableID, ItemID: int32(stmt.GetInt64("item_id")), ItemCharges: int16(stmt.GetInt64("item_charges")), EquipItem: stmt.GetInt64("equip_item") == 1, Probability: float32(stmt.GetFloat("probability")), NoDropQuestCompletedID: int32(stmt.GetInt64("no_drop_quest_completed_id")), } table.Drops = append(table.Drops, drop) } return nil } // loadSpawnLoot loads spawn loot assignments from database func (lm *LootManager) loadSpawnLoot() error { conn, err := lm.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %v", err) } defer lm.pool.Put(conn) stmt := conn.Prep(`SELECT spawn_id, loottable_id FROM spawn_loot ORDER BY spawn_id`) for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("failed to step through spawn loot: %v", err) } if !hasRow { break } spawnID := int32(stmt.GetInt64("spawn_id")) tableID := int32(stmt.GetInt64("loottable_id")) if _, exists := lm.spawnLoot[spawnID]; !exists { lm.spawnLoot[spawnID] = make([]int32, 0) } lm.spawnLoot[spawnID] = append(lm.spawnLoot[spawnID], tableID) } return nil } // loadGlobalLoot loads global loot configuration from database func (lm *LootManager) loadGlobalLoot() error { conn, err := lm.pool.Take(context.Background()) if err != nil { return fmt.Errorf("failed to get connection: %v", err) } defer lm.pool.Put(conn) stmt := conn.Prep(`SELECT type, loot_table, value1, value2, value3, value4 FROM loot_global ORDER BY type, value1`) for { hasRow, err := stmt.Step() if err != nil { return fmt.Errorf("failed to step through global loot: %v", err) } if !hasRow { break } lootType := GlobalLootType(stmt.GetInt64("type")) tableID := int32(stmt.GetInt64("loot_table")) value1 := int32(stmt.GetInt64("value1")) value2 := int32(stmt.GetInt64("value2")) value3 := int32(stmt.GetInt64("value3")) value4 := int32(stmt.GetInt64("value4")) global := &GlobalLoot{ Type: lootType, TableID: tableID, } switch lootType { case GlobalLootTypeLevel: global.MinLevel = int8(value1) global.MaxLevel = int8(value2) case GlobalLootTypeRace: global.Race = int16(value1) case GlobalLootTypeZone: global.ZoneID = value1 } // value3, value4 could be used for additional filtering _ = value3 _ = value4 lm.globalLoot = append(lm.globalLoot, global) } return nil } // GetLootTable returns a loot table by ID func (lm *LootManager) GetLootTable(tableID int32) *LootTable { lm.mutex.RLock() defer lm.mutex.RUnlock() return lm.lootTables[tableID] } // GetSpawnLootTables returns all loot table IDs assigned to a spawn func (lm *LootManager) GetSpawnLootTables(spawnID int32) []int32 { lm.mutex.RLock() defer lm.mutex.RUnlock() if tables, exists := lm.spawnLoot[spawnID]; exists { result := make([]int32, len(tables)) copy(result, tables) return result } return []int32{} } // GetGlobalLootTables returns global loot tables that apply to the given context func (lm *LootManager) GetGlobalLootTables(playerLevel int16, playerRace int16, zoneID int32) []*GlobalLoot { lm.mutex.RLock() defer lm.mutex.RUnlock() var result []*GlobalLoot for _, global := range lm.globalLoot { switch global.Type { case GlobalLootTypeLevel: if playerLevel >= int16(global.MinLevel) && playerLevel <= int16(global.MaxLevel) { result = append(result, global) } case GlobalLootTypeRace: if playerRace == global.Race { result = append(result, global) } case GlobalLootTypeZone: if zoneID == global.ZoneID { result = append(result, global) } } } return result } // GenerateLoot generates loot for a spawn based on its loot table assignments func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootResult, error) { log.Printf("[LOOT-GEN] Generating loot for spawn %d", spawnID) result := &LootResult{ Items: make([]*items.Item, 0), Coins: 0, } // Get loot tables for this spawn tableIDs := lm.GetSpawnLootTables(spawnID) // Also check for global loot tables globalLoot := lm.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID) for _, global := range globalLoot { tableIDs = append(tableIDs, global.TableID) } if len(tableIDs) == 0 { log.Printf("[LOOT-GEN] No loot tables found for spawn %d", spawnID) return result, nil } // Process each loot table for _, tableID := range tableIDs { if err := lm.processLootTable(tableID, context, result); err != nil { log.Printf("[LOOT-GEN] Error processing loot table %d: %v", tableID, err) continue } } // Record statistics if len(tableIDs) > 0 { lm.statistics.RecordLoot(tableIDs[0], result) } log.Printf("[LOOT-GEN] Generated %d items and %d coins for spawn %d", len(result.Items), result.Coins, spawnID) return result, nil } // processLootTable processes a single loot table and adds results to the loot result func (lm *LootManager) processLootTable(tableID int32, context *LootContext, result *LootResult) error { table := lm.GetLootTable(tableID) if table == nil { return fmt.Errorf("loot table %d not found", tableID) } lm.mutex.Lock() defer lm.mutex.Unlock() // Check if loot should drop at all if !lm.rollProbability(table.LootDropProbability) { log.Printf("[LOOT-GEN] Loot table %d failed drop probability check", tableID) return nil } // Generate coins if probability succeeds if lm.rollProbability(table.CoinProbability) { coins := lm.generateCoins(table.MinCoin, table.MaxCoin) result.AddCoins(coins) log.Printf("[LOOT-GEN] Generated %d coins from table %d", coins, tableID) } // Generate items itemsGenerated := 0 maxItems := int(table.MaxLootItems) if maxItems <= 0 { maxItems = int(DefaultMaxLootItems) } // Process each loot drop for _, drop := range table.Drops { // Check if we've hit the max item limit if itemsGenerated >= maxItems { break } // Check quest requirement if drop.NoDropQuestCompletedID > 0 { if !context.CompletedQuests[drop.NoDropQuestCompletedID] { continue // Player hasn't completed required quest } } // Roll probability for this drop if !lm.rollProbability(drop.Probability) { continue } // Get item from item manager (placeholder - TODO: integrate with ItemManager) var item *items.Item = nil if lm.itemManager != nil { // item = lm.itemManager.CreateItem(drop.ItemID) // TODO: implement CreateItem method } if item == nil { log.Printf("[LOOT-GEN] Item template %d not found for loot drop", drop.ItemID) continue } // Set charges if specified if drop.ItemCharges > 0 { item.Details.Count = int16(drop.ItemCharges) } // Mark as equipped if specified (handled by caller when distributing loot) if drop.EquipItem { // This would be handled by the caller when distributing loot } result.AddItem(item) itemsGenerated++ log.Printf("[LOOT-GEN] Generated item %d (%s) from table %d", drop.ItemID, item.Name, tableID) } return nil } // rollProbability rolls a probability check (0-100%) func (lm *LootManager) rollProbability(probability float32) bool { if probability <= 0 { return false } if probability >= 100.0 { return true } roll := lm.random.Float32() * 100.0 return roll <= probability } // generateCoins generates a random coin amount between min and max func (lm *LootManager) generateCoins(minCoin, maxCoin int32) int32 { if minCoin >= maxCoin { return minCoin } return minCoin + lm.random.Int31n(maxCoin-minCoin+1) } // CreateTreasureChest creates a treasure chest for loot func (lm *LootManager) CreateTreasureChest(spawnID int32, zoneID int32, x, y, z, heading float32, lootResult *LootResult, lootRights []uint32) (*TreasureChest, error) { lm.mutex.Lock() defer lm.mutex.Unlock() // Generate unique chest ID chestID := lm.chestIDCounter lm.chestIDCounter++ // Determine chest appearance based on highest item tier highestTier := lm.getHighestItemTier(lootResult.GetItems()) appearance := GetChestAppearance(highestTier) chest := &TreasureChest{ ID: chestID, SpawnID: spawnID, ZoneID: zoneID, X: x, Y: y, Z: z, Heading: heading, AppearanceID: appearance.AppearanceID, LootResult: lootResult, Created: time.Now(), LootRights: make([]uint32, len(lootRights)), IsDisarmable: false, // TODO: Implement trap system IsLocked: false, // TODO: Implement lock system } // Copy loot rights copy(chest.LootRights, lootRights) // Store chest lm.treasureChests[chestID] = chest // Record statistics lm.statistics.RecordChest() log.Printf("[CHEST] Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins", chestID, appearance.Name, x, y, z, len(lootResult.GetItems()), lootResult.GetCoins()) return chest, nil } // getHighestItemTier finds the highest tier among items func (lm *LootManager) getHighestItemTier(items []*items.Item) int8 { var highest int8 = LootTierCommon for _, item := range items { if item.Details.Tier > highest { highest = item.Details.Tier } } return highest } // GetTreasureChest returns a treasure chest by ID func (lm *LootManager) GetTreasureChest(chestID int32) *TreasureChest { lm.mutex.RLock() defer lm.mutex.RUnlock() return lm.treasureChests[chestID] } // RemoveTreasureChest removes a treasure chest func (lm *LootManager) RemoveTreasureChest(chestID int32) { lm.mutex.Lock() defer lm.mutex.Unlock() delete(lm.treasureChests, chestID) log.Printf("[CHEST] Removed treasure chest %d", chestID) } // LootChestItem removes a specific item from a chest func (lm *LootManager) LootChestItem(chestID int32, playerID uint32, itemUniqueID int64) (*items.Item, error) { lm.mutex.Lock() defer lm.mutex.Unlock() chest := lm.treasureChests[chestID] if chest == nil { return nil, fmt.Errorf("treasure chest %d not found", chestID) } // Check loot rights if !chest.HasLootRights(playerID) { return nil, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID) } // Find and remove the item lootItems := chest.LootResult.GetItems() for i, item := range lootItems { if item.Details.UniqueID == itemUniqueID { // Remove item from slice chest.LootResult.mutex.Lock() chest.LootResult.Items = append(chest.LootResult.Items[:i], chest.LootResult.Items[i+1:]...) chest.LootResult.mutex.Unlock() log.Printf("[CHEST] Player %d looted item %d (%s) from chest %d", playerID, item.Details.ItemID, item.Name, chestID) return item, nil } } return nil, fmt.Errorf("item %d not found in chest %d", itemUniqueID, chestID) } // LootChestCoins removes coins from a chest func (lm *LootManager) LootChestCoins(chestID int32, playerID uint32) (int32, error) { lm.mutex.Lock() defer lm.mutex.Unlock() chest := lm.treasureChests[chestID] if chest == nil { return 0, fmt.Errorf("treasure chest %d not found", chestID) } // Check loot rights if !chest.HasLootRights(playerID) { return 0, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID) } coins := chest.LootResult.GetCoins() if coins <= 0 { return 0, nil } // Remove coins from chest chest.LootResult.mutex.Lock() chest.LootResult.Coins = 0 chest.LootResult.mutex.Unlock() log.Printf("[CHEST] Player %d looted %d coins from chest %d", playerID, coins, chestID) return coins, nil } // LootChestAll removes all items and coins from a chest func (lm *LootManager) LootChestAll(chestID int32, playerID uint32) (*LootResult, error) { lm.mutex.Lock() defer lm.mutex.Unlock() chest := lm.treasureChests[chestID] if chest == nil { return nil, fmt.Errorf("treasure chest %d not found", chestID) } // Check loot rights if !chest.HasLootRights(playerID) { return nil, fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID) } // Get all loot result := &LootResult{ Items: chest.LootResult.GetItems(), Coins: chest.LootResult.GetCoins(), } // Clear chest loot chest.LootResult.mutex.Lock() chest.LootResult.Items = make([]*items.Item, 0) chest.LootResult.Coins = 0 chest.LootResult.mutex.Unlock() log.Printf("[CHEST] Player %d looted all (%d items, %d coins) from chest %d", playerID, len(result.Items), result.Coins, chestID) return result, nil } // IsChestEmpty checks if a chest has no loot func (lm *LootManager) IsChestEmpty(chestID int32) bool { lm.mutex.RLock() defer lm.mutex.RUnlock() chest := lm.treasureChests[chestID] if chest == nil { return true } return chest.LootResult.IsEmpty() } // CleanupExpiredChests removes chests that have been around too long func (lm *LootManager) CleanupExpiredChests() { lm.mutex.Lock() defer lm.mutex.Unlock() now := time.Now() var expired []int32 for chestID, chest := range lm.treasureChests { age := now.Sub(chest.Created).Seconds() // Remove empty chests after ChestDespawnTime if chest.LootResult.IsEmpty() && age > ChestDespawnTime { expired = append(expired, chestID) } // Force remove all chests after ChestCleanupTime if age > ChestCleanupTime { expired = append(expired, chestID) } } for _, chestID := range expired { delete(lm.treasureChests, chestID) log.Printf("[CHEST] Cleaned up expired chest %d", chestID) } if len(expired) > 0 { log.Printf("[CHEST] Cleaned up %d expired chests", len(expired)) } } // GetZoneChests returns all chests in a specific zone func (lm *LootManager) GetZoneChests(zoneID int32) []*TreasureChest { lm.mutex.RLock() defer lm.mutex.RUnlock() var chests []*TreasureChest for _, chest := range lm.treasureChests { if chest.ZoneID == zoneID { chests = append(chests, chest) } } return chests } // GetPlayerChests returns all chests a player has loot rights to func (lm *LootManager) GetPlayerChests(playerID uint32) []*TreasureChest { lm.mutex.RLock() defer lm.mutex.RUnlock() var chests []*TreasureChest for _, chest := range lm.treasureChests { if chest.HasLootRights(playerID) { chests = append(chests, chest) } } return chests } // GetStatistics returns loot generation statistics func (lm *LootManager) GetStatistics() LootStatistics { return lm.statistics.GetStatistics() } // ReloadLootData reloads loot data from the database func (lm *LootManager) ReloadLootData() error { log.Printf("[LOOT] Reloading loot data...") return lm.LoadAllLootData() } // AddLootTable adds a new loot table func (lm *LootManager) AddLootTable(table *LootTable) error { log.Printf("[LOOT] Adding loot table %d (%s)", table.ID, table.Name) // Database operations would go here return nil } // UpdateLootTable updates an existing loot table func (lm *LootManager) UpdateLootTable(table *LootTable) error { log.Printf("[LOOT] Updating loot table %d (%s)", table.ID, table.Name) // Database operations would go here return nil } // DeleteLootTable removes a loot table func (lm *LootManager) DeleteLootTable(tableID int32) error { log.Printf("[LOOT] Deleting loot table %d", tableID) // Database operations would go here return nil } // AssignSpawnLoot assigns a loot table to a spawn func (lm *LootManager) AssignSpawnLoot(spawnID, tableID int32) error { log.Printf("[LOOT] Assigning loot table %d to spawn %d", tableID, spawnID) // Database operations would go here return nil } // RemoveSpawnLoot removes loot table assignments from a spawn func (lm *LootManager) RemoveSpawnLoot(spawnID int32) error { log.Printf("[LOOT] Removing loot assignments from spawn %d", spawnID) // Database operations would go here return nil } // StartCleanupTimer starts a background timer to clean up expired chests func (lm *LootManager) StartCleanupTimer() { go func() { ticker := time.NewTicker(5 * time.Minute) // Clean up every 5 minutes defer ticker.Stop() for range ticker.C { lm.CleanupExpiredChests() } }() log.Printf("[LOOT] Started chest cleanup timer") }