diff --git a/internal/npc/ai/ai_test.go b/internal/npc/ai/ai_test.go new file mode 100644 index 0000000..88e3132 --- /dev/null +++ b/internal/npc/ai/ai_test.go @@ -0,0 +1,1624 @@ +package ai + +import ( + "fmt" + "testing" + "time" +) + +// Mock implementations for testing + +// MockNPC provides a mock NPC implementation for testing +type MockNPC struct { + id int32 + name string + hp int32 + totalHP int32 + inCombat bool + target Entity + isPet bool + owner Entity + x, y, z float32 + distance float32 + following bool + followTarget Spawn + runningBack bool + mezzedOrStunned bool + casting bool + dazed bool + feared bool + stifled bool + inWater bool + waterCreature bool + flyingCreature bool + attackAllowed bool + primaryWeaponReady bool + secondaryWeaponReady bool + castPercentage int8 + nextSpell Spell + nextBuffSpell Spell + checkLoS bool + pauseMovementTimer bool + runbackLocation *MovementLocation + runbackDistance float32 + shouldCallRunback bool + spawnScript string +} + +func NewMockNPC(id int32, name string) *MockNPC { + return &MockNPC{ + id: id, + name: name, + hp: 100, + totalHP: 100, + inCombat: false, + isPet: false, + x: 0, + y: 0, + z: 0, + distance: 10.0, + following: false, + runningBack: false, + mezzedOrStunned: false, + casting: false, + dazed: false, + feared: false, + stifled: false, + inWater: false, + waterCreature: false, + flyingCreature: false, + attackAllowed: true, + primaryWeaponReady: true, + secondaryWeaponReady: true, + castPercentage: 25, + checkLoS: true, + pauseMovementTimer: false, + runbackDistance: 0, + shouldCallRunback: false, + spawnScript: "", + } +} + +// Implement NPC interface +func (m *MockNPC) GetID() int32 { return m.id } +func (m *MockNPC) GetName() string { return m.name } +func (m *MockNPC) GetHP() int32 { return m.hp } +func (m *MockNPC) GetTotalHP() int32 { return m.totalHP } +func (m *MockNPC) SetHP(hp int32) { m.hp = hp } +func (m *MockNPC) IsAlive() bool { return m.hp > 0 } +func (m *MockNPC) GetInCombat() bool { return m.inCombat } +func (m *MockNPC) InCombat(val bool) { m.inCombat = val } +func (m *MockNPC) GetTarget() Entity { return m.target } +func (m *MockNPC) SetTarget(target Entity) { m.target = target } +func (m *MockNPC) IsPet() bool { return m.isPet } +func (m *MockNPC) GetOwner() Entity { return m.owner } +func (m *MockNPC) GetX() float32 { return m.x } +func (m *MockNPC) GetY() float32 { return m.y } +func (m *MockNPC) GetZ() float32 { return m.z } +func (m *MockNPC) GetDistance(target Entity) float32 { return m.distance } +func (m *MockNPC) FaceTarget(target Entity, followCaller bool) {} +func (m *MockNPC) IsFollowing() bool { return m.following } +func (m *MockNPC) SetFollowing(val bool) { m.following = val } +func (m *MockNPC) GetFollowTarget() Spawn { return m.followTarget } +func (m *MockNPC) SetFollowTarget(target Spawn, range_ float32) { m.followTarget = target } +func (m *MockNPC) CalculateRunningLocation(clear bool) {} +func (m *MockNPC) ClearRunningLocations() {} +func (m *MockNPC) IsRunningBack() bool { return m.runningBack } +func (m *MockNPC) GetRunbackLocation() *MovementLocation { return m.runbackLocation } +func (m *MockNPC) GetRunbackDistance() float32 { return m.runbackDistance } +func (m *MockNPC) Runback(distance float32) { m.runningBack = true } +func (m *MockNPC) ShouldCallRunback() bool { return m.shouldCallRunback } +func (m *MockNPC) SetCallRunback(val bool) { m.shouldCallRunback = val } +func (m *MockNPC) IsMezzedOrStunned() bool { return m.mezzedOrStunned } +func (m *MockNPC) IsCasting() bool { return m.casting } +func (m *MockNPC) IsDazed() bool { return m.dazed } +func (m *MockNPC) IsFeared() bool { return m.feared } +func (m *MockNPC) IsStifled() bool { return m.stifled } +func (m *MockNPC) InWater() bool { return m.inWater } +func (m *MockNPC) IsWaterCreature() bool { return m.waterCreature } +func (m *MockNPC) IsFlyingCreature() bool { return m.flyingCreature } +func (m *MockNPC) AttackAllowed(target Entity) bool { return m.attackAllowed } +func (m *MockNPC) PrimaryWeaponReady() bool { return m.primaryWeaponReady } +func (m *MockNPC) SecondaryWeaponReady() bool { return m.secondaryWeaponReady } +func (m *MockNPC) SetPrimaryLastAttackTime(time int64) {} +func (m *MockNPC) SetSecondaryLastAttackTime(time int64) {} +func (m *MockNPC) MeleeAttack(target Entity, distance float32, primary bool) {} +func (m *MockNPC) GetCastPercentage() int8 { return m.castPercentage } +func (m *MockNPC) GetNextSpell(target Entity, distance float32) Spell { return m.nextSpell } +func (m *MockNPC) GetNextBuffSpell(target Spawn) Spell { return m.nextBuffSpell } +func (m *MockNPC) SetCastOnAggroCompleted(val bool) {} +func (m *MockNPC) CheckLoS(target Entity) bool { return m.checkLoS } +func (m *MockNPC) IsPauseMovementTimerActive() bool { return m.pauseMovementTimer } +func (m *MockNPC) SetEncounterState(state int8) {} +func (m *MockNPC) GetSpawnScript() string { return m.spawnScript } +func (m *MockNPC) KillSpawn(npc NPC) {} + +// Implement Entity interface (extends Spawn) +func (m *MockNPC) IsPlayer() bool { return false } +func (m *MockNPC) IsBot() bool { return false } + +// MockEntity provides a mock Entity implementation for testing +type MockEntity struct { + id int32 + name string + hp int32 + totalHP int32 + x, y, z float32 + isPlayer bool + isBot bool + isPet bool + owner Entity + inWater bool +} + +func NewMockEntity(id int32, name string) *MockEntity { + return &MockEntity{ + id: id, + name: name, + hp: 100, + totalHP: 100, + x: 0, + y: 0, + z: 0, + isPlayer: false, + isBot: false, + isPet: false, + inWater: false, + } +} + +func (m *MockEntity) GetID() int32 { return m.id } +func (m *MockEntity) GetName() string { return m.name } +func (m *MockEntity) GetHP() int32 { return m.hp } +func (m *MockEntity) GetTotalHP() int32 { return m.totalHP } +func (m *MockEntity) GetX() float32 { return m.x } +func (m *MockEntity) GetY() float32 { return m.y } +func (m *MockEntity) GetZ() float32 { return m.z } +func (m *MockEntity) IsPlayer() bool { return m.isPlayer } +func (m *MockEntity) IsBot() bool { return m.isBot } +func (m *MockEntity) IsPet() bool { return m.isPet } +func (m *MockEntity) GetOwner() Entity { return m.owner } +func (m *MockEntity) InWater() bool { return m.inWater } + +// MockSpell provides a mock Spell implementation for testing +type MockSpell struct { + id int32 + name string + friendly bool + castTime int32 + recoveryTime int32 + range_ float32 + minRange float32 +} + +func NewMockSpell(id int32, name string) *MockSpell { + return &MockSpell{ + id: id, + name: name, + friendly: false, + castTime: 1000, + recoveryTime: 2000, + range_: 30.0, + minRange: 0.0, + } +} + +func (m *MockSpell) GetSpellID() int32 { return m.id } +func (m *MockSpell) GetName() string { return m.name } +func (m *MockSpell) IsFriendlySpell() bool { return m.friendly } +func (m *MockSpell) GetCastTime() int32 { return m.castTime } +func (m *MockSpell) GetRecoveryTime() int32 { return m.recoveryTime } +func (m *MockSpell) GetRange() float32 { return m.range_ } +func (m *MockSpell) GetMinRange() float32 { return m.minRange } + +// MockLogger provides a mock Logger implementation for testing +type MockLogger struct { + messages []string +} + +func NewMockLogger() *MockLogger { + return &MockLogger{ + messages: make([]string, 0), + } +} + +func (m *MockLogger) LogInfo(message string, args ...any) { + m.messages = append(m.messages, "INFO: "+message) +} + +func (m *MockLogger) LogError(message string, args ...any) { + m.messages = append(m.messages, "ERROR: "+message) +} + +func (m *MockLogger) LogDebug(message string, args ...any) { + m.messages = append(m.messages, "DEBUG: "+message) +} + +func (m *MockLogger) LogWarning(message string, args ...any) { + m.messages = append(m.messages, "WARNING: "+message) +} + +func (m *MockLogger) GetMessages() []string { + return m.messages +} + +// MockLuaInterface provides a mock LuaInterface implementation for testing +type MockLuaInterface struct { + executed bool + lastScript string + lastFunction string + shouldError bool +} + +func NewMockLuaInterface() *MockLuaInterface { + return &MockLuaInterface{ + executed: false, + shouldError: false, + } +} + +func (m *MockLuaInterface) RunSpawnScript(script, function string, npc NPC, target Entity) error { + m.executed = true + m.lastScript = script + m.lastFunction = function + + if m.shouldError { + return fmt.Errorf("mock lua error") + } + + return nil +} + +func (m *MockLuaInterface) WasExecuted() bool { + return m.executed +} + +func (m *MockLuaInterface) SetShouldError(shouldError bool) { + m.shouldError = shouldError +} + +// Tests for HateEntry and HateList + +func TestNewHateEntry(t *testing.T) { + entityID := int32(123) + hateValue := int32(500) + + entry := NewHateEntry(entityID, hateValue) + + if entry.EntityID != entityID { + t.Errorf("Expected entity ID %d, got %d", entityID, entry.EntityID) + } + + if entry.HateValue != hateValue { + t.Errorf("Expected hate value %d, got %d", hateValue, entry.HateValue) + } + + if entry.LastUpdated == 0 { + t.Error("Expected LastUpdated to be set") + } +} + +func TestNewHateEntryMinValue(t *testing.T) { + entityID := int32(123) + hateValue := int32(0) // Below minimum + + entry := NewHateEntry(entityID, hateValue) + + if entry.HateValue != MinHateValue { + t.Errorf("Expected hate value to be adjusted to minimum %d, got %d", MinHateValue, entry.HateValue) + } +} + +func TestNewHateList(t *testing.T) { + hateList := NewHateList() + + if hateList == nil { + t.Fatal("NewHateList returned nil") + } + + if hateList.Size() != 0 { + t.Errorf("Expected empty hate list, got size %d", hateList.Size()) + } +} + +func TestHateListAddHate(t *testing.T) { + hateList := NewHateList() + entityID := int32(123) + hateValue := int32(500) + + hateList.AddHate(entityID, hateValue) + + if hateList.Size() != 1 { + t.Errorf("Expected hate list size 1, got %d", hateList.Size()) + } + + retrievedHate := hateList.GetHate(entityID) + if retrievedHate != hateValue { + t.Errorf("Expected hate value %d, got %d", hateValue, retrievedHate) + } +} + +func TestHateListAddHateUpdate(t *testing.T) { + hateList := NewHateList() + entityID := int32(123) + initialHate := int32(500) + additionalHate := int32(200) + + hateList.AddHate(entityID, initialHate) + hateList.AddHate(entityID, additionalHate) + + if hateList.Size() != 1 { + t.Errorf("Expected hate list size 1, got %d", hateList.Size()) + } + + expectedTotal := initialHate + additionalHate + retrievedHate := hateList.GetHate(entityID) + if retrievedHate != expectedTotal { + t.Errorf("Expected hate value %d, got %d", expectedTotal, retrievedHate) + } +} + +func TestHateListGetMostHated(t *testing.T) { + hateList := NewHateList() + + entity1 := int32(1) + entity2 := int32(2) + entity3 := int32(3) + + hateList.AddHate(entity1, 100) + hateList.AddHate(entity2, 500) // Highest hate + hateList.AddHate(entity3, 200) + + mostHated := hateList.GetMostHated() + if mostHated != entity2 { + t.Errorf("Expected most hated entity %d, got %d", entity2, mostHated) + } +} + +func TestHateListGetHatePercentage(t *testing.T) { + hateList := NewHateList() + + entity1 := int32(1) + entity2 := int32(2) + + hateList.AddHate(entity1, 300) // 75% of total hate (300/400) + hateList.AddHate(entity2, 100) // 25% of total hate (100/400) + + percentage1 := hateList.GetHatePercentage(entity1) + percentage2 := hateList.GetHatePercentage(entity2) + + if percentage1 != 75 { + t.Errorf("Expected entity1 hate percentage 75, got %d", percentage1) + } + + if percentage2 != 25 { + t.Errorf("Expected entity2 hate percentage 25, got %d", percentage2) + } +} + +func TestHateListRemoveHate(t *testing.T) { + hateList := NewHateList() + entityID := int32(123) + + hateList.AddHate(entityID, 500) + hateList.RemoveHate(entityID) + + if hateList.Size() != 0 { + t.Errorf("Expected empty hate list after removal, got size %d", hateList.Size()) + } + + retrievedHate := hateList.GetHate(entityID) + if retrievedHate != 0 { + t.Errorf("Expected hate value 0 after removal, got %d", retrievedHate) + } +} + +func TestHateListClear(t *testing.T) { + hateList := NewHateList() + + hateList.AddHate(1, 100) + hateList.AddHate(2, 200) + hateList.AddHate(3, 300) + + hateList.Clear() + + if hateList.Size() != 0 { + t.Errorf("Expected empty hate list after clear, got size %d", hateList.Size()) + } +} + +func TestHateListGetAllEntries(t *testing.T) { + hateList := NewHateList() + + entity1 := int32(1) + entity2 := int32(2) + hate1 := int32(100) + hate2 := int32(200) + + hateList.AddHate(entity1, hate1) + hateList.AddHate(entity2, hate2) + + entries := hateList.GetAllEntries() + + if len(entries) != 2 { + t.Errorf("Expected 2 entries, got %d", len(entries)) + } + + if entries[entity1].HateValue != hate1 { + t.Errorf("Expected entity1 hate %d, got %d", hate1, entries[entity1].HateValue) + } + + if entries[entity2].HateValue != hate2 { + t.Errorf("Expected entity2 hate %d, got %d", hate2, entries[entity2].HateValue) + } +} + +// Tests for EncounterEntry and EncounterList + +func TestNewEncounterEntry(t *testing.T) { + entityID := int32(123) + characterID := int32(456) + isPlayer := true + isBot := false + + entry := NewEncounterEntry(entityID, characterID, isPlayer, isBot) + + if entry.EntityID != entityID { + t.Errorf("Expected entity ID %d, got %d", entityID, entry.EntityID) + } + + if entry.CharacterID != characterID { + t.Errorf("Expected character ID %d, got %d", characterID, entry.CharacterID) + } + + if entry.IsPlayer != isPlayer { + t.Errorf("Expected IsPlayer %v, got %v", isPlayer, entry.IsPlayer) + } + + if entry.IsBot != isBot { + t.Errorf("Expected IsBot %v, got %v", isBot, entry.IsBot) + } + + if entry.AddedTime == 0 { + t.Error("Expected AddedTime to be set") + } +} + +func TestNewEncounterList(t *testing.T) { + encounterList := NewEncounterList() + + if encounterList == nil { + t.Fatal("NewEncounterList returned nil") + } + + if encounterList.Size() != 0 { + t.Errorf("Expected empty encounter list, got size %d", encounterList.Size()) + } + + if encounterList.HasPlayerInEncounter() { + t.Error("Expected no players in encounter initially") + } +} + +func TestEncounterListAddEntity(t *testing.T) { + encounterList := NewEncounterList() + entityID := int32(123) + characterID := int32(456) + + success := encounterList.AddEntity(entityID, characterID, true, false) + + if !success { + t.Error("Expected AddEntity to succeed") + } + + if encounterList.Size() != 1 { + t.Errorf("Expected encounter list size 1, got %d", encounterList.Size()) + } + + if !encounterList.IsEntityInEncounter(entityID) { + t.Error("Entity should be in encounter") + } + + if !encounterList.IsPlayerInEncounter(characterID) { + t.Error("Player should be in encounter") + } + + if !encounterList.HasPlayerInEncounter() { + t.Error("Should have player in encounter") + } +} + +func TestEncounterListAddEntityDuplicate(t *testing.T) { + encounterList := NewEncounterList() + entityID := int32(123) + + success1 := encounterList.AddEntity(entityID, 0, false, false) + success2 := encounterList.AddEntity(entityID, 0, false, false) // Duplicate + + if !success1 { + t.Error("Expected first AddEntity to succeed") + } + + if success2 { + t.Error("Expected second AddEntity to fail (duplicate)") + } + + if encounterList.Size() != 1 { + t.Errorf("Expected encounter list size 1 after duplicate add, got %d", encounterList.Size()) + } +} + +func TestEncounterListRemoveEntity(t *testing.T) { + encounterList := NewEncounterList() + entityID := int32(123) + characterID := int32(456) + + encounterList.AddEntity(entityID, characterID, true, false) + encounterList.RemoveEntity(entityID) + + if encounterList.Size() != 0 { + t.Errorf("Expected empty encounter list after removal, got size %d", encounterList.Size()) + } + + if encounterList.IsEntityInEncounter(entityID) { + t.Error("Entity should not be in encounter after removal") + } + + if encounterList.IsPlayerInEncounter(characterID) { + t.Error("Player should not be in encounter after removal") + } + + if encounterList.HasPlayerInEncounter() { + t.Error("Should not have player in encounter after removal") + } +} + +func TestEncounterListCountPlayerBots(t *testing.T) { + encounterList := NewEncounterList() + + encounterList.AddEntity(1, 101, true, false) // Player + encounterList.AddEntity(2, 0, false, true) // Bot + encounterList.AddEntity(3, 0, false, false) // NPC + + count := encounterList.CountPlayerBots() + if count != 2 { + t.Errorf("Expected 2 players/bots, got %d", count) + } +} + +func TestEncounterListGetAllEntityIDs(t *testing.T) { + encounterList := NewEncounterList() + + entity1 := int32(1) + entity2 := int32(2) + entity3 := int32(3) + + encounterList.AddEntity(entity1, 0, false, false) + encounterList.AddEntity(entity2, 0, false, false) + encounterList.AddEntity(entity3, 0, false, false) + + entityIDs := encounterList.GetAllEntityIDs() + + if len(entityIDs) != 3 { + t.Errorf("Expected 3 entity IDs, got %d", len(entityIDs)) + } + + // Check that all IDs are present (order doesn't matter) + idMap := make(map[int32]bool) + for _, id := range entityIDs { + idMap[id] = true + } + + if !idMap[entity1] || !idMap[entity2] || !idMap[entity3] { + t.Error("Expected all entity IDs to be present") + } +} + +func TestEncounterListClear(t *testing.T) { + encounterList := NewEncounterList() + + encounterList.AddEntity(1, 101, true, false) + encounterList.AddEntity(2, 0, false, false) + + encounterList.Clear() + + if encounterList.Size() != 0 { + t.Errorf("Expected empty encounter list after clear, got size %d", encounterList.Size()) + } + + if encounterList.HasPlayerInEncounter() { + t.Error("Should not have player in encounter after clear") + } +} + +// Tests for BrainState + +func TestNewBrainState(t *testing.T) { + state := NewBrainState() + + if state == nil { + t.Fatal("NewBrainState returned nil") + } + + if state.GetState() != AIStateIdle { + t.Errorf("Expected initial state %d, got %d", AIStateIdle, state.GetState()) + } + + if !state.IsActive() { + t.Error("Expected brain state to be active initially") + } + + if state.GetThinkTick() != DefaultThinkTick { + t.Errorf("Expected think tick %d, got %d", DefaultThinkTick, state.GetThinkTick()) + } + + if state.GetDebugLevel() != DebugLevelNone { + t.Errorf("Expected debug level %d, got %d", DebugLevelNone, state.GetDebugLevel()) + } +} + +func TestBrainStateSetState(t *testing.T) { + state := NewBrainState() + newState := AIStateCombat + + state.SetState(newState) + + if state.GetState() != newState { + t.Errorf("Expected state %d, got %d", newState, state.GetState()) + } +} + +func TestBrainStateSetActive(t *testing.T) { + state := NewBrainState() + + state.SetActive(false) + if state.IsActive() { + t.Error("Expected brain state to be inactive") + } + + state.SetActive(true) + if !state.IsActive() { + t.Error("Expected brain state to be active") + } +} + +func TestBrainStateSetThinkTick(t *testing.T) { + state := NewBrainState() + + // Test normal value + newTick := int32(500) + state.SetThinkTick(newTick) + if state.GetThinkTick() != newTick { + t.Errorf("Expected think tick %d, got %d", newTick, state.GetThinkTick()) + } + + // Test minimum value + state.SetThinkTick(0) + if state.GetThinkTick() != 1 { + t.Errorf("Expected think tick to be adjusted to minimum 1, got %d", state.GetThinkTick()) + } + + // Test maximum value + state.SetThinkTick(MaxThinkTick + 1000) + if state.GetThinkTick() != MaxThinkTick { + t.Errorf("Expected think tick to be adjusted to maximum %d, got %d", MaxThinkTick, state.GetThinkTick()) + } +} + +func TestBrainStateSetLastThink(t *testing.T) { + state := NewBrainState() + timestamp := time.Now().UnixMilli() + + state.SetLastThink(timestamp) + + if state.GetLastThink() != timestamp { + t.Errorf("Expected last think timestamp %d, got %d", timestamp, state.GetLastThink()) + } +} + +func TestBrainStateSpellRecovery(t *testing.T) { + state := NewBrainState() + + // Test future recovery time + futureTime := time.Now().UnixMilli() + 5000 + state.SetSpellRecovery(futureTime) + + if state.GetSpellRecovery() != futureTime { + t.Errorf("Expected spell recovery time %d, got %d", futureTime, state.GetSpellRecovery()) + } + + if state.HasRecovered() { + t.Error("Expected brain to not have recovered yet") + } + + // Test past recovery time + pastTime := time.Now().UnixMilli() - 5000 + state.SetSpellRecovery(pastTime) + + if !state.HasRecovered() { + t.Error("Expected brain to have recovered") + } +} + +func TestBrainStateDebugLevel(t *testing.T) { + state := NewBrainState() + debugLevel := DebugLevelVerbose + + state.SetDebugLevel(debugLevel) + + if state.GetDebugLevel() != debugLevel { + t.Errorf("Expected debug level %d, got %d", debugLevel, state.GetDebugLevel()) + } +} + +// Tests for BrainStatistics + +func TestNewBrainStatistics(t *testing.T) { + stats := NewBrainStatistics() + + if stats == nil { + t.Fatal("NewBrainStatistics returned nil") + } + + if stats.ThinkCycles != 0 { + t.Errorf("Expected think cycles 0, got %d", stats.ThinkCycles) + } + + if stats.SpellsCast != 0 { + t.Errorf("Expected spells cast 0, got %d", stats.SpellsCast) + } + + if stats.MeleeAttacks != 0 { + t.Errorf("Expected melee attacks 0, got %d", stats.MeleeAttacks) + } + + if stats.AverageThinkTime != 0.0 { + t.Errorf("Expected average think time 0.0, got %f", stats.AverageThinkTime) + } + + if stats.LastThinkTime == 0 { + t.Error("Expected LastThinkTime to be set") + } +} + +// Tests for BaseBrain + +func TestNewBaseBrain(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + + brain := NewBaseBrain(npc, logger) + + if brain == nil { + t.Fatal("NewBaseBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeDefault { + t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) + } + + if !brain.IsActive() { + t.Error("Expected brain to be active initially") + } + + if brain.GetBody() != npc { + t.Error("Expected brain body to be the provided NPC") + } +} + +func TestBaseBrainHateManagement(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + brain := NewBaseBrain(npc, logger) + + entityID := int32(456) + hateValue := int32(500) + + // Add hate + brain.AddHate(entityID, hateValue) + + if brain.GetHate(entityID) != hateValue { + t.Errorf("Expected hate value %d, got %d", hateValue, brain.GetHate(entityID)) + } + + if brain.GetMostHated() != entityID { + t.Errorf("Expected most hated entity %d, got %d", entityID, brain.GetMostHated()) + } + + // Clear hate for entity + brain.ClearHateForEntity(entityID) + + if brain.GetHate(entityID) != 0 { + t.Errorf("Expected hate value 0 after clearing, got %d", brain.GetHate(entityID)) + } + + // Add hate again and clear all + brain.AddHate(entityID, hateValue) + brain.ClearHate() + + if brain.GetHate(entityID) != 0 { + t.Errorf("Expected hate value 0 after clearing all, got %d", brain.GetHate(entityID)) + } +} + +func TestBaseBrainEncounterManagement(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + brain := NewBaseBrain(npc, logger) + + entityID := int32(456) + characterID := int32(789) + + // Add to encounter + success := brain.AddToEncounter(entityID, characterID, true, false) + + if !success { + t.Error("Expected AddToEncounter to succeed") + } + + if !brain.IsEntityInEncounter(entityID) { + t.Error("Entity should be in encounter") + } + + if !brain.IsPlayerInEncounter(characterID) { + t.Error("Player should be in encounter") + } + + if !brain.HasPlayerInEncounter() { + t.Error("Should have player in encounter") + } + + if brain.GetEncounterSize() != 1 { + t.Errorf("Expected encounter size 1, got %d", brain.GetEncounterSize()) + } + + // Check loot allowed + if !brain.CheckLootAllowed(entityID) { + t.Error("Loot should be allowed for entity in encounter") + } + + // Clear encounter + brain.ClearEncounter() + + if brain.IsEntityInEncounter(entityID) { + t.Error("Entity should not be in encounter after clear") + } + + if brain.GetEncounterSize() != 0 { + t.Errorf("Expected encounter size 0 after clear, got %d", brain.GetEncounterSize()) + } +} + +func TestBaseBrainStatistics(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + brain := NewBaseBrain(npc, logger) + + // Get initial statistics + stats := brain.GetStatistics() + if stats == nil { + t.Fatal("GetStatistics returned nil") + } + + // Reset statistics + brain.ResetStatistics() + + // Get statistics again + newStats := brain.GetStatistics() + if newStats.ThinkCycles != 0 { + t.Errorf("Expected think cycles 0 after reset, got %d", newStats.ThinkCycles) + } +} + +func TestBaseBrainSetBody(t *testing.T) { + npc1 := NewMockNPC(123, "TestNPC1") + npc2 := NewMockNPC(456, "TestNPC2") + logger := NewMockLogger() + brain := NewBaseBrain(npc1, logger) + + if brain.GetBody() != npc1 { + t.Error("Expected brain body to be npc1 initially") + } + + brain.SetBody(npc2) + + if brain.GetBody() != npc2 { + t.Error("Expected brain body to be npc2 after SetBody") + } +} + +// Tests for Brain Variants + +func TestNewCombatPetBrain(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + npc.isPet = true + logger := NewMockLogger() + + brain := NewCombatPetBrain(npc, logger) + + if brain == nil { + t.Fatal("NewCombatPetBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeCombatPet { + t.Errorf("Expected brain type %d, got %d", BrainTypeCombatPet, brain.GetBrainType()) + } +} + +func TestNewNonCombatPetBrain(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + npc.isPet = true + logger := NewMockLogger() + + brain := NewNonCombatPetBrain(npc, logger) + + if brain == nil { + t.Fatal("NewNonCombatPetBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeNonCombatPet { + t.Errorf("Expected brain type %d, got %d", BrainTypeNonCombatPet, brain.GetBrainType()) + } +} + +func TestNewBlankBrain(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + + brain := NewBlankBrain(npc, logger) + + if brain == nil { + t.Fatal("NewBlankBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeBlank { + t.Errorf("Expected brain type %d, got %d", BrainTypeBlank, brain.GetBrainType()) + } + + if brain.GetThinkTick() != BlankBrainTick { + t.Errorf("Expected think tick %d, got %d", BlankBrainTick, brain.GetThinkTick()) + } +} + +func TestBlankBrainThink(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + brain := NewBlankBrain(npc, logger) + + // Blank brain Think should do nothing and not error + err := brain.Think() + if err != nil { + t.Errorf("Expected no error from blank brain Think, got: %v", err) + } +} + +func TestNewLuaBrain(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + npc.spawnScript = "test_script.lua" + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + + brain := NewLuaBrain(npc, logger, luaInterface) + + if brain == nil { + t.Fatal("NewLuaBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeLua { + t.Errorf("Expected brain type %d, got %d", BrainTypeLua, brain.GetBrainType()) + } +} + +func TestLuaBrainThink(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + npc.spawnScript = "test_script.lua" + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + + brain := NewLuaBrain(npc, logger, luaInterface) + + err := brain.Think() + if err != nil { + t.Errorf("Expected no error from Lua brain Think, got: %v", err) + } + + if !luaInterface.WasExecuted() { + t.Error("Expected Lua interface to be executed") + } + + if luaInterface.lastFunction != "Think" { + t.Errorf("Expected Lua function 'Think', got '%s'", luaInterface.lastFunction) + } +} + +func TestLuaBrainThinkError(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + npc.spawnScript = "test_script.lua" + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + luaInterface.SetShouldError(true) + + brain := NewLuaBrain(npc, logger, luaInterface) + + err := brain.Think() + if err == nil { + t.Error("Expected error from Lua brain Think") + } +} + +func TestNewDumbFirePetBrain(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + target := NewMockEntity(456, "Target") + expireTime := int32(10000) // 10 seconds + logger := NewMockLogger() + + brain := NewDumbFirePetBrain(npc, target, expireTime, logger) + + if brain == nil { + t.Fatal("NewDumbFirePetBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeDumbFire { + t.Errorf("Expected brain type %d, got %d", BrainTypeDumbFire, brain.GetBrainType()) + } + + // Should have max hate for target + if brain.GetHate(target.GetID()) != MaxHateValue { + t.Errorf("Expected max hate for target, got %d", brain.GetHate(target.GetID())) + } + + if brain.IsExpired() { + t.Error("Dumbfire pet should not be expired immediately") + } +} + +func TestDumbFirePetBrainExpiry(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + target := NewMockEntity(456, "Target") + expireTime := int32(100) // 100ms + logger := NewMockLogger() + + brain := NewDumbFirePetBrain(npc, target, expireTime, logger) + + // Wait for expiry + time.Sleep(150 * time.Millisecond) + + if !brain.IsExpired() { + t.Error("Dumbfire pet should be expired") + } +} + +func TestDumbFirePetBrainExtendExpireTime(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + target := NewMockEntity(456, "Target") + expireTime := int32(1000) + logger := NewMockLogger() + + brain := NewDumbFirePetBrain(npc, target, expireTime, logger) + originalExpireTime := brain.GetExpireTime() + + extension := int32(5000) + brain.ExtendExpireTime(extension) + + newExpireTime := brain.GetExpireTime() + if newExpireTime != originalExpireTime+int64(extension) { + t.Errorf("Expected expire time %d, got %d", originalExpireTime+int64(extension), newExpireTime) + } +} + +// Tests for CreateBrain factory function + +func TestCreateBrainDefault(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + + brain := CreateBrain(npc, BrainTypeDefault, logger) + + if brain == nil { + t.Fatal("CreateBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeDefault { + t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) + } +} + +func TestCreateBrainCombatPet(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + logger := NewMockLogger() + + brain := CreateBrain(npc, BrainTypeCombatPet, logger) + + if brain.GetBrainType() != BrainTypeCombatPet { + t.Errorf("Expected brain type %d, got %d", BrainTypeCombatPet, brain.GetBrainType()) + } +} + +func TestCreateBrainLua(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + + brain := CreateBrain(npc, BrainTypeLua, logger, luaInterface) + + if brain.GetBrainType() != BrainTypeLua { + t.Errorf("Expected brain type %d, got %d", BrainTypeLua, brain.GetBrainType()) + } +} + +func TestCreateBrainDumbFire(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + target := NewMockEntity(456, "Target") + expireTime := int32(10000) + logger := NewMockLogger() + + brain := CreateBrain(npc, BrainTypeDumbFire, logger, target, expireTime) + + if brain.GetBrainType() != BrainTypeDumbFire { + t.Errorf("Expected brain type %d, got %d", BrainTypeDumbFire, brain.GetBrainType()) + } +} + +// Tests for AIManager + +func TestNewAIManager(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + + manager := NewAIManager(logger, luaInterface) + + if manager == nil { + t.Fatal("NewAIManager returned nil") + } + + if manager.GetBrainCount() != 0 { + t.Errorf("Expected brain count 0, got %d", manager.GetBrainCount()) + } + + if manager.GetActiveCount() != 0 { + t.Errorf("Expected active count 0, got %d", manager.GetActiveCount()) + } + + if manager.GetTotalThinks() != 0 { + t.Errorf("Expected total thinks 0, got %d", manager.GetTotalThinks()) + } +} + +func TestAIManagerAddBrain(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc := NewMockNPC(123, "TestNPC") + brain := NewBaseBrain(npc, logger) + npcID := npc.GetID() + + err := manager.AddBrain(npcID, brain) + if err != nil { + t.Errorf("Expected no error adding brain, got: %v", err) + } + + if manager.GetBrainCount() != 1 { + t.Errorf("Expected brain count 1, got %d", manager.GetBrainCount()) + } + + if manager.GetActiveCount() != 1 { + t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) + } + + retrievedBrain := manager.GetBrain(npcID) + if retrievedBrain != brain { + t.Error("Retrieved brain should be the same as added brain") + } +} + +func TestAIManagerAddBrainDuplicate(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc := NewMockNPC(123, "TestNPC") + brain1 := NewBaseBrain(npc, logger) + brain2 := NewBaseBrain(npc, logger) + npcID := npc.GetID() + + err1 := manager.AddBrain(npcID, brain1) + err2 := manager.AddBrain(npcID, brain2) // Duplicate + + if err1 != nil { + t.Errorf("Expected no error adding first brain, got: %v", err1) + } + + if err2 == nil { + t.Error("Expected error adding duplicate brain") + } + + if manager.GetBrainCount() != 1 { + t.Errorf("Expected brain count 1 after duplicate add, got %d", manager.GetBrainCount()) + } +} + +func TestAIManagerRemoveBrain(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc := NewMockNPC(123, "TestNPC") + brain := NewBaseBrain(npc, logger) + npcID := npc.GetID() + + manager.AddBrain(npcID, brain) + manager.RemoveBrain(npcID) + + if manager.GetBrainCount() != 0 { + t.Errorf("Expected brain count 0 after removal, got %d", manager.GetBrainCount()) + } + + if manager.GetActiveCount() != 0 { + t.Errorf("Expected active count 0 after removal, got %d", manager.GetActiveCount()) + } + + retrievedBrain := manager.GetBrain(npcID) + if retrievedBrain != nil { + t.Error("Retrieved brain should be nil after removal") + } +} + +func TestAIManagerCreateBrainForNPC(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc := NewMockNPC(123, "TestNPC") + + err := manager.CreateBrainForNPC(npc, BrainTypeDefault) + if err != nil { + t.Errorf("Expected no error creating brain, got: %v", err) + } + + brain := manager.GetBrain(npc.GetID()) + if brain == nil { + t.Error("Expected brain to be created") + } + + if brain.GetBrainType() != BrainTypeDefault { + t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) + } +} + +func TestAIManagerSetBrainActive(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc := NewMockNPC(123, "TestNPC") + brain := NewBaseBrain(npc, logger) + npcID := npc.GetID() + + manager.AddBrain(npcID, brain) + + // Initially active + if manager.GetActiveCount() != 1 { + t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) + } + + // Set inactive + manager.SetBrainActive(npcID, false) + if manager.GetActiveCount() != 0 { + t.Errorf("Expected active count 0, got %d", manager.GetActiveCount()) + } + + // Set active again + manager.SetBrainActive(npcID, true) + if manager.GetActiveCount() != 1 { + t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) + } +} + +func TestAIManagerGetBrainsByType(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc1 := NewMockNPC(123, "TestNPC1") + npc2 := NewMockNPC(456, "TestNPC2") + npc3 := NewMockNPC(789, "TestNPC3") + + brain1 := NewBaseBrain(npc1, logger) + brain2 := NewCombatPetBrain(npc2, logger) + brain3 := NewBlankBrain(npc3, logger) + + manager.AddBrain(npc1.GetID(), brain1) + manager.AddBrain(npc2.GetID(), brain2) + manager.AddBrain(npc3.GetID(), brain3) + + defaultBrains := manager.GetBrainsByType(BrainTypeDefault) + if len(defaultBrains) != 1 { + t.Errorf("Expected 1 default brain, got %d", len(defaultBrains)) + } + + petBrains := manager.GetBrainsByType(BrainTypeCombatPet) + if len(petBrains) != 1 { + t.Errorf("Expected 1 combat pet brain, got %d", len(petBrains)) + } + + blankBrains := manager.GetBrainsByType(BrainTypeBlank) + if len(blankBrains) != 1 { + t.Errorf("Expected 1 blank brain, got %d", len(blankBrains)) + } +} + +func TestAIManagerClearAllBrains(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc1 := NewMockNPC(123, "TestNPC1") + npc2 := NewMockNPC(456, "TestNPC2") + + brain1 := NewBaseBrain(npc1, logger) + brain2 := NewBaseBrain(npc2, logger) + + manager.AddBrain(npc1.GetID(), brain1) + manager.AddBrain(npc2.GetID(), brain2) + + manager.ClearAllBrains() + + if manager.GetBrainCount() != 0 { + t.Errorf("Expected brain count 0 after clear, got %d", manager.GetBrainCount()) + } + + if manager.GetActiveCount() != 0 { + t.Errorf("Expected active count 0 after clear, got %d", manager.GetActiveCount()) + } +} + +func TestAIManagerGetStatistics(t *testing.T) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + npc1 := NewMockNPC(123, "TestNPC1") + npc2 := NewMockNPC(456, "TestNPC2") + + brain1 := NewBaseBrain(npc1, logger) + brain2 := NewCombatPetBrain(npc2, logger) + + manager.AddBrain(npc1.GetID(), brain1) + manager.AddBrain(npc2.GetID(), brain2) + + stats := manager.GetStatistics() + + if stats == nil { + t.Fatal("GetStatistics returned nil") + } + + if stats.TotalBrains != 2 { + t.Errorf("Expected total brains 2, got %d", stats.TotalBrains) + } + + if stats.ActiveBrains != 2 { + t.Errorf("Expected active brains 2, got %d", stats.ActiveBrains) + } + + if len(stats.BrainsByType) == 0 { + t.Error("Expected brain types to be populated") + } +} + +// Tests for utility functions + +func TestGetBrainTypeName(t *testing.T) { + testCases := []struct { + brainType int8 + expected string + }{ + {BrainTypeDefault, "default"}, + {BrainTypeCombatPet, "combat_pet"}, + {BrainTypeNonCombatPet, "non_combat_pet"}, + {BrainTypeBlank, "blank"}, + {BrainTypeLua, "lua"}, + {BrainTypeDumbFire, "dumbfire"}, + {99, "unknown"}, // Unknown type + } + + for _, tc := range testCases { + result := getBrainTypeName(tc.brainType) + if result != tc.expected { + t.Errorf("Expected brain type name '%s' for type %d, got '%s'", tc.expected, tc.brainType, result) + } + } +} + +func TestCurrentTimeMillis(t *testing.T) { + before := time.Now().UnixMilli() + result := currentTimeMillis() + after := time.Now().UnixMilli() + + if result < before || result > after { + t.Errorf("Expected current time to be between %d and %d, got %d", before, after, result) + } +} + +// Tests for AIBrainAdapter + +func TestNewAIBrainAdapter(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + + adapter := NewAIBrainAdapter(npc, logger) + + if adapter == nil { + t.Fatal("NewAIBrainAdapter returned nil") + } + + if adapter.GetNPC() != npc { + t.Error("Expected adapter NPC to be the provided NPC") + } +} + +func TestAIBrainAdapterSetupDefaultBrain(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + adapter := NewAIBrainAdapter(npc, logger) + + brain := adapter.SetupDefaultBrain() + + if brain == nil { + t.Fatal("SetupDefaultBrain returned nil") + } + + if brain.GetBrainType() != BrainTypeDefault { + t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) + } +} + +func TestAIBrainAdapterSetupPetBrain(t *testing.T) { + npc := NewMockNPC(123, "TestPet") + logger := NewMockLogger() + adapter := NewAIBrainAdapter(npc, logger) + + // Test combat pet brain + combatBrain := adapter.SetupPetBrain(true) + if combatBrain.GetBrainType() != BrainTypeCombatPet { + t.Errorf("Expected combat pet brain type %d, got %d", BrainTypeCombatPet, combatBrain.GetBrainType()) + } + + // Test non-combat pet brain + nonCombatBrain := adapter.SetupPetBrain(false) + if nonCombatBrain.GetBrainType() != BrainTypeNonCombatPet { + t.Errorf("Expected non-combat pet brain type %d, got %d", BrainTypeNonCombatPet, nonCombatBrain.GetBrainType()) + } +} + +func TestAIBrainAdapterProcessAI(t *testing.T) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + adapter := NewAIBrainAdapter(npc, logger) + + brain := NewBaseBrain(npc, logger) + + err := adapter.ProcessAI(brain) + if err != nil { + t.Errorf("Expected no error processing AI, got: %v", err) + } + + // Test with nil brain + err = adapter.ProcessAI(nil) + if err == nil { + t.Error("Expected error processing AI with nil brain") + } + + // Test with inactive brain + brain.SetActive(false) + err = adapter.ProcessAI(brain) + if err != nil { + t.Errorf("Expected no error processing inactive AI, got: %v", err) + } +} + +// Tests for HateListDebugger + +func TestNewHateListDebugger(t *testing.T) { + logger := NewMockLogger() + debugger := NewHateListDebugger(logger) + + if debugger == nil { + t.Fatal("NewHateListDebugger returned nil") + } +} + +func TestHateListDebuggerPrintHateList(t *testing.T) { + logger := NewMockLogger() + debugger := NewHateListDebugger(logger) + + hateList := make(map[int32]*HateEntry) + hateList[123] = &HateEntry{EntityID: 123, HateValue: 500} + hateList[456] = &HateEntry{EntityID: 456, HateValue: 300} + + debugger.PrintHateList("TestNPC", hateList) + + messages := logger.GetMessages() + if len(messages) == 0 { + t.Error("Expected debug messages to be logged") + } +} + +func TestHateListDebuggerPrintEncounterList(t *testing.T) { + logger := NewMockLogger() + debugger := NewHateListDebugger(logger) + + encounterList := make(map[int32]*EncounterEntry) + encounterList[123] = &EncounterEntry{EntityID: 123, IsPlayer: true} + encounterList[456] = &EncounterEntry{EntityID: 456, IsBot: true} + + debugger.PrintEncounterList("TestNPC", encounterList) + + messages := logger.GetMessages() + if len(messages) == 0 { + t.Error("Expected debug messages to be logged") + } +} + +// Benchmark tests + +func BenchmarkHateListAddHate(b *testing.B) { + hateList := NewHateList() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hateList.AddHate(int32(i), 100) + } +} + +func BenchmarkHateListGetMostHated(b *testing.B) { + hateList := NewHateList() + + // Populate with some data + for i := 0; i < 100; i++ { + hateList.AddHate(int32(i), int32(i*10)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hateList.GetMostHated() + } +} + +func BenchmarkBrainThink(b *testing.B) { + npc := NewMockNPC(123, "TestNPC") + logger := NewMockLogger() + brain := NewBaseBrain(npc, logger) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + brain.Think() + } +} + +func BenchmarkEncounterListAddEntity(b *testing.B) { + encounterList := NewEncounterList() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + encounterList.AddEntity(int32(i), int32(i*10), i%2 == 0, i%3 == 0) + } +} + +func BenchmarkAIManagerProcessAllBrains(b *testing.B) { + logger := NewMockLogger() + luaInterface := NewMockLuaInterface() + manager := NewAIManager(logger, luaInterface) + + // Add some brains + for i := 0; i < 10; i++ { + npc := NewMockNPC(int32(i), "TestNPC") + brain := NewBaseBrain(npc, logger) + manager.AddBrain(int32(i), brain) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + manager.ProcessAllBrains() + } +} \ No newline at end of file diff --git a/internal/npc/ai/brain.go b/internal/npc/ai/brain.go index 44976de..c29788d 100644 --- a/internal/npc/ai/brain.go +++ b/internal/npc/ai/brain.go @@ -184,8 +184,6 @@ func (bb *BaseBrain) Think() error { } } else { // No target - handle out of combat behavior - wasInCombat := bb.npc.GetInCombat() - if bb.npc.GetInCombat() { bb.npc.InCombat(false) diff --git a/internal/npc/ai/types.go b/internal/npc/ai/types.go index de00d40..1471023 100644 --- a/internal/npc/ai/types.go +++ b/internal/npc/ai/types.go @@ -360,7 +360,7 @@ type BrainState struct { LastThink int64 // Timestamp of last think cycle ThinkTick int32 // Time between think cycles in milliseconds SpellRecovery int64 // Timestamp when spell recovery completes - IsActive bool // Whether the brain is active + active bool // Whether the brain is active DebugLevel int8 // Debug output level mutex sync.RWMutex } @@ -372,7 +372,7 @@ func NewBrainState() *BrainState { LastThink: time.Now().UnixMilli(), ThinkTick: DefaultThinkTick, SpellRecovery: 0, - IsActive: true, + active: true, DebugLevel: DebugLevelNone, } } @@ -453,14 +453,14 @@ func (bs *BrainState) HasRecovered() bool { func (bs *BrainState) IsActive() bool { bs.mutex.RLock() defer bs.mutex.RUnlock() - return bs.IsActive + return bs.active } // SetActive sets the brain's active state func (bs *BrainState) SetActive(active bool) { bs.mutex.Lock() defer bs.mutex.Unlock() - bs.IsActive = active + bs.active = active } // GetDebugLevel returns the debug level diff --git a/internal/npc/constants.go b/internal/npc/constants.go index c3b088c..a929066 100644 --- a/internal/npc/constants.go +++ b/internal/npc/constants.go @@ -82,7 +82,7 @@ const ( // Color randomization constants const ( ColorRandomMin int8 = 0 - ColorRandomMax int8 = 255 + ColorRandomMax int8 = 127 // Max value for int8 ColorVariation int8 = 30 ) diff --git a/internal/npc/manager.go b/internal/npc/manager.go index f3e167c..1558855 100644 --- a/internal/npc/manager.go +++ b/internal/npc/manager.go @@ -5,7 +5,6 @@ import ( "math/rand" "strings" "sync" - "time" ) // Manager provides high-level management of the NPC system @@ -116,10 +115,11 @@ func (m *Manager) addNPCInternal(npc *NPC) error { m.npcs[npcID] = npc // Add to zone index - if npc.Entity != nil { - zoneID := npc.Entity.GetZoneID() - m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) - } + // TODO: Add zone support when Entity.GetZoneID() is available + // if npc.Entity != nil { + // zoneID := npc.Entity.GetZoneID() + // m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) + // } // Add to appearance index appearanceID := npc.GetAppearanceID() @@ -236,13 +236,14 @@ func (m *Manager) UpdateNPC(npc *NPC) error { } // Update indexes if zone or appearance changed - if npc.Entity != nil && oldNPC.Entity != nil { - if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() { - m.removeFromZoneIndex(oldNPC) - zoneID := npc.Entity.GetZoneID() - m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) - } - } + // TODO: Add zone support when Entity.GetZoneID() is available + // if npc.Entity != nil && oldNPC.Entity != nil { + // if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() { + // m.removeFromZoneIndex(oldNPC) + // zoneID := npc.Entity.GetZoneID() + // m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) + // } + // } if npc.GetAppearanceID() != oldNPC.GetAppearanceID() { m.removeFromAppearanceIndex(oldNPC) @@ -311,7 +312,9 @@ func (m *Manager) ProcessCombat() { m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { - if npc.Entity != nil && npc.Entity.GetInCombat() { + // TODO: Add combat status check when GetInCombat() is available + // if npc.Entity != nil && npc.Entity.GetInCombat() { + if npc.Entity != nil { npcs = append(npcs, npc) } } @@ -574,8 +577,9 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) { if npc.Entity != nil { result += fmt.Sprintf("Name: %s\n", npc.Entity.GetName()) result += fmt.Sprintf("Level: %d\n", npc.Entity.GetLevel()) - result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID()) - result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat()) + // TODO: Add zone and combat status when methods are available + // result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID()) + // result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat()) } return result, nil @@ -594,7 +598,7 @@ func (m *Manager) handleCreateCommand(args []string) (string, error) { return "", fmt.Errorf("invalid new ID: %s", args[1]) } - npc, err := m.CreateNPCFromTemplate(templateID, newID) + _, err := m.CreateNPCFromTemplate(templateID, newID) if err != nil { return "", fmt.Errorf("failed to create NPC: %w", err) } @@ -669,21 +673,20 @@ func (m *Manager) removeFromZoneIndex(npc *NPC) { return } - zoneID := npc.Entity.GetZoneID() - npcs := m.npcsByZone[zoneID] - - for i, n := range npcs { - if n == npc { - // Remove from slice - m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...) - break - } - } - - // Clean up empty slices - if len(m.npcsByZone[zoneID]) == 0 { - delete(m.npcsByZone, zoneID) - } + // TODO: Implement zone index removal when Entity.GetZoneID() is available + // zoneID := npc.Entity.GetZoneID() + // npcs := m.npcsByZone[zoneID] + // for i, n := range npcs { + // if n == npc { + // // Remove from slice + // m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...) + // break + // } + // } + // // Clean up empty slices + // if len(m.npcsByZone[zoneID]) == 0 { + // delete(m.npcsByZone, zoneID) + // } } func (m *Manager) removeFromAppearanceIndex(npc *NPC) { diff --git a/internal/npc/npc.go b/internal/npc/npc.go index 14629e0..b1042b1 100644 --- a/internal/npc/npc.go +++ b/internal/npc/npc.go @@ -4,12 +4,8 @@ import ( "fmt" "math" "math/rand" - "sync" - "time" - "eq2emu/internal/common" "eq2emu/internal/entity" - "eq2emu/internal/spawn" ) // NewNPC creates a new NPC with default values @@ -82,19 +78,21 @@ func NewNPCFromExisting(oldNPC *NPC) *NPC { npc.equipmentListID = oldNPC.equipmentListID // Copy entity data (stats, appearance, etc.) - if oldNPC.Entity != nil { - npc.Entity = oldNPC.Entity.Copy().(*entity.Entity) - } + // TODO: Implement entity copying when Entity.Copy() is available + // if oldNPC.Entity != nil { + // npc.Entity = oldNPC.Entity.Copy().(*entity.Entity) + // } // Handle level randomization - if oldNPC.Entity != nil { - minLevel := oldNPC.Entity.GetMinLevel() - maxLevel := oldNPC.Entity.GetMaxLevel() - if minLevel < maxLevel { - randomLevel := minLevel + int8(rand.Intn(int(maxLevel-minLevel)+1)) - npc.Entity.SetLevel(randomLevel) - } - } + // TODO: Implement level randomization when GetMinLevel/GetMaxLevel are available + // if oldNPC.Entity != nil { + // minLevel := oldNPC.Entity.GetMinLevel() + // maxLevel := oldNPC.Entity.GetMaxLevel() + // if minLevel < maxLevel { + // randomLevel := minLevel + int8(rand.Intn(int(maxLevel-minLevel)+1)) + // npc.Entity.SetLevel(randomLevel) + // } + // } // Copy skills (deep copy) npc.copySkills(oldNPC) @@ -103,9 +101,10 @@ func NewNPCFromExisting(oldNPC *NPC) *NPC { npc.copySpells(oldNPC) // Handle appearance randomization - if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 { - npc.randomizeAppearance(oldNPC.Entity.GetRandomize()) - } + // TODO: Implement appearance randomization when GetRandomize is available + // if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 { + // npc.randomizeAppearance(oldNPC.Entity.GetRandomize()) + // } return npc } @@ -514,7 +513,7 @@ func (n *NPC) StartRunback(resetHP bool) { X: n.Entity.GetX(), Y: n.Entity.GetY(), Z: n.Entity.GetZ(), - GridID: n.Entity.GetLocation(), + GridID: 0, // TODO: Implement grid system Stage: 0, ResetHPOnRunback: resetHP, UseNavPath: false, @@ -522,8 +521,11 @@ func (n *NPC) StartRunback(resetHP bool) { } // Store original heading - n.runbackHeadingDir1 = n.Entity.GetHeading() - n.runbackHeadingDir2 = n.Entity.GetHeading() // In C++ these are separate values + // TODO: Implement heading storage when Entity.GetHeading() returns compatible type + // n.runbackHeadingDir1 = int16(n.Entity.GetHeading()) + // n.runbackHeadingDir2 = int16(n.Entity.GetHeading()) // In C++ these are separate values + n.runbackHeadingDir1 = 0 + n.runbackHeadingDir2 = 0 } // Runback initiates runback movement @@ -544,7 +546,8 @@ func (n *NPC) Runback(distance float32, stopFollowing bool) { // This would integrate with the movement system if stopFollowing && n.Entity != nil { - n.Entity.SetFollowing(false) + // TODO: Implement SetFollowing when available on Entity + // n.Entity.SetFollowing(false) } } @@ -661,12 +664,12 @@ func (n *NPC) InCombat(val bool) { return } - currentCombat := n.Entity.GetInCombat() - if currentCombat == val { - return - } - - n.Entity.SetInCombat(val) + // TODO: Implement GetInCombat and SetInCombat when available on Entity + // currentCombat := n.Entity.GetInCombat() + // if currentCombat == val { + // return + // } + // n.Entity.SetInCombat(val) if val { // Entering combat @@ -675,9 +678,10 @@ func (n *NPC) InCombat(val bool) { } // Set max speed for combat - if n.Entity.GetMaxSpeed() > 0 { - n.Entity.SetSpeed(n.Entity.GetMaxSpeed()) - } + // TODO: Implement GetMaxSpeed and SetSpeed when available on Entity + // if n.Entity.GetMaxSpeed() > 0 { + // n.Entity.SetSpeed(n.Entity.GetMaxSpeed()) + // } // TODO: Add combat icon, call spawn scripts, etc. @@ -734,7 +738,7 @@ func (n *NPC) copySpells(oldNPC *NPC) { } // Also copy cast-on spells - for castType, spells := range oldNPC.castOnSpells { + for _, spells := range oldNPC.castOnSpells { for _, spell := range spells { if spell != nil { oldSpells = append(oldSpells, spell.Copy()) @@ -758,15 +762,16 @@ func (n *NPC) randomizeAppearance(flags int32) { // Random gender if flags&RandomizeGender != 0 { - gender := int8(rand.Intn(2) + 1) // 1 or 2 - n.Entity.SetGender(gender) + // TODO: Implement SetGender when available on Entity + // gender := int8(rand.Intn(2) + 1) // 1 or 2 + // n.Entity.SetGender(gender) } // Random race (simplified) if flags&RandomizeRace != 0 { - // TODO: Implement race randomization based on alignment - race := int16(rand.Intn(21)) // 0-20 for basic races - n.Entity.SetRace(race) + // TODO: Implement SetRace when available on Entity + // race := int16(rand.Intn(21)) // 0-20 for basic races + // n.Entity.SetRace(race) } // Color randomization diff --git a/internal/npc/npc_test.go b/internal/npc/npc_test.go index a26d373..fb6125d 100644 --- a/internal/npc/npc_test.go +++ b/internal/npc/npc_test.go @@ -1,20 +1,767 @@ package npc import ( + "fmt" + "strings" "testing" ) -func TestPackageBuild(t *testing.T) { - // Basic test to verify the package builds - manager := NewNPCManager() - if manager == nil { - t.Fatal("NewNPCManager returned nil") +// 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 TestNPCBasics(t *testing.T) { - npcData := &NPC{} - if npcData == nil { - t.Fatal("NPC struct should be accessible") +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() } } \ No newline at end of file diff --git a/internal/npc/race_types/database.go b/internal/npc/race_types/database.go index 7e0b63d..e847d54 100644 --- a/internal/npc/race_types/database.go +++ b/internal/npc/race_types/database.go @@ -1,119 +1,123 @@ package race_types import ( - "database/sql" + "context" "fmt" - "log" + + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) -// DatabaseLoader provides database operations for race types -type DatabaseLoader struct { - db *sql.DB +// SQLiteDatabase provides SQLite database operations for race types +type SQLiteDatabase struct { + pool *sqlitex.Pool } -// NewDatabaseLoader creates a new database loader -func NewDatabaseLoader(db *sql.DB) *DatabaseLoader { - return &DatabaseLoader{db: db} +// NewSQLiteDatabase creates a new SQLite database implementation +func NewSQLiteDatabase(pool *sqlitex.Pool) *SQLiteDatabase { + return &SQLiteDatabase{pool: pool} } // LoadRaceTypes loads all race types from the database -// Converted from C++ WorldDatabase::LoadRaceTypes -func (dl *DatabaseLoader) LoadRaceTypes(masterList *MasterRaceTypeList) error { +func (db *SQLiteDatabase) LoadRaceTypes(masterList *MasterRaceTypeList) error { + conn, err := db.pool.Take(context.Background()) + if err != nil { + return fmt.Errorf("failed to get connection: %w", err) + } + defer db.pool.Put(conn) + query := ` SELECT model_type, race_id, category, subcategory, model_name FROM race_types WHERE race_id > 0 ` - - rows, err := dl.db.Query(query) + + count := 0 + err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + modelType := int16(stmt.ColumnInt(0)) + raceID := int16(stmt.ColumnInt(1)) + category := stmt.ColumnText(2) + subcategory := stmt.ColumnText(3) + modelName := stmt.ColumnText(4) + + // Add to master list + if masterList.AddRaceType(modelType, raceID, category, subcategory, modelName, false) { + count++ + } + return nil + }, + }) + if err != nil { return fmt.Errorf("failed to query race types: %w", err) } - defer rows.Close() - - count := 0 - for rows.Next() { - var modelType, raceID int16 - var category, subcategory, modelName sql.NullString - - err := rows.Scan(&modelType, &raceID, &category, &subcategory, &modelName) - if err != nil { - log.Printf("Error scanning race type row: %v", err) - continue - } - - // Convert null strings to empty strings - categoryStr := "" - if category.Valid { - categoryStr = category.String - } - - subcategoryStr := "" - if subcategory.Valid { - subcategoryStr = subcategory.String - } - - modelNameStr := "" - if modelName.Valid { - modelNameStr = modelName.String - } - - // Add to master list - if masterList.AddRaceType(modelType, raceID, categoryStr, subcategoryStr, modelNameStr, false) { - count++ - } - } - - if err := rows.Err(); err != nil { - return fmt.Errorf("error iterating race type rows: %w", err) - } - - log.Printf("Loaded %d race types from database", count) + return nil } // SaveRaceType saves a single race type to the database -func (dl *DatabaseLoader) SaveRaceType(modelType int16, raceType *RaceType) error { +func (db *SQLiteDatabase) SaveRaceType(modelType int16, raceType *RaceType) error { if raceType == nil || !raceType.IsValid() { return fmt.Errorf("invalid race type") } - + + conn, err := db.pool.Take(context.Background()) + if err != nil { + return fmt.Errorf("failed to get connection: %w", err) + } + defer db.pool.Put(conn) + query := ` INSERT OR REPLACE INTO race_types (model_type, race_id, category, subcategory, model_name) VALUES (?, ?, ?, ?, ?) ` - - _, err := dl.db.Exec(query, modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName) + + err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + Args: []interface{}{modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName}, + }) + if err != nil { return fmt.Errorf("failed to save race type: %w", err) } - + return nil } // DeleteRaceType removes a race type from the database -func (dl *DatabaseLoader) DeleteRaceType(modelType int16) error { +func (db *SQLiteDatabase) DeleteRaceType(modelType int16) error { + conn, err := db.pool.Take(context.Background()) + if err != nil { + return fmt.Errorf("failed to get connection: %w", err) + } + defer db.pool.Put(conn) + query := `DELETE FROM race_types WHERE model_type = ?` - - result, err := dl.db.Exec(query, modelType) + + err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + Args: []interface{}{modelType}, + }) + if err != nil { return fmt.Errorf("failed to delete race type: %w", err) } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %w", err) - } - + + rowsAffected := int64(conn.Changes()) if rowsAffected == 0 { return fmt.Errorf("race type with model_type %d not found", modelType) } - + return nil } // CreateRaceTypesTable creates the race_types table if it doesn't exist -func (dl *DatabaseLoader) CreateRaceTypesTable() error { +func (db *SQLiteDatabase) CreateRaceTypesTable() error { + conn, err := db.pool.Take(context.Background()) + if err != nil { + return fmt.Errorf("failed to get connection: %w", err) + } + defer db.pool.Put(conn) + query := ` CREATE TABLE IF NOT EXISTS race_types ( model_type INTEGER PRIMARY KEY, @@ -124,25 +128,22 @@ func (dl *DatabaseLoader) CreateRaceTypesTable() error { CHECK (race_id > 0) ) ` - - _, err := dl.db.Exec(query) - if err != nil { + + if err := sqlitex.ExecuteTransient(conn, query, nil); err != nil { return fmt.Errorf("failed to create race_types table: %w", err) } - + // Create index on race_id for faster lookups indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_race_id ON race_types(race_id)` - _, err = dl.db.Exec(indexQuery) - if err != nil { + if err := sqlitex.ExecuteTransient(conn, indexQuery, nil); err != nil { return fmt.Errorf("failed to create race_id index: %w", err) } - + // Create index on category for category-based queries categoryIndexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_category ON race_types(category)` - _, err = dl.db.Exec(categoryIndexQuery) - if err != nil { + if err := sqlitex.ExecuteTransient(conn, categoryIndexQuery, nil); err != nil { return fmt.Errorf("failed to create category index: %w", err) } - + return nil } \ No newline at end of file diff --git a/internal/npc/race_types/database_test.go b/internal/npc/race_types/database_test.go new file mode 100644 index 0000000..b336092 --- /dev/null +++ b/internal/npc/race_types/database_test.go @@ -0,0 +1,502 @@ +package race_types + +import ( + "context" + "fmt" + "path/filepath" + "testing" + + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +func TestSQLiteDatabase(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Test table creation + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Verify table exists + conn, err := pool.Take(context.Background()) + if err != nil { + t.Fatalf("Failed to get connection: %v", err) + } + defer pool.Put(conn) + + var tableExists bool + err = sqlitex.ExecuteTransient(conn, "SELECT name FROM sqlite_master WHERE type='table' AND name='race_types'", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + tableExists = true + return nil + }, + }) + if err != nil { + t.Fatalf("Failed to check table existence: %v", err) + } + + if !tableExists { + t.Error("race_types table should exist") + } +} + +func TestSQLiteDatabaseOperations(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types_ops.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Create table + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Test saving race type + raceType := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Male", + } + + err = db.SaveRaceType(100, raceType) + if err != nil { + t.Fatalf("Failed to save race type: %v", err) + } + + // Test loading race types + masterList := NewMasterRaceTypeList() + err = db.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types: %v", err) + } + + if masterList.Count() != 1 { + t.Errorf("Expected 1 race type, got %d", masterList.Count()) + } + + retrievedRaceType := masterList.GetRaceType(100) + if retrievedRaceType != Sentient { + t.Errorf("Expected race type %d, got %d", Sentient, retrievedRaceType) + } + + retrievedInfo := masterList.GetRaceTypeByModelID(100) + if retrievedInfo == nil { + t.Fatal("Should retrieve race type info") + } + + if retrievedInfo.Category != CategorySentient { + t.Errorf("Expected category %s, got %s", CategorySentient, retrievedInfo.Category) + } + + if retrievedInfo.Subcategory != "Human" { + t.Errorf("Expected subcategory 'Human', got %s", retrievedInfo.Subcategory) + } + + if retrievedInfo.ModelName != "Human Male" { + t.Errorf("Expected model name 'Human Male', got %s", retrievedInfo.ModelName) + } + + // Test updating (replace) + updatedRaceType := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Female", + } + + err = db.SaveRaceType(100, updatedRaceType) + if err != nil { + t.Fatalf("Failed to update race type: %v", err) + } + + // Reload and verify update + masterList = NewMasterRaceTypeList() + err = db.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types after update: %v", err) + } + + updatedInfo := masterList.GetRaceTypeByModelID(100) + if updatedInfo.ModelName != "Human Female" { + t.Errorf("Expected updated model name 'Human Female', got %s", updatedInfo.ModelName) + } + + // Test deletion + err = db.DeleteRaceType(100) + if err != nil { + t.Fatalf("Failed to delete race type: %v", err) + } + + // Verify deletion + masterList = NewMasterRaceTypeList() + err = db.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types after deletion: %v", err) + } + + if masterList.Count() != 0 { + t.Errorf("Expected 0 race types after deletion, got %d", masterList.Count()) + } + + // Test deletion of non-existent + err = db.DeleteRaceType(999) + if err == nil { + t.Error("Should fail to delete non-existent race type") + } +} + +func TestSQLiteDatabaseMultipleRaceTypes(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types_multi.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Create table + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Test data + testData := []struct { + modelID int16 + raceTypeID int16 + category string + subcategory string + modelName string + }{ + {100, Sentient, CategorySentient, "Human", "Human Male"}, + {101, Sentient, CategorySentient, "Human", "Human Female"}, + {200, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior"}, + {201, Undead, CategoryUndead, "Zombie", "Zombie Shambler"}, + {300, Natural, CategoryNatural, "Wolf", "Dire Wolf"}, + {301, Natural, CategoryNatural, "Bear", "Grizzly Bear"}, + } + + // Save all test data + for _, data := range testData { + raceType := &RaceType{ + RaceTypeID: data.raceTypeID, + Category: data.category, + Subcategory: data.subcategory, + ModelName: data.modelName, + } + + err = db.SaveRaceType(data.modelID, raceType) + if err != nil { + t.Fatalf("Failed to save race type %d: %v", data.modelID, err) + } + } + + // Load and verify all data + masterList := NewMasterRaceTypeList() + err = db.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types: %v", err) + } + + if masterList.Count() != len(testData) { + t.Errorf("Expected %d race types, got %d", len(testData), masterList.Count()) + } + + // Verify each race type + for _, data := range testData { + retrievedRaceType := masterList.GetRaceType(data.modelID) + if retrievedRaceType != data.raceTypeID { + t.Errorf("Model %d: expected race type %d, got %d", data.modelID, data.raceTypeID, retrievedRaceType) + } + + retrievedInfo := masterList.GetRaceTypeByModelID(data.modelID) + if retrievedInfo == nil { + t.Errorf("Model %d: should have race type info", data.modelID) + continue + } + + if retrievedInfo.Category != data.category { + t.Errorf("Model %d: expected category %s, got %s", data.modelID, data.category, retrievedInfo.Category) + } + + if retrievedInfo.Subcategory != data.subcategory { + t.Errorf("Model %d: expected subcategory %s, got %s", data.modelID, data.subcategory, retrievedInfo.Subcategory) + } + + if retrievedInfo.ModelName != data.modelName { + t.Errorf("Model %d: expected model name %s, got %s", data.modelID, data.modelName, retrievedInfo.ModelName) + } + } + + // Test category-based queries by verifying the loaded data + sentientTypes := masterList.GetRaceTypesByCategory(CategorySentient) + if len(sentientTypes) != 2 { + t.Errorf("Expected 2 sentient types, got %d", len(sentientTypes)) + } + + undeadTypes := masterList.GetRaceTypesByCategory(CategoryUndead) + if len(undeadTypes) != 2 { + t.Errorf("Expected 2 undead types, got %d", len(undeadTypes)) + } + + naturalTypes := masterList.GetRaceTypesByCategory(CategoryNatural) + if len(naturalTypes) != 2 { + t.Errorf("Expected 2 natural types, got %d", len(naturalTypes)) + } +} + +func TestSQLiteDatabaseInvalidRaceType(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types_invalid.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Create table + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Test saving nil race type + err = db.SaveRaceType(100, nil) + if err == nil { + t.Error("Should fail to save nil race type") + } + + // Test saving invalid race type + invalidRaceType := &RaceType{ + RaceTypeID: 0, // Invalid + Category: "", + Subcategory: "", + ModelName: "", + } + + err = db.SaveRaceType(100, invalidRaceType) + if err == nil { + t.Error("Should fail to save invalid race type") + } +} + +func TestSQLiteDatabaseIndexes(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types_indexes.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Create table + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Verify indexes exist + conn, err := pool.Take(context.Background()) + if err != nil { + t.Fatalf("Failed to get connection: %v", err) + } + defer pool.Put(conn) + + indexes := []string{ + "idx_race_types_race_id", + "idx_race_types_category", + } + + for _, indexName := range indexes { + var indexExists bool + query := "SELECT name FROM sqlite_master WHERE type='index' AND name=?" + err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ + Args: []interface{}{indexName}, + ResultFunc: func(stmt *sqlite.Stmt) error { + indexExists = true + return nil + }, + }) + if err != nil { + t.Fatalf("Failed to check index %s: %v", indexName, err) + } + + if !indexExists { + t.Errorf("Index %s should exist", indexName) + } + } +} + +func TestSQLiteDatabaseConcurrency(t *testing.T) { + // Create temporary database + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test_race_types_concurrent.db") + + // Create database pool with multiple connections + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 3, + }) + if err != nil { + t.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + + // Create table + err = db.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + // Test concurrent operations + const numOperations = 10 + results := make(chan error, numOperations) + + // Concurrent saves + for i := 0; i < numOperations; i++ { + go func(id int) { + raceType := &RaceType{ + RaceTypeID: int16(id%5 + 1), + Category: CategorySentient, + Subcategory: "Test", + ModelName: fmt.Sprintf("Test Model %d", id), + } + results <- db.SaveRaceType(int16(100+id), raceType) + }(i) + } + + // Wait for all operations to complete + for i := 0; i < numOperations; i++ { + if err := <-results; err != nil { + t.Errorf("Concurrent save operation failed: %v", err) + } + } + + // Verify all data was saved + masterList := NewMasterRaceTypeList() + err = db.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types after concurrent operations: %v", err) + } + + if masterList.Count() != numOperations { + t.Errorf("Expected %d race types after concurrent operations, got %d", numOperations, masterList.Count()) + } +} + +// Benchmark tests for SQLite database + +func BenchmarkSQLiteDatabaseSave(b *testing.B) { + // Create temporary database + tempDir := b.TempDir() + dbPath := filepath.Join(tempDir, "bench_race_types_save.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + b.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + db.CreateRaceTypesTable() + + raceType := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Male", + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + db.SaveRaceType(int16(i), raceType) + } +} + +func BenchmarkSQLiteDatabaseLoad(b *testing.B) { + // Create temporary database with test data + tempDir := b.TempDir() + dbPath := filepath.Join(tempDir, "bench_race_types_load.db") + + // Create database pool + pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ + PoolSize: 1, + }) + if err != nil { + b.Fatalf("Failed to create database pool: %v", err) + } + defer pool.Close() + + db := NewSQLiteDatabase(pool) + db.CreateRaceTypesTable() + + // Add test data + raceType := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Male", + } + + for i := 0; i < 1000; i++ { + db.SaveRaceType(int16(i), raceType) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + masterList := NewMasterRaceTypeList() + db.LoadRaceTypes(masterList) + } +} \ No newline at end of file diff --git a/internal/npc/race_types/interfaces.go b/internal/npc/race_types/interfaces.go index d6c16bc..e8fa271 100644 --- a/internal/npc/race_types/interfaces.go +++ b/internal/npc/race_types/interfaces.go @@ -1,5 +1,21 @@ package race_types +// Database interface for race type persistence +type Database interface { + LoadRaceTypes(masterList *MasterRaceTypeList) error + SaveRaceType(modelType int16, raceType *RaceType) error + DeleteRaceType(modelType int16) error + CreateRaceTypesTable() error +} + +// Logger interface for race type logging +type Logger interface { + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) +} + // RaceTypeProvider defines the interface for accessing race type information type RaceTypeProvider interface { // GetRaceType returns the race type ID for a given model ID diff --git a/internal/npc/race_types/manager.go b/internal/npc/race_types/manager.go index 9717484..e5a036b 100644 --- a/internal/npc/race_types/manager.go +++ b/internal/npc/race_types/manager.go @@ -1,9 +1,7 @@ package race_types import ( - "database/sql" "fmt" - "log" "strings" "sync" ) @@ -11,19 +9,19 @@ import ( // Manager provides high-level race type management type Manager struct { masterList *MasterRaceTypeList - dbLoader *DatabaseLoader - db *sql.DB + database Database + logger Logger // Thread safety for manager operations mutex sync.RWMutex } // NewManager creates a new race type manager -func NewManager(db *sql.DB) *Manager { +func NewManager(database Database, logger Logger) *Manager { return &Manager{ masterList: NewMasterRaceTypeList(), - dbLoader: NewDatabaseLoader(db), - db: db, + database: database, + logger: logger, } } @@ -33,16 +31,18 @@ func (m *Manager) Initialize() error { defer m.mutex.Unlock() // Create table if needed - if err := m.dbLoader.CreateRaceTypesTable(); err != nil { + if err := m.database.CreateRaceTypesTable(); err != nil { return fmt.Errorf("failed to create race types table: %w", err) } // Load race types from database - if err := m.dbLoader.LoadRaceTypes(m.masterList); err != nil { + if err := m.database.LoadRaceTypes(m.masterList); err != nil { return fmt.Errorf("failed to load race types: %w", err) } - log.Printf("Race type system initialized with %d race types", m.masterList.Count()) + if m.logger != nil { + m.logger.LogInfo("Race type system initialized with %d race types", m.masterList.Count()) + } return nil } @@ -99,10 +99,10 @@ func (m *Manager) AddRaceType(modelID int16, raceTypeID int16, category, subcate ModelName: modelName, } - if err := m.dbLoader.SaveRaceType(modelID, raceType); err != nil { + if err := m.database.SaveRaceType(modelID, raceType); err != nil { // Rollback from master list m.masterList.Clear() // This is not ideal but ensures consistency - m.dbLoader.LoadRaceTypes(m.masterList) + m.database.LoadRaceTypes(m.masterList) return fmt.Errorf("failed to save race type: %w", err) } @@ -132,10 +132,10 @@ func (m *Manager) UpdateRaceType(modelID int16, raceTypeID int16, category, subc ModelName: modelName, } - if err := m.dbLoader.SaveRaceType(modelID, raceType); err != nil { + if err := m.database.SaveRaceType(modelID, raceType); err != nil { // Reload from database to ensure consistency m.masterList.Clear() - m.dbLoader.LoadRaceTypes(m.masterList) + m.database.LoadRaceTypes(m.masterList) return fmt.Errorf("failed to update race type in database: %w", err) } @@ -153,13 +153,13 @@ func (m *Manager) RemoveRaceType(modelID int16) error { } // Delete from database first - if err := m.dbLoader.DeleteRaceType(modelID); err != nil { + if err := m.database.DeleteRaceType(modelID); err != nil { return fmt.Errorf("failed to delete race type from database: %w", err) } // Reload master list to ensure consistency m.masterList.Clear() - m.dbLoader.LoadRaceTypes(m.masterList) + m.database.LoadRaceTypes(m.masterList) return nil } diff --git a/internal/npc/race_types/race_types_test.go b/internal/npc/race_types/race_types_test.go new file mode 100644 index 0000000..9fb3cb0 --- /dev/null +++ b/internal/npc/race_types/race_types_test.go @@ -0,0 +1,550 @@ +package race_types + +import ( + "fmt" + "testing" +) + +// Mock implementations for testing + +// MockDatabase implements the Database interface for testing +type MockDatabase struct { + raceTypes map[int16]*RaceType + created bool +} + +func NewMockDatabase() *MockDatabase { + return &MockDatabase{ + raceTypes: make(map[int16]*RaceType), + created: false, + } +} + +func (md *MockDatabase) LoadRaceTypes(masterList *MasterRaceTypeList) error { + for modelType, raceType := range md.raceTypes { + masterList.AddRaceType(modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName, false) + } + return nil +} + +func (md *MockDatabase) SaveRaceType(modelType int16, raceType *RaceType) error { + if raceType == nil || !raceType.IsValid() { + return fmt.Errorf("invalid race type") + } + md.raceTypes[modelType] = &RaceType{ + RaceTypeID: raceType.RaceTypeID, + Category: raceType.Category, + Subcategory: raceType.Subcategory, + ModelName: raceType.ModelName, + } + return nil +} + +func (md *MockDatabase) DeleteRaceType(modelType int16) error { + if _, exists := md.raceTypes[modelType]; !exists { + return fmt.Errorf("race type with model_type %d not found", modelType) + } + delete(md.raceTypes, modelType) + return nil +} + +func (md *MockDatabase) CreateRaceTypesTable() error { + md.created = true + 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] +} + +// Mock entity for testing race type aware interface +type MockEntity struct { + modelType int16 +} + +func NewMockEntity(modelType int16) *MockEntity { + return &MockEntity{modelType: modelType} +} + +func (me *MockEntity) GetModelType() int16 { + return me.modelType +} + +func (me *MockEntity) SetModelType(modelType int16) { + me.modelType = modelType +} + +// Test functions + +func TestRaceTypeBasics(t *testing.T) { + rt := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Male", + } + + if !rt.IsValid() { + t.Error("Race type should be valid") + } + + if rt.RaceTypeID != Sentient { + t.Errorf("Expected race type ID %d, got %d", Sentient, rt.RaceTypeID) + } + + if rt.Category != CategorySentient { + t.Errorf("Expected category %s, got %s", CategorySentient, rt.Category) + } +} + +func TestRaceTypeInvalid(t *testing.T) { + rt := &RaceType{ + RaceTypeID: 0, // Invalid + Category: "", + Subcategory: "", + ModelName: "", + } + + if rt.IsValid() { + t.Error("Race type with zero ID should not be valid") + } +} + +func TestMasterRaceTypeList(t *testing.T) { + masterList := NewMasterRaceTypeList() + + // Test initial state + if masterList.Count() != 0 { + t.Errorf("Expected count 0, got %d", masterList.Count()) + } + + // Add a race type + modelID := int16(100) + raceTypeID := int16(Sentient) + category := CategorySentient + subcategory := "Human" + modelName := "Human Male" + + if !masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) { + t.Error("Failed to add race type") + } + + if masterList.Count() != 1 { + t.Errorf("Expected count 1, got %d", masterList.Count()) + } + + // Test retrieval + retrievedRaceType := masterList.GetRaceType(modelID) + if retrievedRaceType != raceTypeID { + t.Errorf("Expected race type %d, got %d", raceTypeID, retrievedRaceType) + } + + // Test category retrieval + retrievedCategory := masterList.GetRaceTypeCategory(modelID) + if retrievedCategory != category { + t.Errorf("Expected category %s, got %s", category, retrievedCategory) + } + + // Test duplicate addition + if masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) { + t.Error("Should not allow duplicate race type without override") + } + + // Test override + newModelName := "Human Female" + if !masterList.AddRaceType(modelID, raceTypeID, category, subcategory, newModelName, true) { + t.Error("Should allow override of existing race type") + } + + retrievedInfo := masterList.GetRaceTypeByModelID(modelID) + if retrievedInfo == nil { + t.Fatal("Should retrieve race type info") + } + + if retrievedInfo.ModelName != newModelName { + t.Errorf("Expected model name %s, got %s", newModelName, retrievedInfo.ModelName) + } +} + +func TestMasterRaceTypeListBaseFunctions(t *testing.T) { + masterList := NewMasterRaceTypeList() + + // Add some test race types + testData := []struct { + modelID int16 + raceTypeID int16 + category string + subcategory string + modelName string + }{ + {100, Sentient, CategorySentient, "Human", "Human Male"}, + {101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior"}, + {102, Natural, CategoryNatural, "Wolf", "Dire Wolf"}, + {103, Dragonkind, CategoryDragonkind, "Dragon", "Red Dragon"}, + } + + for _, data := range testData { + masterList.AddRaceType(data.modelID, data.raceTypeID, data.category, data.subcategory, data.modelName, false) + } + + // Test base type functions + if masterList.GetRaceBaseType(100) != Sentient { + t.Error("Human should be sentient") + } + + if masterList.GetRaceBaseType(101) != Undead { + t.Error("Skeleton should be undead") + } + + if masterList.GetRaceBaseType(102) != Natural { + t.Error("Wolf should be natural") + } + + if masterList.GetRaceBaseType(103) != Dragonkind { + t.Error("Dragon should be dragonkind") + } + + // Test category functions + sentientTypes := masterList.GetRaceTypesByCategory(CategorySentient) + if len(sentientTypes) != 1 { + t.Errorf("Expected 1 sentient type, got %d", len(sentientTypes)) + } + + undeadTypes := masterList.GetRaceTypesByCategory(CategoryUndead) + if len(undeadTypes) != 1 { + t.Errorf("Expected 1 undead type, got %d", len(undeadTypes)) + } + + // Test subcategory functions + humanTypes := masterList.GetRaceTypesBySubcategory("Human") + if len(humanTypes) != 1 { + t.Errorf("Expected 1 human type, got %d", len(humanTypes)) + } + + // Test statistics + stats := masterList.GetStatistics() + if stats.TotalRaceTypes != 4 { + t.Errorf("Expected 4 total race types, got %d", stats.TotalRaceTypes) + } +} + +func TestMockDatabase(t *testing.T) { + database := NewMockDatabase() + masterList := NewMasterRaceTypeList() + + // Test table creation + err := database.CreateRaceTypesTable() + if err != nil { + t.Fatalf("Failed to create table: %v", err) + } + + if !database.created { + t.Error("Database should be marked as created") + } + + // Test saving + raceType := &RaceType{ + RaceTypeID: Sentient, + Category: CategorySentient, + Subcategory: "Human", + ModelName: "Human Male", + } + + err = database.SaveRaceType(100, raceType) + if err != nil { + t.Fatalf("Failed to save race type: %v", err) + } + + // Test loading + err = database.LoadRaceTypes(masterList) + if err != nil { + t.Fatalf("Failed to load race types: %v", err) + } + + if masterList.Count() != 1 { + t.Errorf("Expected 1 race type loaded, got %d", masterList.Count()) + } + + // Test deletion + err = database.DeleteRaceType(100) + if err != nil { + t.Fatalf("Failed to delete race type: %v", err) + } + + // Test deletion of non-existent + err = database.DeleteRaceType(999) + if err == nil { + t.Error("Should fail to delete non-existent race type") + } +} + +func TestManager(t *testing.T) { + database := NewMockDatabase() + logger := NewMockLogger() + manager := NewManager(database, logger) + + // Test initialization + err := manager.Initialize() + if err != nil { + t.Fatalf("Failed to initialize manager: %v", err) + } + + if !database.created { + t.Error("Database table should be created during initialization") + } + + // Test adding race type + err = manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") + if err != nil { + t.Fatalf("Failed to add race type: %v", err) + } + + // Test retrieval + raceTypeID := manager.GetRaceType(100) + if raceTypeID != Sentient { + t.Errorf("Expected race type %d, got %d", Sentient, raceTypeID) + } + + info := manager.GetRaceTypeInfo(100) + if info == nil { + t.Fatal("Should retrieve race type info") + } + + if info.ModelName != "Human Male" { + t.Errorf("Expected model name 'Human Male', got %s", info.ModelName) + } + + // Test updating + err = manager.UpdateRaceType(100, Sentient, CategorySentient, "Human", "Human Female") + if err != nil { + t.Fatalf("Failed to update race type: %v", err) + } + + updatedInfo := manager.GetRaceTypeInfo(100) + if updatedInfo.ModelName != "Human Female" { + t.Errorf("Expected updated model name 'Human Female', got %s", updatedInfo.ModelName) + } + + // Test type checking functions + if !manager.IsSentient(100) { + t.Error("Model 100 should be sentient") + } + + if manager.IsUndead(100) { + t.Error("Model 100 should not be undead") + } + + // Test removal + err = manager.RemoveRaceType(100) + if err != nil { + t.Fatalf("Failed to remove race type: %v", err) + } + + // Test removal of non-existent + err = manager.RemoveRaceType(999) + if err == nil { + t.Error("Should fail to remove non-existent race type") + } +} + +func TestNPCRaceTypeAdapter(t *testing.T) { + database := NewMockDatabase() + logger := NewMockLogger() + manager := NewManager(database, logger) + + // Initialize and add test data + manager.Initialize() + manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") + manager.AddRaceType(101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior") + + // Create mock entity + entity := NewMockEntity(100) + adapter := NewNPCRaceTypeAdapter(entity, manager) + + // Test race type functions + if adapter.GetRaceType() != Sentient { + t.Errorf("Expected race type %d, got %d", Sentient, adapter.GetRaceType()) + } + + if adapter.GetRaceBaseType() != Sentient { + t.Errorf("Expected base type %d, got %d", Sentient, adapter.GetRaceBaseType()) + } + + if adapter.GetRaceTypeCategory() != CategorySentient { + t.Errorf("Expected category %s, got %s", CategorySentient, adapter.GetRaceTypeCategory()) + } + + // Test type checking + if !adapter.IsSentient() { + t.Error("Human should be sentient") + } + + if adapter.IsUndead() { + t.Error("Human should not be undead") + } + + // Test with undead entity + entity.SetModelType(101) + if !adapter.IsUndead() { + t.Error("Skeleton should be undead") + } + + if adapter.IsSentient() { + t.Error("Skeleton should not be sentient") + } +} + +func TestRaceTypeConstants(t *testing.T) { + // Test that constants are defined correctly + if Sentient == 0 { + t.Error("Sentient should not be 0") + } + + if Natural == 0 { + t.Error("Natural should not be 0") + } + + if Undead == 0 { + t.Error("Undead should not be 0") + } + + // Test category constants + if CategorySentient == "" { + t.Error("CategorySentient should not be empty") + } + + if CategoryNatural == "" { + t.Error("CategoryNatural should not be empty") + } + + if CategoryUndead == "" { + t.Error("CategoryUndead should not be empty") + } +} + +func TestManagerCommands(t *testing.T) { + database := NewMockDatabase() + logger := NewMockLogger() + manager := NewManager(database, logger) + + // Initialize and add test data + manager.Initialize() + manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") + manager.AddRaceType(101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior") + + // Test stats command + result := manager.ProcessCommand([]string{"stats"}) + if result == "" { + t.Error("Stats command should return non-empty result") + } + + // Test list command + result = manager.ProcessCommand([]string{"list", CategorySentient}) + if result == "" { + t.Error("List command should return non-empty result") + } + + // Test info command + result = manager.ProcessCommand([]string{"info", "100"}) + if result == "" { + t.Error("Info command should return non-empty result") + } + + // Test category command + result = manager.ProcessCommand([]string{"category"}) + if result == "" { + t.Error("Category command should return non-empty result") + } + + // Test invalid command + result = manager.ProcessCommand([]string{"invalid"}) + if result == "" { + t.Error("Invalid command should return error message") + } +} + +// Benchmark tests + +func BenchmarkMasterRaceTypeListLookup(b *testing.B) { + masterList := NewMasterRaceTypeList() + + // Add many race types + for i := 0; i < 1000; i++ { + masterList.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i), false) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + masterList.GetRaceType(int16(i % 1000)) + } +} + +func BenchmarkManagerOperations(b *testing.B) { + database := NewMockDatabase() + logger := NewMockLogger() + manager := NewManager(database, logger) + manager.Initialize() + + // Add some test data + for i := 0; i < 100; i++ { + manager.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i)) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + manager.GetRaceType(int16(i % 100)) + } +} + +func BenchmarkNPCRaceTypeAdapter(b *testing.B) { + database := NewMockDatabase() + logger := NewMockLogger() + manager := NewManager(database, logger) + manager.Initialize() + + // Add test data + for i := 0; i < 50; i++ { + manager.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i)) + } + + entity := NewMockEntity(25) + adapter := NewNPCRaceTypeAdapter(entity, manager) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + entity.SetModelType(int16(i % 50)) + adapter.GetRaceType() + adapter.IsSentient() + } +} \ No newline at end of file diff --git a/internal/npc/types.go b/internal/npc/types.go index ed50df0..075581e 100644 --- a/internal/npc/types.go +++ b/internal/npc/types.go @@ -4,9 +4,7 @@ import ( "sync" "time" - "eq2emu/internal/common" "eq2emu/internal/entity" - "eq2emu/internal/spawn" ) // NPCSpell represents a spell configuration for NPCs