434 lines
13 KiB
Go
434 lines
13 KiB
Go
package loot
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
// @TODO: Fix MasterItemListService type import - temporarily commented out
|
|
// "eq2emu/internal/items"
|
|
"zombiezen.com/go/sqlite/sqlitex"
|
|
)
|
|
|
|
// 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 {
|
|
DatabasePool *sqlitex.Pool
|
|
// @TODO: Fix MasterItemListService type import
|
|
ItemMasterList interface{} // was 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.DatabasePool == nil {
|
|
return nil, fmt.Errorf("database pool is required")
|
|
}
|
|
|
|
if config.ItemMasterList == nil {
|
|
return nil, fmt.Errorf("item master list is required")
|
|
}
|
|
|
|
// Create database layer
|
|
database := NewLootDatabase(config.DatabasePool)
|
|
|
|
// 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]any, error) {
|
|
stats := make(map[string]any)
|
|
|
|
// 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
|
|
ctx := context.Background()
|
|
conn, err := ls.Database.pool.Take(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get database connection: %w", err)
|
|
}
|
|
defer ls.Database.pool.Put(conn)
|
|
|
|
query := `INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4) VALUES (?, ?, ?, ?, ?, ?)`
|
|
err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{
|
|
Args: []any{"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 {
|
|
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
|
|
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
|
|
var item interface{} = nil
|
|
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 {
|
|
// @TODO: Fix MasterItemListService type import - itemMasterList method calls disabled
|
|
// item := ls.Manager.itemMasterList.GetItem(drop.ItemID)
|
|
var item interface{} = nil
|
|
if item == nil {
|
|
continue
|
|
}
|
|
|
|
// @TODO: Fix MasterItemListService type import - preview item creation disabled
|
|
previewItem := &LootPreviewItem{
|
|
ItemID: drop.ItemID,
|
|
ItemName: "[DISABLED - TYPE IMPORT ISSUE]",
|
|
Probability: drop.Probability,
|
|
Tier: 0, // item.Details.Tier disabled
|
|
}
|
|
|
|
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"`
|
|
}
|