eq2go/internal/npc/npc_test.go

767 lines
18 KiB
Go

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()
}
}