483 lines
13 KiB
Go
483 lines
13 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
|
|
itemMasterList 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
|
|
func NewLootManager(database *LootDatabase, itemMasterList items.MasterItemListService) *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 = 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 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
|
|
}
|
|
|
|
// Create item instance
|
|
item := items.NewItemFromTemplate(itemTemplate)
|
|
|
|
// 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)
|
|
} |