package loot import ( "fmt" "log" "math/rand" "sync" "time" "eq2emu/internal/items" ) // LootManager handles all loot generation and management type LootManager struct { database *LootDatabase // @TODO: Fix MasterItemListService type import itemMasterList interface{} // was items.MasterItemListService statistics *LootStatistics treasureChests map[int32]*TreasureChest // chest_id -> TreasureChest chestIDCounter int32 random *rand.Rand mutex sync.RWMutex } // NewLootManager creates a new loot manager // @TODO: Fix MasterItemListService type import func NewLootManager(database *LootDatabase, itemMasterList interface{}) *LootManager { return &LootManager{ database: database, itemMasterList: itemMasterList, statistics: NewLootStatistics(), treasureChests: make(map[int32]*TreasureChest), chestIDCounter: 1, random: rand.New(rand.NewSource(time.Now().UnixNano())), } } // GenerateLoot generates loot for a spawn based on its loot table assignments func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootResult, error) { log.Printf("%s Generating loot for spawn %d", LogPrefixGeneration, spawnID) result := &LootResult{ Items: make([]*items.Item, 0), Coins: 0, } // Get loot tables for this spawn tableIDs := lm.database.GetSpawnLootTables(spawnID) // Also check for global loot tables globalLoot := lm.database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID) for _, global := range globalLoot { tableIDs = append(tableIDs, global.TableID) } if len(tableIDs) == 0 { log.Printf("%s No loot tables found for spawn %d", LogPrefixGeneration, spawnID) return result, nil } // Process each loot table for _, tableID := range tableIDs { if err := lm.processLootTable(tableID, context, result); err != nil { log.Printf("%s Error processing loot table %d: %v", LogPrefixGeneration, tableID, err) continue } } // Record statistics if len(tableIDs) > 0 { lm.statistics.RecordLoot(tableIDs[0], result) // Use first table for stats } log.Printf("%s Generated %d items and %d coins for spawn %d", LogPrefixGeneration, 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.database.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("%s Loot table %d failed drop probability check", LogPrefixGeneration, 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("%s Generated %d coins from table %d", LogPrefixGeneration, 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 } // @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled // Get item template // itemTemplate := lm.itemMasterList.GetItem(drop.ItemID) // if itemTemplate == nil { // log.Printf("%s Item template %d not found for loot drop", LogPrefixGeneration, drop.ItemID) // continue // } var itemTemplate interface{} = nil if itemTemplate == nil { log.Printf("%s Item template %d not found for loot drop (disabled due to type import issue)", LogPrefixGeneration, drop.ItemID) continue } // @TODO: Fix MasterItemListService type import - item creation disabled // Create item instance // item := items.NewItemFromTemplate(itemTemplate) var item *items.Item = nil // Set charges if specified if drop.ItemCharges > 0 { item.Details.Count = drop.ItemCharges } // Mark as equipped if specified if drop.EquipItem { // This would be handled by the caller when distributing loot // For now, we just note it in the item } result.AddItem(item) itemsGenerated++ log.Printf("%s Generated item %d (%s) from table %d", LogPrefixGeneration, 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("%s Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins", LogPrefixChest, 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("%s Removed treasure chest %d", LogPrefixChest, 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("%s Player %d looted item %d (%s) from chest %d", LogPrefixChest, 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("%s Player %d looted %d coins from chest %d", LogPrefixChest, 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("%s Player %d looted all (%d items, %d coins) from chest %d", LogPrefixChest, 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("%s Cleaned up expired chest %d", LogPrefixChest, chestID) } if len(expired) > 0 { log.Printf("%s Cleaned up %d expired chests", LogPrefixChest, 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("%s Reloading loot data...", LogPrefixLoot) return lm.database.ReloadLootData() } // AddLootTable adds a new loot table func (lm *LootManager) AddLootTable(table *LootTable) error { log.Printf("%s Adding loot table %d (%s)", LogPrefixLoot, table.ID, table.Name) return lm.database.AddLootTable(table) } // UpdateLootTable updates an existing loot table func (lm *LootManager) UpdateLootTable(table *LootTable) error { log.Printf("%s Updating loot table %d (%s)", LogPrefixLoot, table.ID, table.Name) return lm.database.UpdateLootTable(table) } // DeleteLootTable removes a loot table func (lm *LootManager) DeleteLootTable(tableID int32) error { log.Printf("%s Deleting loot table %d", LogPrefixLoot, tableID) return lm.database.DeleteLootTable(tableID) } // AssignSpawnLoot assigns a loot table to a spawn func (lm *LootManager) AssignSpawnLoot(spawnID, tableID int32) error { log.Printf("%s Assigning loot table %d to spawn %d", LogPrefixLoot, tableID, spawnID) return lm.database.AddSpawnLoot(spawnID, tableID) } // RemoveSpawnLoot removes loot table assignments from a spawn func (lm *LootManager) RemoveSpawnLoot(spawnID int32) error { log.Printf("%s Removing loot assignments from spawn %d", LogPrefixLoot, spawnID) return lm.database.DeleteSpawnLoot(spawnID) } // 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("%s Started chest cleanup timer", LogPrefixLoot) }