eq2go/internal/items/loot/manager.go

494 lines
14 KiB
Go

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 any // 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 any) *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 any = 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)
}