package npc import ( "fmt" "strings" "testing" ) // Mock implementations for testing // MockDatabase implements the Database interface for testing type MockDatabase struct { npcs map[int32]*NPC spells map[int32][]*NPCSpell skills map[int32]map[string]*Skill created bool } func NewMockDatabase() *MockDatabase { return &MockDatabase{ npcs: make(map[int32]*NPC), spells: make(map[int32][]*NPCSpell), skills: make(map[int32]map[string]*Skill), created: false, } } func (md *MockDatabase) LoadAllNPCs() ([]*NPC, error) { var npcs []*NPC for _, npc := range md.npcs { // Create a copy to avoid modifying the stored version npcCopy := NewNPCFromExisting(npc) npcs = append(npcs, npcCopy) } return npcs, nil } func (md *MockDatabase) SaveNPC(npc *NPC) error { if npc == nil || !npc.IsValid() { return fmt.Errorf("invalid NPC") } md.npcs[npc.GetNPCID()] = NewNPCFromExisting(npc) return nil } func (md *MockDatabase) DeleteNPC(npcID int32) error { if _, exists := md.npcs[npcID]; !exists { return fmt.Errorf("NPC with ID %d not found", npcID) } delete(md.npcs, npcID) delete(md.spells, npcID) delete(md.skills, npcID) return nil } func (md *MockDatabase) LoadNPCSpells(npcID int32) ([]*NPCSpell, error) { if spells, exists := md.spells[npcID]; exists { var result []*NPCSpell for _, spell := range spells { result = append(result, spell.Copy()) } return result, nil } return []*NPCSpell{}, nil } func (md *MockDatabase) SaveNPCSpells(npcID int32, spells []*NPCSpell) error { var spellCopies []*NPCSpell for _, spell := range spells { if spell != nil { spellCopies = append(spellCopies, spell.Copy()) } } md.spells[npcID] = spellCopies return nil } func (md *MockDatabase) LoadNPCSkills(npcID int32) (map[string]*Skill, error) { if skills, exists := md.skills[npcID]; exists { result := make(map[string]*Skill) for name, skill := range skills { result[name] = NewSkill(skill.SkillID, skill.Name, skill.GetCurrentVal(), skill.MaxVal) } return result, nil } return make(map[string]*Skill), nil } func (md *MockDatabase) SaveNPCSkills(npcID int32, skills map[string]*Skill) error { skillCopies := make(map[string]*Skill) for name, skill := range skills { if skill != nil { skillCopies[name] = NewSkill(skill.SkillID, skill.Name, skill.GetCurrentVal(), skill.MaxVal) } } md.skills[npcID] = skillCopies return nil } // MockLogger implements the Logger interface for testing type MockLogger struct { logs []string } func NewMockLogger() *MockLogger { return &MockLogger{ logs: make([]string, 0), } } func (ml *MockLogger) LogInfo(message string, args ...any) { ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...)) } func (ml *MockLogger) LogError(message string, args ...any) { ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...)) } func (ml *MockLogger) LogDebug(message string, args ...any) { ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...)) } func (ml *MockLogger) LogWarning(message string, args ...any) { ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...)) } func (ml *MockLogger) GetLogs() []string { return ml.logs } func (ml *MockLogger) Clear() { ml.logs = ml.logs[:0] } // Test functions func TestNewNPC(t *testing.T) { npc := NewNPC() if npc == nil { t.Fatal("NewNPC returned nil") } if npc.Entity == nil { t.Error("NPC should have an Entity") } if npc.GetNPCID() != 0 { t.Errorf("Expected NPC ID 0, got %d", npc.GetNPCID()) } if npc.GetAIStrategy() != AIStrategyBalanced { t.Errorf("Expected AI strategy %d, got %d", AIStrategyBalanced, npc.GetAIStrategy()) } if npc.GetAggroRadius() != DefaultAggroRadius { t.Errorf("Expected aggro radius %f, got %f", DefaultAggroRadius, npc.GetAggroRadius()) } if npc.GetBrain() == nil { t.Error("NPC should have a brain") } } func TestNPCBasicProperties(t *testing.T) { npc := NewNPC() // Test NPC ID testNPCID := int32(12345) npc.SetNPCID(testNPCID) if npc.GetNPCID() != testNPCID { t.Errorf("Expected NPC ID %d, got %d", testNPCID, npc.GetNPCID()) } // Test AI Strategy npc.SetAIStrategy(AIStrategyOffensive) if npc.GetAIStrategy() != AIStrategyOffensive { t.Errorf("Expected AI strategy %d, got %d", AIStrategyOffensive, npc.GetAIStrategy()) } // Test Aggro Radius testRadius := float32(25.5) npc.SetAggroRadius(testRadius, false) if npc.GetAggroRadius() != testRadius { t.Errorf("Expected aggro radius %f, got %f", testRadius, npc.GetAggroRadius()) } // Test Appearance ID testAppearanceID := int32(5432) npc.SetAppearanceID(testAppearanceID) if npc.GetAppearanceID() != testAppearanceID { t.Errorf("Expected appearance ID %d, got %d", testAppearanceID, npc.GetAppearanceID()) } } func TestNPCEntityIntegration(t *testing.T) { npc := NewNPC() if npc.Entity == nil { t.Fatal("NPC should have an Entity") } // Test entity properties through NPC testName := "Test NPC" npc.Entity.SetName(testName) // Trim the name to handle fixed-size array padding retrievedName := strings.TrimRight(npc.Entity.GetName(), "\x00") if retrievedName != testName { t.Errorf("Expected name '%s', got '%s'", testName, retrievedName) } // Test level through InfoStruct since Entity doesn't have SetLevel testLevel := int16(25) if npc.Entity.GetInfoStruct() != nil { npc.Entity.GetInfoStruct().SetLevel(testLevel) if npc.Entity.GetLevel() != int8(testLevel) { t.Errorf("Expected level %d, got %d", testLevel, npc.Entity.GetLevel()) } } testHP := int32(1500) npc.Entity.SetHP(testHP) if npc.Entity.GetHP() != testHP { t.Errorf("Expected HP %d, got %d", testHP, npc.Entity.GetHP()) } } func TestNPCSpells(t *testing.T) { npc := NewNPC() // Test initial spell state if npc.HasSpells() { t.Error("New NPC should not have spells") } if len(npc.GetSpells()) != 0 { t.Errorf("Expected 0 spells, got %d", len(npc.GetSpells())) } // Create test spells (without cast-on flags so they go into main spells array) spell1 := NewNPCSpell() spell1.SetSpellID(100) spell1.SetTier(1) spell2 := NewNPCSpell() spell2.SetSpellID(200) spell2.SetTier(2) spells := []*NPCSpell{spell1, spell2} npc.SetSpells(spells) // Test spell retrieval retrievedSpells := npc.GetSpells() if len(retrievedSpells) != 2 { t.Errorf("Expected 2 spells, got %d", len(retrievedSpells)) } if npc.HasSpells() != true { t.Error("NPC should have spells after setting them") } } func TestNPCSkills(t *testing.T) { npc := NewNPC() // Create test skills skill1 := NewSkill(1, "Sword", 50, 100) skill2 := NewSkill(2, "Shield", 75, 100) skills := map[string]*Skill{ "Sword": skill1, "Shield": skill2, } npc.SetSkills(skills) // Test skill retrieval by name retrievedSkill := npc.GetSkillByName("Sword", false) if retrievedSkill == nil { t.Fatal("Should retrieve Sword skill") } if retrievedSkill.GetCurrentVal() != 50 { t.Errorf("Expected skill value 50, got %d", retrievedSkill.GetCurrentVal()) } // Test non-existent skill nonExistentSkill := npc.GetSkillByName("Magic", false) if nonExistentSkill != nil { t.Error("Should not retrieve non-existent skill") } } func TestNPCRunback(t *testing.T) { npc := NewNPC() // Test initial runback state if npc.GetRunbackLocation() != nil { t.Error("New NPC should not have runback location") } if npc.IsRunningBack() { t.Error("New NPC should not be running back") } // Set runback location testX, testY, testZ := float32(10.5), float32(20.3), float32(30.7) testGridID := int32(12) npc.SetRunbackLocation(testX, testY, testZ, testGridID, true) runbackLoc := npc.GetRunbackLocation() if runbackLoc == nil { t.Fatal("Should have runback location after setting") } if runbackLoc.X != testX || runbackLoc.Y != testY || runbackLoc.Z != testZ { t.Errorf("Runback location mismatch: expected (%f,%f,%f), got (%f,%f,%f)", testX, testY, testZ, runbackLoc.X, runbackLoc.Y, runbackLoc.Z) } if runbackLoc.GridID != testGridID { t.Errorf("Expected grid ID %d, got %d", testGridID, runbackLoc.GridID) } // Test clearing runback npc.ClearRunback() if npc.GetRunbackLocation() != nil { t.Error("Runback location should be cleared") } } func TestNPCMovementTimer(t *testing.T) { npc := NewNPC() // Test initial timer state if npc.IsPauseMovementTimerActive() { t.Error("Movement timer should not be active initially") } // Test pausing movement if !npc.PauseMovement(100) { t.Error("Should be able to pause movement") } // Note: Timer might not be immediately active due to implementation details // The test focuses on the API being callable without errors } func TestNPCBrain(t *testing.T) { npc := NewNPC() // Test default brain brain := npc.GetBrain() if brain == nil { t.Fatal("NPC should have a default brain") } if !brain.IsActive() { t.Error("Default brain should be active") } if brain.GetBody() != npc { t.Error("Brain should reference the NPC") } // Test brain thinking (should not error) err := brain.Think() if err != nil { t.Errorf("Brain thinking should not error: %v", err) } // Test setting brain inactive brain.SetActive(false) if brain.IsActive() { t.Error("Brain should be inactive after setting to false") } } func TestNPCValidation(t *testing.T) { npc := NewNPC() // Set a valid level for the NPC to pass validation if npc.Entity != nil && npc.Entity.GetInfoStruct() != nil { npc.Entity.GetInfoStruct().SetLevel(10) // Valid level between 1-100 } // NPC should be valid if it has an entity with valid level if !npc.IsValid() { t.Error("NPC with valid level should be valid") } // Test NPC without entity npc.Entity = nil if npc.IsValid() { t.Error("NPC without entity should not be valid") } } func TestNPCString(t *testing.T) { npc := NewNPC() npc.SetNPCID(123) if npc.Entity != nil { npc.Entity.SetName("Test NPC") } str := npc.String() if str == "" { t.Error("NPC string representation should not be empty") } } func TestNPCCopyFromExisting(t *testing.T) { // Create original NPC originalNPC := NewNPC() originalNPC.SetNPCID(100) originalNPC.SetAIStrategy(AIStrategyDefensive) originalNPC.SetAggroRadius(30.0, false) if originalNPC.Entity != nil { originalNPC.Entity.SetName("Original NPC") if originalNPC.Entity.GetInfoStruct() != nil { originalNPC.Entity.GetInfoStruct().SetLevel(10) } } // Create copy copiedNPC := NewNPCFromExisting(originalNPC) if copiedNPC == nil { t.Fatal("NewNPCFromExisting returned nil") } // Verify copy has same properties if copiedNPC.GetNPCID() != originalNPC.GetNPCID() { t.Errorf("NPC ID mismatch: expected %d, got %d", originalNPC.GetNPCID(), copiedNPC.GetNPCID()) } if copiedNPC.GetAIStrategy() != originalNPC.GetAIStrategy() { t.Errorf("AI strategy mismatch: expected %d, got %d", originalNPC.GetAIStrategy(), copiedNPC.GetAIStrategy()) } // Test copying from nil nilCopy := NewNPCFromExisting(nil) if nilCopy == nil { t.Error("NewNPCFromExisting(nil) should return a new NPC, not nil") } } func TestNPCCombat(t *testing.T) { npc := NewNPC() // Test combat state npc.InCombat(true) // Note: The actual combat state checking would depend on Entity implementation // Test combat processing (should not error) npc.ProcessCombat() } func TestNPCShardSystem(t *testing.T) { npc := NewNPC() // Test shard properties testShardID := int32(5) npc.SetShardID(testShardID) if npc.GetShardID() != testShardID { t.Errorf("Expected shard ID %d, got %d", testShardID, npc.GetShardID()) } testCharID := int32(12345) npc.SetShardCharID(testCharID) if npc.GetShardCharID() != testCharID { t.Errorf("Expected shard char ID %d, got %d", testCharID, npc.GetShardCharID()) } testTimestamp := int64(1609459200) // 2021-01-01 00:00:00 UTC npc.SetShardCreatedTimestamp(testTimestamp) if npc.GetShardCreatedTimestamp() != testTimestamp { t.Errorf("Expected timestamp %d, got %d", testTimestamp, npc.GetShardCreatedTimestamp()) } } func TestNPCSkillBonuses(t *testing.T) { npc := NewNPC() // Test adding skill bonus spellID := int32(500) skillID := int32(10) bonusValue := float32(15.5) npc.AddSkillBonus(spellID, skillID, bonusValue) // Test removing skill bonus npc.RemoveSkillBonus(spellID) } func TestNPCSpellTypes(t *testing.T) { // Test NPCSpell creation and methods spell := NewNPCSpell() if spell == nil { t.Fatal("NewNPCSpell returned nil") } // Test default values if spell.GetListID() != 0 { t.Errorf("Expected list ID 0, got %d", spell.GetListID()) } if spell.GetTier() != 1 { t.Errorf("Expected tier 1, got %d", spell.GetTier()) } // Test setters and getters testSpellID := int32(12345) spell.SetSpellID(testSpellID) if spell.GetSpellID() != testSpellID { t.Errorf("Expected spell ID %d, got %d", testSpellID, spell.GetSpellID()) } testTier := int8(5) spell.SetTier(testTier) if spell.GetTier() != testTier { t.Errorf("Expected tier %d, got %d", testTier, spell.GetTier()) } // Test boolean properties spell.SetCastOnSpawn(true) if !spell.GetCastOnSpawn() { t.Error("Expected cast on spawn to be true") } spell.SetCastOnInitialAggro(true) if !spell.GetCastOnInitialAggro() { t.Error("Expected cast on initial aggro to be true") } // Test HP ratio testRatio := int8(75) spell.SetRequiredHPRatio(testRatio) if spell.GetRequiredHPRatio() != testRatio { t.Errorf("Expected HP ratio %d, got %d", testRatio, spell.GetRequiredHPRatio()) } // Test spell copy spellCopy := spell.Copy() if spellCopy == nil { t.Fatal("Spell copy returned nil") } if spellCopy.GetSpellID() != spell.GetSpellID() { t.Error("Spell copy should have same spell ID") } if spellCopy.GetTier() != spell.GetTier() { t.Error("Spell copy should have same tier") } } func TestSkillTypes(t *testing.T) { // Test Skill creation and methods testID := int32(10) testName := "TestSkill" testCurrent := int16(50) testMax := int16(100) skill := NewSkill(testID, testName, testCurrent, testMax) if skill == nil { t.Fatal("NewSkill returned nil") } if skill.SkillID != testID { t.Errorf("Expected skill ID %d, got %d", testID, skill.SkillID) } if skill.Name != testName { t.Errorf("Expected skill name '%s', got '%s'", testName, skill.Name) } if skill.GetCurrentVal() != testCurrent { t.Errorf("Expected current value %d, got %d", testCurrent, skill.GetCurrentVal()) } if skill.MaxVal != testMax { t.Errorf("Expected max value %d, got %d", testMax, skill.MaxVal) } // Test skill value modification newValue := int16(75) skill.SetCurrentVal(newValue) if skill.GetCurrentVal() != newValue { t.Errorf("Expected current value %d after setting, got %d", newValue, skill.GetCurrentVal()) } // Test skill increase originalValue := skill.GetCurrentVal() increased := skill.IncreaseSkill() if increased && skill.GetCurrentVal() <= originalValue { t.Error("Skill value should increase when IncreaseSkill returns true") } // Test skill at max skill.SetCurrentVal(testMax) increased = skill.IncreaseSkill() if increased { t.Error("Skill at max should not increase") } } func TestMovementLocation(t *testing.T) { testX, testY, testZ := float32(1.5), float32(2.5), float32(3.5) testGridID := int32(99) loc := NewMovementLocation(testX, testY, testZ, testGridID) if loc == nil { t.Fatal("NewMovementLocation returned nil") } if loc.X != testX || loc.Y != testY || loc.Z != testZ { t.Errorf("Location coordinates mismatch: expected (%f,%f,%f), got (%f,%f,%f)", testX, testY, testZ, loc.X, loc.Y, loc.Z) } if loc.GridID != testGridID { t.Errorf("Expected grid ID %d, got %d", testGridID, loc.GridID) } // Test copy locCopy := loc.Copy() if locCopy == nil { t.Fatal("Movement location copy returned nil") } if locCopy.X != loc.X || locCopy.Y != loc.Y || locCopy.Z != loc.Z { t.Error("Movement location copy should have same coordinates") } if locCopy.GridID != loc.GridID { t.Error("Movement location copy should have same grid ID") } } func TestTimer(t *testing.T) { timer := NewTimer() if timer == nil { t.Fatal("NewTimer returned nil") } // Test initial state if timer.Enabled() { t.Error("New timer should not be enabled") } if timer.Check() { t.Error("Disabled timer should not be checked as expired") } // Test starting timer timer.Start(100, false) // 100ms if !timer.Enabled() { t.Error("Timer should be enabled after starting") } // Test disabling timer timer.Disable() if timer.Enabled() { t.Error("Timer should be disabled after calling Disable") } } func TestSkillBonus(t *testing.T) { spellID := int32(123) bonus := NewSkillBonus(spellID) if bonus == nil { t.Fatal("NewSkillBonus returned nil") } if bonus.SpellID != spellID { t.Errorf("Expected spell ID %d, got %d", spellID, bonus.SpellID) } // Test adding skills skillID1 := int32(10) value1 := float32(15.5) bonus.AddSkill(skillID1, value1) skillID2 := int32(20) value2 := float32(25.0) bonus.AddSkill(skillID2, value2) // Test getting skills skills := bonus.GetSkills() if len(skills) != 2 { t.Errorf("Expected 2 skills, got %d", len(skills)) } if skills[skillID1].Value != value1 { t.Errorf("Expected skill 1 value %f, got %f", value1, skills[skillID1].Value) } if skills[skillID2].Value != value2 { t.Errorf("Expected skill 2 value %f, got %f", value2, skills[skillID2].Value) } // Test removing skill if !bonus.RemoveSkill(skillID1) { t.Error("Should be able to remove existing skill") } updatedSkills := bonus.GetSkills() if len(updatedSkills) != 1 { t.Errorf("Expected 1 skill after removal, got %d", len(updatedSkills)) } // Test removing non-existent skill if bonus.RemoveSkill(999) { t.Error("Should not be able to remove non-existent skill") } } // Benchmark tests func BenchmarkNewNPC(b *testing.B) { for i := 0; i < b.N; i++ { NewNPC() } } func BenchmarkNPCPropertyAccess(b *testing.B) { npc := NewNPC() npc.SetNPCID(12345) npc.SetAIStrategy(AIStrategyOffensive) b.ResetTimer() for i := 0; i < b.N; i++ { npc.GetNPCID() npc.GetAIStrategy() npc.GetAggroRadius() } } func BenchmarkNPCSpellOperations(b *testing.B) { npc := NewNPC() // Create test spells spells := make([]*NPCSpell, 10) for i := 0; i < 10; i++ { spell := NewNPCSpell() spell.SetSpellID(int32(i + 100)) spell.SetTier(int8(i%5 + 1)) spells[i] = spell } b.ResetTimer() for i := 0; i < b.N; i++ { npc.SetSpells(spells) npc.GetSpells() npc.HasSpells() } } func BenchmarkSkillOperations(b *testing.B) { skill := NewSkill(1, "TestSkill", 50, 100) b.ResetTimer() for i := 0; i < b.N; i++ { skill.GetCurrentVal() skill.SetCurrentVal(int16(i % 100)) skill.IncreaseSkill() } }