eq2go/internal/items/loot/loot_test.go

671 lines
18 KiB
Go

package loot
import (
"database/sql"
"testing"
"time"
"eq2emu/internal/items"
_ "zombiezen.com/go/sqlite"
)
// Test helper functions and mock implementations
// MockItemMasterList implements items.MasterItemListService for testing
type MockItemMasterList struct {
items map[int32]*items.Item
}
func NewMockItemMasterList() *MockItemMasterList {
return &MockItemMasterList{
items: make(map[int32]*items.Item),
}
}
func (m *MockItemMasterList) GetItem(itemID int32) *items.Item {
return m.items[itemID]
}
func (m *MockItemMasterList) AddTestItem(itemID int32, name string, tier int8) {
item := &items.Item{
Name: name,
Details: items.ItemDetails{
ItemID: itemID,
Tier: tier,
},
GenericInfo: items.ItemGenericInfo{
ItemType: items.ItemTypeNormal,
},
}
item.Details.UniqueID = items.NextUniqueItemID()
m.items[itemID] = item
}
// MockPlayerService implements PlayerService for testing
type MockPlayerService struct {
playerPositions map[uint32][5]float32 // x, y, z, heading, zoneID
inventorySpace map[uint32]int
combat map[uint32]bool
skills map[uint32]map[string]int32
}
func NewMockPlayerService() *MockPlayerService {
return &MockPlayerService{
playerPositions: make(map[uint32][5]float32),
inventorySpace: make(map[uint32]int),
combat: make(map[uint32]bool),
skills: make(map[uint32]map[string]int32),
}
}
func (m *MockPlayerService) GetPlayerPosition(playerID uint32) (x, y, z, heading float32, zoneID int32, err error) {
pos := m.playerPositions[playerID]
return pos[0], pos[1], pos[2], pos[3], int32(pos[4]), nil
}
func (m *MockPlayerService) IsPlayerInCombat(playerID uint32) bool {
return m.combat[playerID]
}
func (m *MockPlayerService) CanPlayerCarryItems(playerID uint32, itemCount int) bool {
space := m.inventorySpace[playerID]
return space >= itemCount
}
func (m *MockPlayerService) AddItemsToPlayer(playerID uint32, items []*items.Item) error {
return nil
}
func (m *MockPlayerService) AddCoinsToPlayer(playerID uint32, coins int32) error {
return nil
}
func (m *MockPlayerService) GetPlayerSkillValue(playerID uint32, skillName string) int32 {
if skills, exists := m.skills[playerID]; exists {
return skills[skillName]
}
return 0
}
func (m *MockPlayerService) AddPlayerExperience(playerID uint32, experience int32, skillName string) error {
return nil
}
func (m *MockPlayerService) SendMessageToPlayer(playerID uint32, message string) error {
return nil
}
func (m *MockPlayerService) SetPlayerPosition(playerID uint32, x, y, z, heading float32, zoneID int32) {
m.playerPositions[playerID] = [5]float32{x, y, z, heading, float32(zoneID)}
}
func (m *MockPlayerService) SetInventorySpace(playerID uint32, space int) {
m.inventorySpace[playerID] = space
}
// MockZoneService implements ZoneService for testing
type MockZoneService struct {
rules map[int32]map[string]any
objects map[int32]map[int32]any // zoneID -> objectID
}
func NewMockZoneService() *MockZoneService {
return &MockZoneService{
rules: make(map[int32]map[string]any),
objects: make(map[int32]map[int32]any),
}
}
func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (any, error) {
if rules, exists := m.rules[zoneID]; exists {
return rules[ruleName], nil
}
return true, nil // Default to enabled
}
func (m *MockZoneService) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) {
objectID := int32(len(m.objects[zoneID]) + 1)
if m.objects[zoneID] == nil {
m.objects[zoneID] = make(map[int32]any)
}
m.objects[zoneID][objectID] = struct{}{}
return objectID, nil
}
func (m *MockZoneService) RemoveObjectFromZone(zoneID int32, objectID int32) error {
if objects, exists := m.objects[zoneID]; exists {
delete(objects, objectID)
}
return nil
}
func (m *MockZoneService) GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32 {
dx := x1 - x2
dy := y1 - y2
dz := z1 - z2
return float32(dx*dx + dy*dy + dz*dz) // Simplified distance calculation
}
// Test database setup
func setupTestDatabase(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
schema := `
CREATE TABLE loottable (
id INTEGER PRIMARY KEY,
name TEXT,
mincoin INTEGER DEFAULT 0,
maxcoin INTEGER DEFAULT 0,
maxlootitems INTEGER DEFAULT 6,
lootdrop_probability REAL DEFAULT 100.0,
coin_probability REAL DEFAULT 50.0
);
CREATE TABLE lootdrop (
loot_table_id INTEGER,
item_id INTEGER,
item_charges INTEGER DEFAULT 1,
equip_item INTEGER DEFAULT 0,
probability REAL DEFAULT 100.0,
no_drop_quest_completed_id INTEGER DEFAULT 0
);
CREATE TABLE spawn_loot (
spawn_id INTEGER,
loottable_id INTEGER
);
CREATE TABLE loot_global (
type TEXT,
loot_table INTEGER,
value1 INTEGER,
value2 INTEGER,
value3 INTEGER,
value4 INTEGER
);
`
if _, err := db.Exec(schema); err != nil {
t.Fatalf("Failed to create test schema: %v", err)
}
return db
}
// Insert test data
func insertTestLootData(t *testing.T, db *sql.DB) {
// Test loot table
_, err := db.Exec(`
INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability)
VALUES (1, 'Test Loot Table', 10, 50, 3, 100.0, 75.0)
`)
if err != nil {
t.Fatalf("Failed to insert test loot table: %v", err)
}
// Test loot drops
lootDrops := []struct {
tableID int32
itemID int32
charges int16
probability float32
}{
{1, 101, 1, 100.0}, // Always drops
{1, 102, 5, 50.0}, // 50% chance
{1, 103, 1, 25.0}, // 25% chance
}
for _, drop := range lootDrops {
_, err := db.Exec(`
INSERT INTO lootdrop (loot_table_id, item_id, item_charges, probability)
VALUES (?, ?, ?, ?)
`, drop.tableID, drop.itemID, drop.charges, drop.probability)
if err != nil {
t.Fatalf("Failed to insert loot drop: %v", err)
}
}
// Test spawn loot assignment
_, err = db.Exec(`
INSERT INTO spawn_loot (spawn_id, loottable_id)
VALUES (1001, 1)
`)
if err != nil {
t.Fatalf("Failed to insert spawn loot: %v", err)
}
// Test global loot
_, err = db.Exec(`
INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4)
VALUES ('level', 1, 10, 20, 1, 0)
`)
if err != nil {
t.Fatalf("Failed to insert global loot: %v", err)
}
}
// Test Functions
func TestNewLootDatabase(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
lootDB := NewLootDatabase(db)
if lootDB == nil {
t.Fatal("Expected non-nil LootDatabase")
}
if lootDB.db != db {
t.Error("Expected database connection to be set")
}
if len(lootDB.queries) == 0 {
t.Error("Expected queries to be prepared")
}
}
func TestLoadLootData(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
insertTestLootData(t, db)
lootDB := NewLootDatabase(db)
err := lootDB.LoadAllLootData()
if err != nil {
t.Fatalf("Failed to load loot data: %v", err)
}
// Test loot table loaded
table := lootDB.GetLootTable(1)
if table == nil {
t.Fatal("Expected to find loot table 1")
}
if table.Name != "Test Loot Table" {
t.Errorf("Expected table name 'Test Loot Table', got '%s'", table.Name)
}
if len(table.Drops) != 3 {
t.Errorf("Expected 3 loot drops, got %d", len(table.Drops))
}
// Test spawn loot assignment
tables := lootDB.GetSpawnLootTables(1001)
if len(tables) != 1 || tables[0] != 1 {
t.Errorf("Expected spawn 1001 to have loot table 1, got %v", tables)
}
// Test global loot
globalLoot := lootDB.GetGlobalLootTables(15, 0, 0)
if len(globalLoot) != 1 {
t.Errorf("Expected 1 global loot entry for level 15, got %d", len(globalLoot))
}
}
func TestLootManager(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
insertTestLootData(t, db)
lootDB := NewLootDatabase(db)
err := lootDB.LoadAllLootData()
if err != nil {
t.Fatalf("Failed to load loot data: %v", err)
}
// Create mock item master list
itemList := NewMockItemMasterList()
itemList.AddTestItem(101, "Test Sword", LootTierCommon)
itemList.AddTestItem(102, "Test Potion", LootTierCommon)
itemList.AddTestItem(103, "Test Shield", LootTierTreasured)
lootManager := NewLootManager(lootDB, itemList)
// Test loot generation
context := &LootContext{
PlayerLevel: 15,
PlayerRace: 1,
ZoneID: 100,
KillerID: 1,
GroupMembers: []uint32{1},
CompletedQuests: make(map[int32]bool),
LootMethod: GroupLootMethodFreeForAll,
}
result, err := lootManager.GenerateLoot(1001, context)
if err != nil {
t.Fatalf("Failed to generate loot: %v", err)
}
if result == nil {
t.Fatal("Expected non-nil loot result")
}
// Should have at least one item (100% drop chance for item 101)
items := result.GetItems()
if len(items) == 0 {
t.Error("Expected at least one item in loot result")
}
// Should have coins (75% probability)
coins := result.GetCoins()
t.Logf("Generated %d items and %d coins", len(items), coins)
}
func TestTreasureChestCreation(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
lootDB := NewLootDatabase(db)
itemList := NewMockItemMasterList()
itemList.AddTestItem(101, "Test Item", LootTierLegendary) // High tier for ornate chest
lootManager := NewLootManager(lootDB, itemList)
// Create loot result
item := itemList.GetItem(101)
lootResult := &LootResult{
Items: []*items.Item{item},
Coins: 100,
}
// Create treasure chest
chest, err := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1, 2})
if err != nil {
t.Fatalf("Failed to create treasure chest: %v", err)
}
if chest.AppearanceID != ChestAppearanceOrnate {
t.Errorf("Expected ornate chest appearance %d for legendary item, got %d",
ChestAppearanceOrnate, chest.AppearanceID)
}
if len(chest.LootRights) != 2 {
t.Errorf("Expected 2 players with loot rights, got %d", len(chest.LootRights))
}
if !chest.HasLootRights(1) {
t.Error("Expected player 1 to have loot rights")
}
if chest.HasLootRights(3) {
t.Error("Expected player 3 to not have loot rights")
}
}
func TestChestService(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
lootDB := NewLootDatabase(db)
itemList := NewMockItemMasterList()
itemList.AddTestItem(101, "Test Item", LootTierCommon)
lootManager := NewLootManager(lootDB, itemList)
// Create mock services
playerService := NewMockPlayerService()
zoneService := NewMockZoneService()
// Set up player near chest
playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
playerService.SetInventorySpace(1, 10)
chestService := NewChestService(lootManager, playerService, zoneService)
// Create loot and chest
item := itemList.GetItem(101)
lootResult := &LootResult{
Items: []*items.Item{item},
Coins: 50,
}
chest, err := chestService.CreateTreasureChestFromLoot(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})
if err != nil {
t.Fatalf("Failed to create treasure chest: %v", err)
}
// Test viewing chest
result := chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
if !result.Success {
t.Errorf("Expected successful chest view, got: %s", result.Message)
}
if len(result.Items) != 1 {
t.Errorf("Expected 1 item in view result, got %d", len(result.Items))
}
// Test looting item
result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionLoot, item.Details.UniqueID)
if !result.Success {
t.Errorf("Expected successful item loot, got: %s", result.Message)
}
if len(result.Items) != 1 {
t.Errorf("Expected 1 looted item, got %d", len(result.Items))
}
// Chest should now be empty of items but still have coins
if lootManager.IsChestEmpty(chest.ID) {
t.Error("Expected chest to still have coins")
}
// Test looting all remaining (coins)
result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionLootAll, 0)
if !result.Success {
t.Errorf("Expected successful loot all, got: %s", result.Message)
}
if result.Coins != 50 {
t.Errorf("Expected 50 coins looted, got %d", result.Coins)
}
// Chest should now be empty
if !lootManager.IsChestEmpty(chest.ID) {
t.Error("Expected chest to be empty after looting all")
}
}
func TestLootStatistics(t *testing.T) {
stats := NewLootStatistics()
// Create test loot result
item := &items.Item{
Details: items.ItemDetails{
ItemID: 101,
Tier: LootTierRare,
},
}
lootResult := &LootResult{
Items: []*items.Item{item},
Coins: 100,
}
// Record loot
stats.RecordLoot(1, lootResult)
stats.RecordChest()
current := stats.GetStatistics()
if current.TotalLoots != 1 {
t.Errorf("Expected 1 total loot, got %d", current.TotalLoots)
}
if current.TotalItems != 1 {
t.Errorf("Expected 1 total item, got %d", current.TotalItems)
}
if current.TotalCoins != 100 {
t.Errorf("Expected 100 total coins, got %d", current.TotalCoins)
}
if current.TreasureChests != 1 {
t.Errorf("Expected 1 treasure chest, got %d", current.TreasureChests)
}
if current.ItemsByTier[LootTierRare] != 1 {
t.Errorf("Expected 1 rare item, got %d", current.ItemsByTier[LootTierRare])
}
}
func TestChestAppearanceSelection(t *testing.T) {
testCases := []struct {
tier int8
expected int32
}{
{LootTierCommon, ChestAppearanceSmall},
{LootTierTreasured, ChestAppearanceTreasure},
{LootTierLegendary, ChestAppearanceOrnate},
{LootTierFabled, ChestAppearanceExquisite},
{LootTierMythical, ChestAppearanceExquisite},
}
for _, tc := range testCases {
appearance := GetChestAppearance(tc.tier)
if appearance.AppearanceID != tc.expected {
t.Errorf("For tier %d, expected appearance %d, got %d",
tc.tier, tc.expected, appearance.AppearanceID)
}
}
}
func TestLootValidation(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
lootDB := NewLootDatabase(db)
itemList := NewMockItemMasterList()
lootManager := NewLootManager(lootDB, itemList)
playerService := NewMockPlayerService()
zoneService := NewMockZoneService()
chestService := NewChestService(lootManager, playerService, zoneService)
// Create a chest with loot rights for player 1
lootResult := &LootResult{Items: []*items.Item{}, Coins: 100}
chest, _ := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})
// Test player without loot rights
result := chestService.HandleChestInteraction(chest.ID, 2, ChestInteractionView, 0)
if result.Success {
t.Error("Expected failure for player without loot rights")
}
if result.Result != ChestResultNoRights {
t.Errorf("Expected no rights result, got %d", result.Result)
}
// Test player in combat
playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
playerService.combat[1] = true
result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
if result.Success {
t.Error("Expected failure for player in combat")
}
if result.Result != ChestResultInCombat {
t.Errorf("Expected in combat result, got %d", result.Result)
}
// Test player too far away
playerService.combat[1] = false
playerService.SetPlayerPosition(1, 100.0, 100.0, 100.0, 0.0, 100)
result = chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
if result.Success {
t.Error("Expected failure for player too far away")
}
if result.Result != ChestResultTooFar {
t.Errorf("Expected too far result, got %d", result.Result)
}
}
func TestCleanupExpiredChests(t *testing.T) {
db := setupTestDatabase(t)
defer db.Close()
lootDB := NewLootDatabase(db)
itemList := NewMockItemMasterList()
lootManager := NewLootManager(lootDB, itemList)
// Create an empty chest (should be cleaned up quickly)
emptyResult := &LootResult{Items: []*items.Item{}, Coins: 0}
emptyChest, _ := lootManager.CreateTreasureChest(1001, 100, 10.0, 20.0, 30.0, 0.0, emptyResult, []uint32{1})
// Modify the created time to make it expired
emptyChest.Created = time.Now().Add(-time.Duration(ChestDespawnTime+1) * time.Second)
// Run cleanup
lootManager.CleanupExpiredChests()
// Check that empty chest was removed
if lootManager.GetTreasureChest(emptyChest.ID) != nil {
t.Error("Expected expired empty chest to be cleaned up")
}
}
// Benchmark tests
func BenchmarkLootGeneration(b *testing.B) {
db := setupTestDatabase(b)
defer db.Close()
insertTestLootData(b, db)
lootDB := NewLootDatabase(db)
lootDB.LoadAllLootData()
itemList := NewMockItemMasterList()
itemList.AddTestItem(101, "Test Item", LootTierCommon)
itemList.AddTestItem(102, "Test Item 2", LootTierCommon)
itemList.AddTestItem(103, "Test Item 3", LootTierCommon)
lootManager := NewLootManager(lootDB, itemList)
context := &LootContext{
PlayerLevel: 15,
CompletedQuests: make(map[int32]bool),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := lootManager.GenerateLoot(1001, context)
if err != nil {
b.Fatalf("Failed to generate loot: %v", err)
}
}
}
func BenchmarkChestInteraction(b *testing.B) {
db := setupTestDatabase(b)
defer db.Close()
lootDB := NewLootDatabase(db)
itemList := NewMockItemMasterList()
itemList.AddTestItem(101, "Test Item", LootTierCommon)
lootManager := NewLootManager(lootDB, itemList)
playerService := NewMockPlayerService()
zoneService := NewMockZoneService()
chestService := NewChestService(lootManager, playerService, zoneService)
// Set up player
playerService.SetPlayerPosition(1, 10.0, 20.0, 30.0, 0.0, 100)
playerService.SetInventorySpace(1, 100)
// Create chest with loot
item := itemList.GetItem(101)
lootResult := &LootResult{Items: []*items.Item{item}, Coins: 100}
chest, _ := chestService.CreateTreasureChestFromLoot(1001, 100, 10.0, 20.0, 30.0, 0.0, lootResult, []uint32{1})
b.ResetTimer()
for i := 0; i < b.N; i++ {
chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0)
}
}