1162 lines
31 KiB
Go
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")
|
|
} |