package loot import ( "database/sql" "fmt" "log" "eq2emu/internal/items" ) // LootSystem represents the complete loot system integration type LootSystem struct { Database *LootDatabase Manager *LootManager ChestService *ChestService PacketService *LootPacketService } // LootSystemConfig holds configuration for the loot system type LootSystemConfig struct { DatabaseConnection *sql.DB ItemMasterList items.MasterItemListService PlayerService PlayerService ZoneService ZoneService ClientService ClientService ItemPacketBuilder ItemPacketBuilder StartCleanupTimer bool } // NewLootSystem creates a complete loot system with all components func NewLootSystem(config *LootSystemConfig) (*LootSystem, error) { if config.DatabaseConnection == nil { return nil, fmt.Errorf("database connection is required") } if config.ItemMasterList == nil { return nil, fmt.Errorf("item master list is required") } // Create database layer database := NewLootDatabase(config.DatabaseConnection) // Load loot data if err := database.LoadAllLootData(); err != nil { return nil, fmt.Errorf("failed to load loot data: %v", err) } // Create loot manager manager := NewLootManager(database, config.ItemMasterList) // Create chest service (optional - requires player and zone services) var chestService *ChestService if config.PlayerService != nil && config.ZoneService != nil { chestService = NewChestService(manager, config.PlayerService, config.ZoneService) } // Create packet service (optional - requires client and item packet builder) var packetService *LootPacketService if config.ClientService != nil && config.ItemPacketBuilder != nil { packetBuilder := NewLootPacketBuilder(config.ItemPacketBuilder) packetService = NewLootPacketService(packetBuilder, config.ClientService) } // Start cleanup timer if requested if config.StartCleanupTimer { manager.StartCleanupTimer() } system := &LootSystem{ Database: database, Manager: manager, ChestService: chestService, PacketService: packetService, } log.Printf("%s Loot system initialized successfully", LogPrefixLoot) return system, nil } // GenerateAndCreateChest generates loot for a spawn and creates a treasure chest func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32, context *LootContext) (*TreasureChest, error) { if ls.ChestService == nil { return nil, fmt.Errorf("chest service not available") } // Generate loot lootResult, err := ls.Manager.GenerateLoot(spawnID, context) if err != nil { return nil, fmt.Errorf("failed to generate loot: %v", err) } // Don't create chest if no loot if lootResult.IsEmpty() { log.Printf("%s No loot generated for spawn %d, not creating chest", LogPrefixLoot, spawnID) return nil, nil } // Create treasure chest chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading, lootResult, context.GroupMembers) if err != nil { return nil, fmt.Errorf("failed to create treasure chest: %v", err) } return chest, nil } // HandlePlayerLootInteraction handles a player's interaction with a chest and sends appropriate packets func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32, interaction ChestInteraction, itemUniqueID int64) error { if ls.ChestService == nil { return fmt.Errorf("chest service not available") } // Handle the interaction result := ls.ChestService.HandleChestInteraction(chestID, playerID, interaction, itemUniqueID) // Send response packet if packet service is available if ls.PacketService != nil { if err := ls.PacketService.SendLootResponse(result, playerID); err != nil { log.Printf("%s Failed to send loot response packet: %v", LogPrefixLoot, err) } // Send updated loot window if chest is still open and has items if result.Success && !result.ChestClosed { chest := ls.Manager.GetTreasureChest(chestID) if chest != nil && !chest.LootResult.IsEmpty() { if err := ls.PacketService.SendLootUpdate(chest, playerID); err != nil { log.Printf("%s Failed to send loot update packet: %v", LogPrefixLoot, err) } } else if chest != nil && chest.LootResult.IsEmpty() { // Send stopped looting packet for empty chest if err := ls.PacketService.SendStoppedLooting(chestID, playerID); err != nil { log.Printf("%s Failed to send stopped looting packet: %v", LogPrefixLoot, err) } } } } // Log the interaction log.Printf("%s Player %d %s chest %d: %s", LogPrefixLoot, playerID, interaction.String(), chestID, result.Message) return nil } // ShowChestToPlayer sends the loot window to a player func (ls *LootSystem) ShowChestToPlayer(chestID int32, playerID uint32) error { if ls.PacketService == nil { return fmt.Errorf("packet service not available") } chest := ls.Manager.GetTreasureChest(chestID) if chest == nil { return fmt.Errorf("chest %d not found", chestID) } // Check loot rights if !chest.HasLootRights(playerID) { return fmt.Errorf("player %d has no loot rights for chest %d", playerID, chestID) } // Send loot update packet return ls.PacketService.SendLootUpdate(chest, playerID) } // GetSystemStatistics returns comprehensive statistics about the loot system func (ls *LootSystem) GetSystemStatistics() (map[string]interface{}, error) { stats := make(map[string]interface{}) // Database statistics if dbStats, err := ls.Database.GetLootStatistics(); err == nil { stats["database"] = dbStats } // Manager statistics stats["generation"] = ls.Manager.GetStatistics() // Active chests count chestCount := 0 ls.Manager.mutex.RLock() chestCount = len(ls.Manager.treasureChests) ls.Manager.mutex.RUnlock() stats["active_chests"] = chestCount return stats, nil } // ReloadAllData reloads all loot data from the database func (ls *LootSystem) ReloadAllData() error { log.Printf("%s Reloading all loot system data", LogPrefixLoot) return ls.Database.LoadAllLootData() } // Shutdown gracefully shuts down the loot system func (ls *LootSystem) Shutdown() error { log.Printf("%s Shutting down loot system", LogPrefixLoot) // Close database connections if err := ls.Database.Close(); err != nil { log.Printf("%s Error closing database: %v", LogPrefixLoot, err) return err } // Clear active chests ls.Manager.mutex.Lock() ls.Manager.treasureChests = make(map[int32]*TreasureChest) ls.Manager.mutex.Unlock() log.Printf("%s Loot system shutdown complete", LogPrefixLoot) return nil } // AddLootTableWithDrops adds a complete loot table with drops in a single transaction func (ls *LootSystem) AddLootTableWithDrops(table *LootTable) error { return ls.Database.AddLootTable(table) } // CreateQuickLootTable creates a simple loot table with basic parameters func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem, minCoin, maxCoin int32, maxItems int16) error { table := &LootTable{ ID: tableID, Name: name, MinCoin: minCoin, MaxCoin: maxCoin, MaxLootItems: maxItems, LootDropProbability: DefaultLootDropProbability, CoinProbability: DefaultCoinProbability, Drops: make([]*LootDrop, len(items)), } for i, item := range items { table.Drops[i] = &LootDrop{ LootTableID: tableID, ItemID: item.ItemID, ItemCharges: item.Charges, EquipItem: item.AutoEquip, Probability: item.Probability, } } return ls.AddLootTableWithDrops(table) } // QuickLootItem represents a simple loot item for quick table creation type QuickLootItem struct { ItemID int32 Charges int16 Probability float32 AutoEquip bool } // AssignLootToSpawns assigns a loot table to multiple spawns func (ls *LootSystem) AssignLootToSpawns(tableID int32, spawnIDs []int32) error { for _, spawnID := range spawnIDs { if err := ls.Database.AddSpawnLoot(spawnID, tableID); err != nil { return fmt.Errorf("failed to assign loot table %d to spawn %d: %v", tableID, spawnID, err) } } log.Printf("%s Assigned loot table %d to %d spawns", LogPrefixLoot, tableID, len(spawnIDs)) return nil } // CreateGlobalLevelLoot creates global loot for a level range func (ls *LootSystem) CreateGlobalLevelLoot(minLevel, maxLevel int8, tableID int32, tier int32) error { global := &GlobalLoot{ Type: GlobalLootTypeLevel, MinLevel: minLevel, MaxLevel: maxLevel, TableID: tableID, LootTier: tier, } // Insert into database query := `INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4) VALUES (?, ?, ?, ?, ?, ?)` _, err := ls.Database.db.Exec(query, "level", tableID, minLevel, maxLevel, tier, 0) if err != nil { return fmt.Errorf("failed to insert global level loot: %v", err) } // Add to in-memory cache ls.Database.mutex.Lock() ls.Database.globalLoot = append(ls.Database.globalLoot, global) ls.Database.mutex.Unlock() log.Printf("%s Created global level loot for levels %d-%d using table %d", LogPrefixLoot, minLevel, maxLevel, tableID) return nil } // GetActiveChestsInZone returns all active chests in a specific zone func (ls *LootSystem) GetActiveChestsInZone(zoneID int32) []*TreasureChest { return ls.Manager.GetZoneChests(zoneID) } // CleanupZoneChests removes all chests from a specific zone func (ls *LootSystem) CleanupZoneChests(zoneID int32) { chests := ls.Manager.GetZoneChests(zoneID) for _, chest := range chests { ls.Manager.RemoveTreasureChest(chest.ID) // Remove from zone if chest service is available if ls.ChestService != nil { ls.ChestService.zoneService.RemoveObjectFromZone(zoneID, chest.ID) } } log.Printf("%s Cleaned up %d chests from zone %d", LogPrefixLoot, len(chests), zoneID) } // ValidateItemsInLootTables checks that all items in loot tables exist in the item master list func (ls *LootSystem) ValidateItemsInLootTables() []ValidationError { var errors []ValidationError ls.Database.mutex.RLock() defer ls.Database.mutex.RUnlock() for tableID, table := range ls.Database.lootTables { for _, drop := range table.Drops { item := ls.Manager.itemMasterList.GetItem(drop.ItemID) if item == nil { errors = append(errors, ValidationError{ Type: "missing_item", TableID: tableID, ItemID: drop.ItemID, Description: fmt.Sprintf("Item %d in loot table %d (%s) does not exist", drop.ItemID, tableID, table.Name), }) } } } if len(errors) > 0 { log.Printf("%s Found %d validation errors in loot tables", LogPrefixLoot, len(errors)) } return errors } // ValidationError represents a loot system validation error type ValidationError struct { Type string `json:"type"` TableID int32 `json:"table_id"` ItemID int32 `json:"item_id,omitempty"` Description string `json:"description"` } // GetLootPreview generates a preview of potential loot without actually creating it func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*LootPreview, error) { tableIDs := ls.Database.GetSpawnLootTables(spawnID) globalLoot := ls.Database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID) for _, global := range globalLoot { tableIDs = append(tableIDs, global.TableID) } preview := &LootPreview{ SpawnID: spawnID, TableIDs: tableIDs, PossibleItems: make([]*LootPreviewItem, 0), MinCoins: 0, MaxCoins: 0, } for _, tableID := range tableIDs { table := ls.Database.GetLootTable(tableID) if table == nil { continue } preview.MinCoins += table.MinCoin preview.MaxCoins += table.MaxCoin for _, drop := range table.Drops { item := ls.Manager.itemMasterList.GetItem(drop.ItemID) if item == nil { continue } previewItem := &LootPreviewItem{ ItemID: drop.ItemID, ItemName: item.Name, Probability: drop.Probability, Tier: item.Details.Tier, } preview.PossibleItems = append(preview.PossibleItems, previewItem) } } return preview, nil } // LootPreview represents a preview of potential loot type LootPreview struct { SpawnID int32 `json:"spawn_id"` TableIDs []int32 `json:"table_ids"` PossibleItems []*LootPreviewItem `json:"possible_items"` MinCoins int32 `json:"min_coins"` MaxCoins int32 `json:"max_coins"` } // LootPreviewItem represents a potential loot item in a preview type LootPreviewItem struct { ItemID int32 `json:"item_id"` ItemName string `json:"item_name"` Probability float32 `json:"probability"` Tier int8 `json:"tier"` }