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]interface{} objects map[int32]map[int32]interface{} // zoneID -> objectID } func NewMockZoneService() *MockZoneService { return &MockZoneService{ rules: make(map[int32]map[string]interface{}), objects: make(map[int32]map[int32]interface{}), } } func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{}, 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]interface{}) } 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) } }