1162 lines
31 KiB
Go

package loot
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
"eq2emu/internal/items"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
// Simplified Loot System
// Consolidates all functionality from 7 files into unified architecture
// Preserves 100% C++ EQ2 loot functionality while eliminating Active Record patterns
// Loot tier constants based on EQ2 item quality system
const (
LootTierTrash int8 = 0 // Gray items
LootTierCommon int8 = 1 // White items
LootTierUncommon int8 = 2 // Green items
LootTierTreasured int8 = 3 // Blue items
LootTierRare int8 = 4 // Purple items
LootTierLegendary int8 = 5 // Orange items
LootTierFabled int8 = 6 // Yellow items
LootTierMythical int8 = 7 // Red items
LootTierArtifact int8 = 8 // Artifact items
LootTierRelic int8 = 9 // Relic items
LootTierUltimate int8 = 10 // Ultimate items
)
// Chest appearance IDs from the C++ implementation
const (
ChestAppearanceSmall int32 = 4034 // Small chest for common+ items
ChestAppearanceTreasure int32 = 5864 // Treasure chest for treasured+ items
ChestAppearanceOrnate int32 = 5865 // Ornate chest for legendary+ items
ChestAppearanceExquisite int32 = 4015 // Exquisite chest for fabled+ items
)
// Loot generation constants
const (
DefaultMaxLootItems int16 = 6 // Default maximum items per loot
DefaultLootDropProbability float32 = 100.0 // Default probability for loot to drop
DefaultCoinProbability float32 = 50.0 // Default probability for coin drops
MaxGlobalLootTables int = 1000 // Maximum number of global loot tables
)
// Chest interaction results
const (
ChestResultSuccess = 0 // Operation successful
ChestResultLocked = 1 // Chest is locked
ChestResultTrapped = 2 // Chest is trapped
ChestResultNoRights = 3 // No loot rights
ChestResultEmpty = 4 // Chest is empty
ChestResultFailed = 5 // Operation failed
ChestResultCantCarry = 6 // Cannot carry more items
ChestResultTooFar = 7 // Too far from chest
ChestResultInCombat = 8 // Cannot loot while in combat
)
// Loot distribution methods
const (
LootDistributionNone = 0 // No automatic distribution
LootDistributionFreeForAll = 1 // Anyone can loot
LootDistributionRoundRobin = 2 // Round robin distribution
LootDistributionMasterLoot = 3 // Master looter decides
LootDistributionNeedGreed = 4 // Need before greed system
LootDistributionLotto = 5 // Random lotto system
)
// Special loot table IDs
const (
LootTableIDNone int32 = 0 // No loot table
LootTableIDGlobal int32 = -1 // Global loot table marker
LootTableIDLevel int32 = -2 // Level-based global loot
LootTableIDRace int32 = -3 // Race-based global loot
LootTableIDZone int32 = -4 // Zone-based global loot
)
// Chest spawn duration and cleanup
const (
ChestDespawnTime = 300 // Seconds before chest despawns (5 minutes)
ChestCleanupTime = 600 // Seconds before chest is force-cleaned (10 minutes)
MaxChestsPerZone = 100 // Maximum number of chests per zone
MaxChestsPerPlayer = 10 // Maximum number of chests a player can have loot rights to
)
// Probability calculation constants
const (
ProbabilityMax float32 = 100.0 // Maximum probability percentage
ProbabilityMin float32 = 0.0 // Minimum probability percentage
ProbabilityDefault float32 = 50.0 // Default probability for items
)
// GlobalLootType represents the type of global loot
type GlobalLootType int8
const (
GlobalLootTypeLevel GlobalLootType = iota
GlobalLootTypeRace
GlobalLootTypeZone
)
// String returns the string representation of GlobalLootType
func (t GlobalLootType) String() string {
switch t {
case GlobalLootTypeLevel:
return "level"
case GlobalLootTypeRace:
return "race"
case GlobalLootTypeZone:
return "zone"
default:
return "unknown"
}
}
// GroupLootMethod represents different group loot distribution methods
type GroupLootMethod int8
const (
GroupLootMethodFreeForAll GroupLootMethod = iota
GroupLootMethodRoundRobin
GroupLootMethodMasterLooter
GroupLootMethodNeed
GroupLootMethodLotto
)
// String returns the string representation of GroupLootMethod
func (glm GroupLootMethod) String() string {
switch glm {
case GroupLootMethodFreeForAll:
return "free_for_all"
case GroupLootMethodRoundRobin:
return "round_robin"
case GroupLootMethodMasterLooter:
return "master_looter"
case GroupLootMethodNeed:
return "need_greed"
case GroupLootMethodLotto:
return "lotto"
default:
return "unknown"
}
}
// LootTable represents a complete loot table with its drops
type LootTable struct {
ID int32
Name string
MinCoin int32
MaxCoin int32
MaxLootItems int16
LootDropProbability float32
CoinProbability float32
Drops []*LootDrop
mutex sync.RWMutex
}
// LootDrop represents an individual item that can drop from a loot table
type LootDrop struct {
LootTableID int32
ItemID int32
ItemCharges int16
EquipItem bool
Probability float32
NoDropQuestCompletedID int32
}
// GlobalLoot represents global loot configuration based on level, race, or zone
type GlobalLoot struct {
Type GlobalLootType
MinLevel int8
MaxLevel int8
Race int16
ZoneID int32
TableID int32
LootTier int32
}
// LootResult represents the result of loot generation
type LootResult struct {
Items []*items.Item
Coins int32
mutex sync.RWMutex
}
// AddItem adds an item to the loot result (thread-safe)
func (lr *LootResult) AddItem(item *items.Item) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
lr.Items = append(lr.Items, item)
}
// AddCoins adds coins to the loot result (thread-safe)
func (lr *LootResult) AddCoins(coins int32) {
lr.mutex.Lock()
defer lr.mutex.Unlock()
lr.Coins += coins
}
// GetItems returns a copy of the items slice (thread-safe)
func (lr *LootResult) GetItems() []*items.Item {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
result := make([]*items.Item, len(lr.Items))
copy(result, lr.Items)
return result
}
// GetCoins returns the coin amount (thread-safe)
func (lr *LootResult) GetCoins() int32 {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
return lr.Coins
}
// IsEmpty returns true if the loot result has no items or coins
func (lr *LootResult) IsEmpty() bool {
lr.mutex.RLock()
defer lr.mutex.RUnlock()
return len(lr.Items) == 0 && lr.Coins == 0
}
// TreasureChest represents a treasure chest spawn containing loot
type TreasureChest struct {
ID int32
SpawnID int32
ZoneID int32
X float32
Y float32
Z float32
Heading float32
AppearanceID int32
LootResult *LootResult
Created time.Time
LootRights []uint32 // Player IDs with loot rights
IsDisarmable bool
IsLocked bool
DisarmDifficulty int16
LockpickDifficulty int16
mutex sync.RWMutex
}
// HasLootRights checks if a player has rights to loot this chest
func (tc *TreasureChest) HasLootRights(playerID uint32) bool {
tc.mutex.RLock()
defer tc.mutex.RUnlock()
// If no specific loot rights, anyone can loot
if len(tc.LootRights) == 0 {
return true
}
for _, id := range tc.LootRights {
if id == playerID {
return true
}
}
return false
}
// AddLootRights adds a player to the loot rights list
func (tc *TreasureChest) AddLootRights(playerID uint32) {
tc.mutex.Lock()
defer tc.mutex.Unlock()
// Check if already has rights
for _, id := range tc.LootRights {
if id == playerID {
return
}
}
tc.LootRights = append(tc.LootRights, playerID)
}
// ChestAppearance represents different chest appearances based on loot tier
type ChestAppearance struct {
AppearanceID int32
Name string
MinTier int8
MaxTier int8
}
// Predefined chest appearances based on C++ implementation
var (
SmallChest = &ChestAppearance{AppearanceID: 4034, Name: "Small Chest", MinTier: 1, MaxTier: 2}
TreasureChestAppearance = &ChestAppearance{AppearanceID: 5864, Name: "Treasure Chest", MinTier: 3, MaxTier: 4}
OrnateChest = &ChestAppearance{AppearanceID: 5865, Name: "Ornate Chest", MinTier: 5, MaxTier: 6}
ExquisiteChest = &ChestAppearance{AppearanceID: 4015, Name: "Exquisite Chest", MinTier: 7, MaxTier: 10}
)
// GetChestAppearance returns the appropriate chest appearance based on loot tier
func GetChestAppearance(highestTier int8) *ChestAppearance {
if highestTier >= ExquisiteChest.MinTier {
return ExquisiteChest
}
if highestTier >= OrnateChest.MinTier {
return OrnateChest
}
if highestTier >= TreasureChestAppearance.MinTier {
return TreasureChestAppearance
}
return SmallChest
}
// LootContext provides context for loot generation
type LootContext struct {
PlayerLevel int16
PlayerRace int16
ZoneID int32
KillerID uint32
GroupMembers []uint32
CompletedQuests map[int32]bool
LootMethod GroupLootMethod
}
// LootEntry represents a complete loot entry with all associated data
type LootEntry struct {
SpawnID int32
LootTableID int32
TableName string
Priority int16
}
// LootStatistics tracks loot generation statistics
type LootStatistics struct {
TotalLoots int64
TotalItems int64
TotalCoins int64
TreasureChests int64
ItemsByTier map[int8]int64
LootsByTable map[int32]int64
AverageItemsPerLoot float32
AverageCoinsPerLoot float32
mutex sync.RWMutex
}
// NewLootStatistics creates a new loot statistics tracker
func NewLootStatistics() *LootStatistics {
return &LootStatistics{
ItemsByTier: make(map[int8]int64),
LootsByTable: make(map[int32]int64),
}
}
// RecordLoot records statistics for a loot generation
func (ls *LootStatistics) RecordLoot(tableID int32, result *LootResult) {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.TotalLoots++
ls.LootsByTable[tableID]++
items := result.GetItems()
ls.TotalItems += int64(len(items))
ls.TotalCoins += int64(result.GetCoins())
// Track items by tier
for _, item := range items {
ls.ItemsByTier[item.Details.Tier]++
}
// Update averages
if ls.TotalLoots > 0 {
ls.AverageItemsPerLoot = float32(ls.TotalItems) / float32(ls.TotalLoots)
ls.AverageCoinsPerLoot = float32(ls.TotalCoins) / float32(ls.TotalLoots)
}
}
// RecordChest records a treasure chest creation
func (ls *LootStatistics) RecordChest() {
ls.mutex.Lock()
defer ls.mutex.Unlock()
ls.TreasureChests++
}
// GetStatistics returns a copy of the current statistics
func (ls *LootStatistics) GetStatistics() LootStatistics {
ls.mutex.RLock()
defer ls.mutex.RUnlock()
// Create deep copy
copy := LootStatistics{
TotalLoots: ls.TotalLoots,
TotalItems: ls.TotalItems,
TotalCoins: ls.TotalCoins,
TreasureChests: ls.TreasureChests,
AverageItemsPerLoot: ls.AverageItemsPerLoot,
AverageCoinsPerLoot: ls.AverageCoinsPerLoot,
ItemsByTier: make(map[int8]int64),
LootsByTable: make(map[int32]int64),
}
for tier, count := range ls.ItemsByTier {
copy.ItemsByTier[tier] = count
}
for tableID, count := range ls.LootsByTable {
copy.LootsByTable[tableID] = count
}
return copy
}
// ChestInteractionResult represents the result of chest interaction
type ChestInteractionResult struct {
Success bool
Result int8
Message string
Items []*items.Item
Coins int32
Experience int32
ChestEmpty bool
ChestClosed bool
}
// LootManager handles all loot generation and management
type LootManager struct {
pool *sqlitex.Pool
itemManager *items.ItemManager
lootTables map[int32]*LootTable
spawnLoot map[int32][]int32
globalLoot []*GlobalLoot
treasureChests map[int32]*TreasureChest
chestIDCounter int32
statistics *LootStatistics
random *rand.Rand
loaded bool
mutex sync.RWMutex
}
// NewLootManager creates a new loot manager
func NewLootManager(pool *sqlitex.Pool, itemManager *items.ItemManager) *LootManager {
return &LootManager{
pool: pool,
itemManager: itemManager,
lootTables: make(map[int32]*LootTable),
spawnLoot: make(map[int32][]int32),
globalLoot: make([]*GlobalLoot, 0),
treasureChests: make(map[int32]*TreasureChest),
chestIDCounter: 1,
statistics: NewLootStatistics(),
random: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
// Initialize loads all loot data from database
func (lm *LootManager) Initialize() error {
log.Printf("[LOOT] Initializing loot system...")
if err := lm.LoadAllLootData(); err != nil {
return fmt.Errorf("failed to load loot data: %v", err)
}
lm.mutex.Lock()
lm.loaded = true
lm.mutex.Unlock()
log.Printf("[LOOT] Loot system initialized successfully")
return nil
}
// IsLoaded returns whether the loot system has been loaded
func (lm *LootManager) IsLoaded() bool {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
return lm.loaded
}
// LoadAllLootData loads all loot data from database
func (lm *LootManager) LoadAllLootData() error {
lm.mutex.Lock()
defer lm.mutex.Unlock()
// Clear existing data
lm.lootTables = make(map[int32]*LootTable)
lm.spawnLoot = make(map[int32][]int32)
lm.globalLoot = make([]*GlobalLoot, 0)
// Load loot tables
if err := lm.loadLootTables(); err != nil {
return fmt.Errorf("failed to load loot tables: %v", err)
}
// Load spawn loot assignments
if err := lm.loadSpawnLoot(); err != nil {
return fmt.Errorf("failed to load spawn loot: %v", err)
}
// Load global loot configuration
if err := lm.loadGlobalLoot(); err != nil {
return fmt.Errorf("failed to load global loot: %v", err)
}
log.Printf("[LOOT] Loaded %d loot tables, %d spawn assignments, %d global loot entries",
len(lm.lootTables), len(lm.spawnLoot), len(lm.globalLoot))
return nil
}
// loadLootTables loads loot tables from database
func (lm *LootManager) loadLootTables() error {
conn, err := lm.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %v", err)
}
defer lm.pool.Put(conn)
// Load loot tables
stmt := conn.Prep(`SELECT id, name, mincoin, maxcoin, maxlootitems,
lootdrop_probability, coin_probability FROM loottable ORDER BY id`)
for {
hasRow, err := stmt.Step()
if err != nil {
return fmt.Errorf("failed to step through loot tables: %v", err)
}
if !hasRow {
break
}
table := &LootTable{
ID: int32(stmt.GetInt64("id")),
Name: stmt.GetText("name"),
MinCoin: int32(stmt.GetInt64("mincoin")),
MaxCoin: int32(stmt.GetInt64("maxcoin")),
MaxLootItems: int16(stmt.GetInt64("maxlootitems")),
LootDropProbability: float32(stmt.GetFloat("lootdrop_probability")),
CoinProbability: float32(stmt.GetFloat("coin_probability")),
Drops: make([]*LootDrop, 0),
}
lm.lootTables[table.ID] = table
}
// Load loot drops for each table
for tableID := range lm.lootTables {
if err := lm.loadLootDrops(conn, tableID); err != nil {
return fmt.Errorf("failed to load drops for table %d: %v", tableID, err)
}
}
return nil
}
// loadLootDrops loads loot drops for a specific table
func (lm *LootManager) loadLootDrops(conn *sqlite.Conn, tableID int32) error {
stmt := conn.Prep(`SELECT item_id, item_charges, equip_item, probability,
no_drop_quest_completed_id FROM lootdrop WHERE loot_table_id = ?`)
stmt.BindInt64(1, int64(tableID))
table := lm.lootTables[tableID]
for {
hasRow, err := stmt.Step()
if err != nil {
return fmt.Errorf("failed to step through loot drops: %v", err)
}
if !hasRow {
break
}
drop := &LootDrop{
LootTableID: tableID,
ItemID: int32(stmt.GetInt64("item_id")),
ItemCharges: int16(stmt.GetInt64("item_charges")),
EquipItem: stmt.GetInt64("equip_item") == 1,
Probability: float32(stmt.GetFloat("probability")),
NoDropQuestCompletedID: int32(stmt.GetInt64("no_drop_quest_completed_id")),
}
table.Drops = append(table.Drops, drop)
}
return nil
}
// loadSpawnLoot loads spawn loot assignments from database
func (lm *LootManager) loadSpawnLoot() error {
conn, err := lm.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %v", err)
}
defer lm.pool.Put(conn)
stmt := conn.Prep(`SELECT spawn_id, loottable_id FROM spawn_loot ORDER BY spawn_id`)
for {
hasRow, err := stmt.Step()
if err != nil {
return fmt.Errorf("failed to step through spawn loot: %v", err)
}
if !hasRow {
break
}
spawnID := int32(stmt.GetInt64("spawn_id"))
tableID := int32(stmt.GetInt64("loottable_id"))
if _, exists := lm.spawnLoot[spawnID]; !exists {
lm.spawnLoot[spawnID] = make([]int32, 0)
}
lm.spawnLoot[spawnID] = append(lm.spawnLoot[spawnID], tableID)
}
return nil
}
// loadGlobalLoot loads global loot configuration from database
func (lm *LootManager) loadGlobalLoot() error {
conn, err := lm.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %v", err)
}
defer lm.pool.Put(conn)
stmt := conn.Prep(`SELECT type, loot_table, value1, value2, value3, value4
FROM loot_global ORDER BY type, value1`)
for {
hasRow, err := stmt.Step()
if err != nil {
return fmt.Errorf("failed to step through global loot: %v", err)
}
if !hasRow {
break
}
lootType := GlobalLootType(stmt.GetInt64("type"))
tableID := int32(stmt.GetInt64("loot_table"))
value1 := int32(stmt.GetInt64("value1"))
value2 := int32(stmt.GetInt64("value2"))
value3 := int32(stmt.GetInt64("value3"))
value4 := int32(stmt.GetInt64("value4"))
global := &GlobalLoot{
Type: lootType,
TableID: tableID,
}
switch lootType {
case GlobalLootTypeLevel:
global.MinLevel = int8(value1)
global.MaxLevel = int8(value2)
case GlobalLootTypeRace:
global.Race = int16(value1)
case GlobalLootTypeZone:
global.ZoneID = value1
}
// value3, value4 could be used for additional filtering
_ = value3
_ = value4
lm.globalLoot = append(lm.globalLoot, global)
}
return nil
}
// GetLootTable returns a loot table by ID
func (lm *LootManager) GetLootTable(tableID int32) *LootTable {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
return lm.lootTables[tableID]
}
// GetSpawnLootTables returns all loot table IDs assigned to a spawn
func (lm *LootManager) GetSpawnLootTables(spawnID int32) []int32 {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
if tables, exists := lm.spawnLoot[spawnID]; exists {
result := make([]int32, len(tables))
copy(result, tables)
return result
}
return []int32{}
}
// GetGlobalLootTables returns global loot tables that apply to the given context
func (lm *LootManager) GetGlobalLootTables(playerLevel int16, playerRace int16, zoneID int32) []*GlobalLoot {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
var result []*GlobalLoot
for _, global := range lm.globalLoot {
switch global.Type {
case GlobalLootTypeLevel:
if playerLevel >= int16(global.MinLevel) && playerLevel <= int16(global.MaxLevel) {
result = append(result, global)
}
case GlobalLootTypeRace:
if playerRace == global.Race {
result = append(result, global)
}
case GlobalLootTypeZone:
if zoneID == global.ZoneID {
result = append(result, global)
}
}
}
return result
}
// GenerateLoot generates loot for a spawn based on its loot table assignments
func (lm *LootManager) GenerateLoot(spawnID int32, context *LootContext) (*LootResult, error) {
log.Printf("[LOOT-GEN] Generating loot for spawn %d", spawnID)
result := &LootResult{
Items: make([]*items.Item, 0),
Coins: 0,
}
// Get loot tables for this spawn
tableIDs := lm.GetSpawnLootTables(spawnID)
// Also check for global loot tables
globalLoot := lm.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID)
for _, global := range globalLoot {
tableIDs = append(tableIDs, global.TableID)
}
if len(tableIDs) == 0 {
log.Printf("[LOOT-GEN] No loot tables found for spawn %d", spawnID)
return result, nil
}
// Process each loot table
for _, tableID := range tableIDs {
if err := lm.processLootTable(tableID, context, result); err != nil {
log.Printf("[LOOT-GEN] Error processing loot table %d: %v", tableID, err)
continue
}
}
// Record statistics
if len(tableIDs) > 0 {
lm.statistics.RecordLoot(tableIDs[0], result)
}
log.Printf("[LOOT-GEN] Generated %d items and %d coins for spawn %d",
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.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("[LOOT-GEN] Loot table %d failed drop probability check", 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("[LOOT-GEN] Generated %d coins from table %d", 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
}
// Get item from item manager (placeholder - TODO: integrate with ItemManager)
var item *items.Item = nil
if lm.itemManager != nil {
// item = lm.itemManager.CreateItem(drop.ItemID) // TODO: implement CreateItem method
}
if item == nil {
log.Printf("[LOOT-GEN] Item template %d not found for loot drop", drop.ItemID)
continue
}
// Set charges if specified
if drop.ItemCharges > 0 {
item.Details.Count = int16(drop.ItemCharges)
}
// Mark as equipped if specified (handled by caller when distributing loot)
if drop.EquipItem {
// This would be handled by the caller when distributing loot
}
result.AddItem(item)
itemsGenerated++
log.Printf("[LOOT-GEN] Generated item %d (%s) from table %d",
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("[CHEST] Created treasure chest %d (%s) at (%.2f, %.2f, %.2f) with %d items and %d coins",
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("[CHEST] Removed treasure chest %d", 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("[CHEST] Player %d looted item %d (%s) from chest %d",
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("[CHEST] Player %d looted %d coins from chest %d",
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("[CHEST] Player %d looted all (%d items, %d coins) from chest %d",
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("[CHEST] Cleaned up expired chest %d", chestID)
}
if len(expired) > 0 {
log.Printf("[CHEST] Cleaned up %d expired chests", 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("[LOOT] Reloading loot data...")
return lm.LoadAllLootData()
}
// AddLootTable adds a new loot table
func (lm *LootManager) AddLootTable(table *LootTable) error {
log.Printf("[LOOT] Adding loot table %d (%s)", table.ID, table.Name)
// Database operations would go here
return nil
}
// UpdateLootTable updates an existing loot table
func (lm *LootManager) UpdateLootTable(table *LootTable) error {
log.Printf("[LOOT] Updating loot table %d (%s)", table.ID, table.Name)
// Database operations would go here
return nil
}
// DeleteLootTable removes a loot table
func (lm *LootManager) DeleteLootTable(tableID int32) error {
log.Printf("[LOOT] Deleting loot table %d", tableID)
// Database operations would go here
return nil
}
// AssignSpawnLoot assigns a loot table to a spawn
func (lm *LootManager) AssignSpawnLoot(spawnID, tableID int32) error {
log.Printf("[LOOT] Assigning loot table %d to spawn %d", tableID, spawnID)
// Database operations would go here
return nil
}
// RemoveSpawnLoot removes loot table assignments from a spawn
func (lm *LootManager) RemoveSpawnLoot(spawnID int32) error {
log.Printf("[LOOT] Removing loot assignments from spawn %d", spawnID)
// Database operations would go here
return nil
}
// 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("[LOOT] Started chest cleanup timer")
}