implement tests and fixes for entity package
This commit is contained in:
parent
f9fdef9466
commit
0388396b0d
506
internal/entity/benchmark_test.go
Normal file
506
internal/entity/benchmark_test.go
Normal file
@ -0,0 +1,506 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BenchmarkEntityCreation measures entity creation performance
|
||||
func BenchmarkEntityCreation(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
entity := NewEntity()
|
||||
_ = entity
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkEntityCombatState measures combat state operations
|
||||
func BenchmarkEntityCombatState(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
|
||||
b.Run("Sequential", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
entity.SetInCombat(true)
|
||||
_ = entity.IsInCombat()
|
||||
entity.SetInCombat(false)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Parallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
entity.SetInCombat(true)
|
||||
_ = entity.IsInCombat()
|
||||
entity.SetInCombat(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkEntityCastingState measures casting state operations
|
||||
func BenchmarkEntityCastingState(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
|
||||
b.Run("Sequential", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
entity.SetCasting(true)
|
||||
_ = entity.IsCasting()
|
||||
entity.SetCasting(false)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Parallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
entity.SetCasting(true)
|
||||
_ = entity.IsCasting()
|
||||
entity.SetCasting(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkEntityStatCalculations measures stat calculation performance
|
||||
func BenchmarkEntityStatCalculations(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Set up some base stats
|
||||
info.SetStr(100.0)
|
||||
info.SetSta(100.0)
|
||||
info.SetAgi(100.0)
|
||||
info.SetWis(100.0)
|
||||
info.SetIntel(100.0)
|
||||
|
||||
b.Run("GetStats", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = entity.GetStr()
|
||||
_ = entity.GetSta()
|
||||
_ = entity.GetAgi()
|
||||
_ = entity.GetWis()
|
||||
_ = entity.GetIntel()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("GetStatsParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_ = entity.GetStr()
|
||||
_ = entity.GetSta()
|
||||
_ = entity.GetAgi()
|
||||
_ = entity.GetWis()
|
||||
_ = entity.GetIntel()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("GetPrimaryStat", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = entity.GetPrimaryStat()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CalculateBonuses", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
entity.CalculateBonuses()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkEntitySpellEffects measures spell effect operations
|
||||
func BenchmarkEntitySpellEffects(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
|
||||
b.Run("AddRemoveSpellEffect", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
spellID := int32(i + 1000)
|
||||
entity.AddSpellEffect(spellID, 123, 30.0)
|
||||
entity.RemoveSpellEffect(spellID)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("AddRemoveSpellEffectParallel", func(b *testing.B) {
|
||||
var counter int64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
spellID := int32(counter + 1000)
|
||||
counter++
|
||||
entity.AddSpellEffect(spellID, 123, 30.0)
|
||||
entity.RemoveSpellEffect(spellID)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkEntityMaintainedSpells measures maintained spell operations
|
||||
func BenchmarkEntityMaintainedSpells(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetMaxConcentration(1000) // Large pool for benchmarking
|
||||
|
||||
b.Run("AddRemoveMaintainedSpell", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
spellID := int32(i + 2000)
|
||||
if entity.AddMaintainedSpell("Benchmark Spell", spellID, 60.0, 1) {
|
||||
entity.RemoveMaintainedSpell(spellID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("AddRemoveMaintainedSpellParallel", func(b *testing.B) {
|
||||
var counter int64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
spellID := int32(counter + 2000)
|
||||
counter++
|
||||
if entity.AddMaintainedSpell("Benchmark Spell", spellID, 60.0, 1) {
|
||||
entity.RemoveMaintainedSpell(spellID)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkInfoStructBasicOps measures basic InfoStruct operations
|
||||
func BenchmarkInfoStructBasicOps(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
b.Run("SetGetName", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
info.SetName("BenchmarkCharacter")
|
||||
_ = info.GetName()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetGetNameParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
info.SetName("BenchmarkCharacter")
|
||||
_ = info.GetName()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("SetGetLevel", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
info.SetLevel(int16(i % 100))
|
||||
_ = info.GetLevel()
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetGetStats", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
val := float32(i % 1000)
|
||||
info.SetStr(val)
|
||||
info.SetSta(val)
|
||||
info.SetAgi(val)
|
||||
info.SetWis(val)
|
||||
info.SetIntel(val)
|
||||
|
||||
_ = info.GetStr()
|
||||
_ = info.GetSta()
|
||||
_ = info.GetAgi()
|
||||
_ = info.GetWis()
|
||||
_ = info.GetIntel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkInfoStructConcentration measures concentration operations
|
||||
func BenchmarkInfoStructConcentration(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
info.SetMaxConcentration(1000)
|
||||
|
||||
b.Run("AddRemoveConcentration", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
amount := int16(i%10 + 1)
|
||||
if info.AddConcentration(amount) {
|
||||
info.RemoveConcentration(amount)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("AddRemoveConcentrationParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
amount := int16(1) // Use small amount to reduce contention
|
||||
if info.AddConcentration(amount) {
|
||||
info.RemoveConcentration(amount)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkInfoStructCoins measures coin operations
|
||||
func BenchmarkInfoStructCoins(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
b.Run("AddRemoveCoins", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
amount := int32(i%10000 + 1)
|
||||
info.AddCoins(amount)
|
||||
info.RemoveCoins(amount / 2)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("AddRemoveCoinsParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
amount := int32(100)
|
||||
info.AddCoins(amount)
|
||||
info.RemoveCoins(amount / 2)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("GetCoins", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = info.GetCoins()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkInfoStructResistances measures resistance operations
|
||||
func BenchmarkInfoStructResistances(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
resistTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"}
|
||||
|
||||
b.Run("SetGetResistances", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
resistType := resistTypes[i%len(resistTypes)]
|
||||
value := int16(i % 100)
|
||||
info.SetResistance(resistType, value)
|
||||
_ = info.GetResistance(resistType)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetGetResistancesParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
resistType := resistTypes[rand.Intn(len(resistTypes))]
|
||||
value := int16(rand.Intn(100))
|
||||
info.SetResistance(resistType, value)
|
||||
_ = info.GetResistance(resistType)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkInfoStructClone measures clone operations
|
||||
func BenchmarkInfoStructClone(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Set up some state to clone
|
||||
info.SetName("Original Character")
|
||||
info.SetLevel(50)
|
||||
info.SetStr(100.0)
|
||||
info.SetSta(120.0)
|
||||
info.SetAgi(90.0)
|
||||
info.SetWis(110.0)
|
||||
info.SetIntel(105.0)
|
||||
info.AddConcentration(5)
|
||||
info.AddCoins(50000)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
clone := info.Clone()
|
||||
_ = clone
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkEntityPetManagement measures pet management operations
|
||||
func BenchmarkEntityPetManagement(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
|
||||
b.Run("SetGetPet", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
pet := NewEntity()
|
||||
entity.SetPet(pet)
|
||||
_ = entity.GetPet()
|
||||
entity.SetPet(nil)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("SetGetPetParallel", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
pet := NewEntity()
|
||||
entity.SetPet(pet)
|
||||
_ = entity.GetPet()
|
||||
entity.SetPet(nil)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("AllPetTypes", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
pet := NewEntity()
|
||||
|
||||
entity.SetPet(pet)
|
||||
_ = entity.GetPet()
|
||||
|
||||
entity.SetCharmedPet(pet)
|
||||
_ = entity.GetCharmedPet()
|
||||
|
||||
entity.SetDeityPet(pet)
|
||||
_ = entity.GetDeityPet()
|
||||
|
||||
entity.SetCosmeticPet(pet)
|
||||
_ = entity.GetCosmeticPet()
|
||||
|
||||
// Clear all pets
|
||||
entity.SetPet(nil)
|
||||
entity.SetCharmedPet(nil)
|
||||
entity.SetDeityPet(nil)
|
||||
entity.SetCosmeticPet(nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentWorkload simulates a realistic concurrent workload
|
||||
func BenchmarkConcurrentWorkload(b *testing.B) {
|
||||
numEntities := 100
|
||||
entities := make([]*Entity, numEntities)
|
||||
|
||||
// Create entities
|
||||
for i := 0; i < numEntities; i++ {
|
||||
entities[i] = NewEntity()
|
||||
info := entities[i].GetInfoStruct()
|
||||
info.SetMaxConcentration(50)
|
||||
info.SetName("Entity" + string(rune('A'+i%26)))
|
||||
info.SetLevel(int16(i%100 + 1))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.Run("MixedOperations", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
entityIdx := rand.Intn(numEntities)
|
||||
entity := entities[entityIdx]
|
||||
|
||||
switch rand.Intn(10) {
|
||||
case 0, 1: // Combat state changes (20%)
|
||||
entity.SetInCombat(rand.Intn(2) == 1)
|
||||
_ = entity.IsInCombat()
|
||||
case 2, 3: // Stat reads (20%)
|
||||
_ = entity.GetStr()
|
||||
_ = entity.GetSta()
|
||||
_ = entity.GetPrimaryStat()
|
||||
case 4: // Spell effects (10%)
|
||||
spellID := int32(rand.Intn(1000) + 10000)
|
||||
entity.AddSpellEffect(spellID, int32(entityIdx), 30.0)
|
||||
entity.RemoveSpellEffect(spellID)
|
||||
case 5: // Maintained spells (10%)
|
||||
spellID := int32(rand.Intn(100) + 20000)
|
||||
if entity.AddMaintainedSpell("Workload Spell", spellID, 60.0, 1) {
|
||||
entity.RemoveMaintainedSpell(spellID)
|
||||
}
|
||||
case 6, 7: // InfoStruct operations (20%)
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetStr(float32(rand.Intn(200) + 50))
|
||||
_ = info.GetStr()
|
||||
case 8: // Coin operations (10%)
|
||||
info := entity.GetInfoStruct()
|
||||
info.AddCoins(int32(rand.Intn(1000)))
|
||||
_ = info.GetCoins()
|
||||
case 9: // Resistance operations (10%)
|
||||
info := entity.GetInfoStruct()
|
||||
resistTypes := []string{"heat", "cold", "magic", "mental"}
|
||||
resistType := resistTypes[rand.Intn(len(resistTypes))]
|
||||
info.SetResistance(resistType, int16(rand.Intn(100)))
|
||||
_ = info.GetResistance(resistType)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkMemoryAllocation measures memory allocation patterns
|
||||
func BenchmarkMemoryAllocation(b *testing.B) {
|
||||
b.Run("EntityAllocation", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
entity := NewEntity()
|
||||
_ = entity
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("InfoStructAllocation", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
info := NewInfoStruct()
|
||||
_ = info
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("CloneAllocation", func(b *testing.B) {
|
||||
info := NewInfoStruct()
|
||||
info.SetName("Test")
|
||||
info.SetLevel(50)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
clone := info.Clone()
|
||||
_ = clone
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkContention measures performance under high contention
|
||||
func BenchmarkContention(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetMaxConcentration(10) // Low limit to create contention
|
||||
|
||||
b.Run("HighContentionConcentration", func(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if info.AddConcentration(1) {
|
||||
// Hold for a brief moment to increase contention
|
||||
time.Sleep(time.Nanosecond)
|
||||
info.RemoveConcentration(1)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
b.Run("HighContentionSpellEffects", func(b *testing.B) {
|
||||
var spellCounter int64
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
spellID := int32(spellCounter % 100) // Reuse spell IDs to create contention
|
||||
spellCounter++
|
||||
entity.AddSpellEffect(spellID, 123, 30.0)
|
||||
entity.RemoveSpellEffect(spellID)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkScalability tests performance as load increases
|
||||
func BenchmarkScalability(b *testing.B) {
|
||||
goroutineCounts := []int{1, 2, 4, 8, 16, 32, 64}
|
||||
|
||||
for _, numGoroutines := range goroutineCounts {
|
||||
b.Run(fmt.Sprintf("Goroutines_%d", numGoroutines), func(b *testing.B) {
|
||||
entity := NewEntity()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
entity.SetInCombat(true)
|
||||
_ = entity.IsInCombat()
|
||||
entity.SetInCombat(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
445
internal/entity/concurrency_test.go
Normal file
445
internal/entity/concurrency_test.go
Normal file
@ -0,0 +1,445 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestEntityConcurrencyStress performs intensive concurrent operations to identify race conditions
|
||||
func TestEntityConcurrencyStress(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetMaxConcentration(1000) // Large concentration pool
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 100
|
||||
operationsPerGoroutine := 100
|
||||
|
||||
// Test 1: Concurrent combat and casting state changes
|
||||
t.Run("CombatCastingStates", func(t *testing.T) {
|
||||
var combatOps, castingOps int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
// Combat state operations
|
||||
entity.SetInCombat(true)
|
||||
if entity.IsInCombat() {
|
||||
atomic.AddInt64(&combatOps, 1)
|
||||
}
|
||||
entity.SetInCombat(false)
|
||||
|
||||
// Casting state operations
|
||||
entity.SetCasting(true)
|
||||
if entity.IsCasting() {
|
||||
atomic.AddInt64(&castingOps, 1)
|
||||
}
|
||||
entity.SetCasting(false)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Combat operations: %d, Casting operations: %d", combatOps, castingOps)
|
||||
})
|
||||
|
||||
// Test 2: Concurrent spell effect operations
|
||||
t.Run("SpellEffects", func(t *testing.T) {
|
||||
var addOps, removeOps int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
spellID := int32(goroutineID*1000 + j)
|
||||
|
||||
if entity.AddSpellEffect(spellID, int32(goroutineID), 30.0) {
|
||||
atomic.AddInt64(&addOps, 1)
|
||||
}
|
||||
|
||||
if entity.RemoveSpellEffect(spellID) {
|
||||
atomic.AddInt64(&removeOps, 1)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Spell effect adds: %d, removes: %d", addOps, removeOps)
|
||||
})
|
||||
|
||||
// Test 3: Concurrent maintained spell operations with concentration management
|
||||
t.Run("MaintainedSpells", func(t *testing.T) {
|
||||
var addOps, removeOps, concentrationFailures int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer ops due to concentration limits
|
||||
spellID := int32(goroutineID*100 + j + 10000)
|
||||
|
||||
if entity.AddMaintainedSpell("Stress Test Spell", spellID, 60.0, 1) {
|
||||
atomic.AddInt64(&addOps, 1)
|
||||
|
||||
// Small delay to increase contention
|
||||
time.Sleep(time.Microsecond)
|
||||
|
||||
if entity.RemoveMaintainedSpell(spellID) {
|
||||
atomic.AddInt64(&removeOps, 1)
|
||||
}
|
||||
} else {
|
||||
atomic.AddInt64(&concentrationFailures, 1)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Maintained spell adds: %d, removes: %d, concentration failures: %d",
|
||||
addOps, removeOps, concentrationFailures)
|
||||
|
||||
// Verify concentration was properly managed
|
||||
currentConc := info.GetCurConcentration()
|
||||
if currentConc != 0 {
|
||||
t.Errorf("Expected concentration to be 0 after all operations, got %d", currentConc)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Concurrent stat calculations with bonuses
|
||||
t.Run("StatCalculations", func(t *testing.T) {
|
||||
var statReads int64
|
||||
|
||||
// Set some base stats
|
||||
info.SetStr(100.0)
|
||||
info.SetSta(100.0)
|
||||
info.SetAgi(100.0)
|
||||
info.SetWis(100.0)
|
||||
info.SetIntel(100.0)
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
// Add some stat bonuses
|
||||
entity.AddStatBonus(int32(goroutineID*1000+j), 1, float32(j%10))
|
||||
|
||||
// Read stats (these involve bonus calculations)
|
||||
_ = entity.GetStr()
|
||||
_ = entity.GetSta()
|
||||
_ = entity.GetAgi()
|
||||
_ = entity.GetWis()
|
||||
_ = entity.GetIntel()
|
||||
_ = entity.GetPrimaryStat()
|
||||
|
||||
atomic.AddInt64(&statReads, 6)
|
||||
|
||||
// Trigger bonus recalculation
|
||||
entity.CalculateBonuses()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Stat reads: %d", statReads)
|
||||
})
|
||||
|
||||
// Test 5: Concurrent pet management
|
||||
t.Run("PetManagement", func(t *testing.T) {
|
||||
var petOps int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine/10; j++ {
|
||||
pet := NewEntity()
|
||||
|
||||
// Test different pet types
|
||||
switch j % 4 {
|
||||
case 0:
|
||||
entity.SetPet(pet)
|
||||
_ = entity.GetPet()
|
||||
entity.SetPet(nil)
|
||||
case 1:
|
||||
entity.SetCharmedPet(pet)
|
||||
_ = entity.GetCharmedPet()
|
||||
entity.SetCharmedPet(nil)
|
||||
case 2:
|
||||
entity.SetDeityPet(pet)
|
||||
_ = entity.GetDeityPet()
|
||||
entity.SetDeityPet(nil)
|
||||
case 3:
|
||||
entity.SetCosmeticPet(pet)
|
||||
_ = entity.GetCosmeticPet()
|
||||
entity.SetCosmeticPet(nil)
|
||||
}
|
||||
|
||||
atomic.AddInt64(&petOps, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Pet operations: %d", petOps)
|
||||
})
|
||||
}
|
||||
|
||||
// TestInfoStructConcurrencyStress performs intensive concurrent operations on InfoStruct
|
||||
func TestInfoStructConcurrencyStress(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
info.SetMaxConcentration(1000)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 100
|
||||
operationsPerGoroutine := 100
|
||||
|
||||
// Test 1: Concurrent basic property access
|
||||
t.Run("BasicProperties", func(t *testing.T) {
|
||||
var nameOps, levelOps, statOps int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
// Name operations
|
||||
info.SetName("TestChar" + string(rune('A'+goroutineID%26)))
|
||||
_ = info.GetName()
|
||||
atomic.AddInt64(&nameOps, 1)
|
||||
|
||||
// Level operations
|
||||
info.SetLevel(int16(j % 100))
|
||||
_ = info.GetLevel()
|
||||
atomic.AddInt64(&levelOps, 1)
|
||||
|
||||
// Stat operations
|
||||
info.SetStr(float32(j))
|
||||
_ = info.GetStr()
|
||||
atomic.AddInt64(&statOps, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Name ops: %d, Level ops: %d, Stat ops: %d", nameOps, levelOps, statOps)
|
||||
})
|
||||
|
||||
// Test 2: Concurrent concentration management
|
||||
t.Run("ConcentrationManagement", func(t *testing.T) {
|
||||
var addSuccesses, addFailures, removes int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
amount := int16(j%5 + 1) // 1-5 concentration points
|
||||
|
||||
if info.AddConcentration(amount) {
|
||||
atomic.AddInt64(&addSuccesses, 1)
|
||||
|
||||
// Small delay to increase contention
|
||||
time.Sleep(time.Microsecond)
|
||||
|
||||
info.RemoveConcentration(amount)
|
||||
atomic.AddInt64(&removes, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&addFailures, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Concentration adds: %d, failures: %d, removes: %d",
|
||||
addSuccesses, addFailures, removes)
|
||||
|
||||
// Verify final state
|
||||
finalConc := info.GetCurConcentration()
|
||||
if finalConc < 0 || finalConc > info.GetMaxConcentration() {
|
||||
t.Errorf("Invalid final concentration: %d (max: %d)", finalConc, info.GetMaxConcentration())
|
||||
}
|
||||
})
|
||||
|
||||
// Test 3: Concurrent coin operations
|
||||
t.Run("CoinOperations", func(t *testing.T) {
|
||||
var addOps, removeSuccesses, removeFailures int64
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
amount := int32((goroutineID*1000 + j) % 10000)
|
||||
|
||||
// Add coins
|
||||
info.AddCoins(amount)
|
||||
atomic.AddInt64(&addOps, 1)
|
||||
|
||||
// Try to remove some coins
|
||||
removeAmount := amount / 2
|
||||
if info.RemoveCoins(removeAmount) {
|
||||
atomic.AddInt64(&removeSuccesses, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&removeFailures, 1)
|
||||
}
|
||||
|
||||
// Read total coins
|
||||
_ = info.GetCoins()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Coin adds: %d, remove successes: %d, failures: %d",
|
||||
addOps, removeSuccesses, removeFailures)
|
||||
|
||||
// Verify coins are non-negative
|
||||
finalCoins := info.GetCoins()
|
||||
if finalCoins < 0 {
|
||||
t.Errorf("Coins became negative: %d", finalCoins)
|
||||
}
|
||||
})
|
||||
|
||||
// Test 4: Concurrent resistance operations
|
||||
t.Run("ResistanceOperations", func(t *testing.T) {
|
||||
var resistOps int64
|
||||
resistTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"}
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(goroutineID int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine; j++ {
|
||||
resistType := resistTypes[j%len(resistTypes)]
|
||||
value := int16(j % 100)
|
||||
|
||||
info.SetResistance(resistType, value)
|
||||
_ = info.GetResistance(resistType)
|
||||
atomic.AddInt64(&resistOps, 1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Resistance operations: %d", resistOps)
|
||||
})
|
||||
|
||||
// Test 5: Concurrent clone operations
|
||||
t.Run("CloneOperations", func(t *testing.T) {
|
||||
var cloneOps int64
|
||||
|
||||
// Set some initial state
|
||||
info.SetName("Original")
|
||||
info.SetLevel(50)
|
||||
info.SetStr(100.0)
|
||||
info.AddConcentration(5)
|
||||
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer clones as they're expensive
|
||||
clone := info.Clone()
|
||||
if clone != nil {
|
||||
// Verify clone independence
|
||||
clone.SetName("Clone")
|
||||
if info.GetName() == "Clone" {
|
||||
t.Errorf("Clone modified original")
|
||||
}
|
||||
atomic.AddInt64(&cloneOps, 1)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("Clone operations: %d", cloneOps)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRaceConditionDetection uses the race detector to find potential issues
|
||||
func TestRaceConditionDetection(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping race condition test in short mode")
|
||||
}
|
||||
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetMaxConcentration(100)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 50
|
||||
|
||||
// Create a scenario designed to trigger race conditions
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < 50; j++ {
|
||||
// Mix of read and write operations that could race
|
||||
entity.SetInCombat(id%2 == 0)
|
||||
isInCombat := entity.IsInCombat()
|
||||
|
||||
entity.SetCasting(j%2 == 0)
|
||||
isCasting := entity.IsCasting()
|
||||
|
||||
// Stats with bonus calculations
|
||||
info.SetStr(float32(id + j))
|
||||
str := entity.GetStr()
|
||||
|
||||
// Concentration with potential for contention
|
||||
if info.AddConcentration(1) {
|
||||
info.RemoveConcentration(1)
|
||||
}
|
||||
|
||||
// Use the values to prevent optimization
|
||||
_ = isInCombat
|
||||
_ = isCasting
|
||||
_ = str
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Cleanup test to run after stress tests
|
||||
func TestConcurrencyCleanup(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Verify entity is in clean state after stress tests
|
||||
if entity.IsInCombat() {
|
||||
t.Error("Entity should not be in combat after tests")
|
||||
}
|
||||
|
||||
if entity.IsCasting() {
|
||||
t.Error("Entity should not be casting after tests")
|
||||
}
|
||||
|
||||
if info.GetCurConcentration() < 0 {
|
||||
t.Error("Concentration should not be negative")
|
||||
}
|
||||
|
||||
if info.GetCoins() < 0 {
|
||||
t.Error("Coins should not be negative")
|
||||
}
|
||||
}
|
@ -88,6 +88,7 @@ type Entity struct {
|
||||
detrimentalMutex sync.RWMutex
|
||||
commandMutex sync.Mutex
|
||||
bonusCalculationMutex sync.RWMutex
|
||||
petMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewEntity creates a new Entity with default values
|
||||
@ -253,35 +254,35 @@ func (e *Entity) HasMoved() bool {
|
||||
// GetStr returns the effective strength stat
|
||||
func (e *Entity) GetStr() int16 {
|
||||
base := int16(e.infoStruct.GetStr())
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(1, 0, e.infoStruct.GetRace(), 0)) // Stat type 1 = STR
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(1, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 1 = STR
|
||||
return base + bonus
|
||||
}
|
||||
|
||||
// GetSta returns the effective stamina stat
|
||||
func (e *Entity) GetSta() int16 {
|
||||
base := int16(e.infoStruct.GetSta())
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(2, 0, e.infoStruct.GetRace(), 0)) // Stat type 2 = STA
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(2, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 2 = STA
|
||||
return base + bonus
|
||||
}
|
||||
|
||||
// GetAgi returns the effective agility stat
|
||||
func (e *Entity) GetAgi() int16 {
|
||||
base := int16(e.infoStruct.GetAgi())
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(3, 0, e.infoStruct.GetRace(), 0)) // Stat type 3 = AGI
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(3, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 3 = AGI
|
||||
return base + bonus
|
||||
}
|
||||
|
||||
// GetWis returns the effective wisdom stat
|
||||
func (e *Entity) GetWis() int16 {
|
||||
base := int16(e.infoStruct.GetWis())
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(4, 0, e.infoStruct.GetRace(), 0)) // Stat type 4 = WIS
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(4, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 4 = WIS
|
||||
return base + bonus
|
||||
}
|
||||
|
||||
// GetIntel returns the effective intelligence stat
|
||||
func (e *Entity) GetIntel() int16 {
|
||||
base := int16(e.infoStruct.GetIntel())
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(5, 0, e.infoStruct.GetRace(), 0)) // Stat type 5 = INT
|
||||
bonus := int16(e.spellEffectManager.GetBonusValue(5, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 5 = INT
|
||||
return base + bonus
|
||||
}
|
||||
|
||||
@ -469,7 +470,7 @@ func (e *Entity) CalculateBonuses() {
|
||||
e.infoStruct.ResetEffects()
|
||||
|
||||
entityClass := int64(1 << e.infoStruct.GetClass1()) // Convert class to bitmask
|
||||
race := e.infoStruct.GetRace()
|
||||
race := int16(e.infoStruct.GetRace())
|
||||
factionID := int32(e.GetFactionID())
|
||||
|
||||
// Apply stat bonuses
|
||||
@ -508,11 +509,15 @@ func (e *Entity) CalculateBonuses() {
|
||||
|
||||
// GetPet returns the summon pet
|
||||
func (e *Entity) GetPet() *Entity {
|
||||
e.petMutex.RLock()
|
||||
defer e.petMutex.RUnlock()
|
||||
return e.pet
|
||||
}
|
||||
|
||||
// SetPet sets the summon pet
|
||||
func (e *Entity) SetPet(pet *Entity) {
|
||||
e.petMutex.Lock()
|
||||
defer e.petMutex.Unlock()
|
||||
e.pet = pet
|
||||
if pet != nil {
|
||||
pet.owner = e.GetID()
|
||||
@ -522,11 +527,15 @@ func (e *Entity) SetPet(pet *Entity) {
|
||||
|
||||
// GetCharmedPet returns the charmed pet
|
||||
func (e *Entity) GetCharmedPet() *Entity {
|
||||
e.petMutex.RLock()
|
||||
defer e.petMutex.RUnlock()
|
||||
return e.charmedPet
|
||||
}
|
||||
|
||||
// SetCharmedPet sets the charmed pet
|
||||
func (e *Entity) SetCharmedPet(pet *Entity) {
|
||||
e.petMutex.Lock()
|
||||
defer e.petMutex.Unlock()
|
||||
e.charmedPet = pet
|
||||
if pet != nil {
|
||||
pet.owner = e.GetID()
|
||||
@ -536,11 +545,15 @@ func (e *Entity) SetCharmedPet(pet *Entity) {
|
||||
|
||||
// GetDeityPet returns the deity pet
|
||||
func (e *Entity) GetDeityPet() *Entity {
|
||||
e.petMutex.RLock()
|
||||
defer e.petMutex.RUnlock()
|
||||
return e.deityPet
|
||||
}
|
||||
|
||||
// SetDeityPet sets the deity pet
|
||||
func (e *Entity) SetDeityPet(pet *Entity) {
|
||||
e.petMutex.Lock()
|
||||
defer e.petMutex.Unlock()
|
||||
e.deityPet = pet
|
||||
if pet != nil {
|
||||
pet.owner = e.GetID()
|
||||
@ -550,11 +563,15 @@ func (e *Entity) SetDeityPet(pet *Entity) {
|
||||
|
||||
// GetCosmeticPet returns the cosmetic pet
|
||||
func (e *Entity) GetCosmeticPet() *Entity {
|
||||
e.petMutex.RLock()
|
||||
defer e.petMutex.RUnlock()
|
||||
return e.cosmeticPet
|
||||
}
|
||||
|
||||
// SetCosmeticPet sets the cosmetic pet
|
||||
func (e *Entity) SetCosmeticPet(pet *Entity) {
|
||||
e.petMutex.Lock()
|
||||
defer e.petMutex.Unlock()
|
||||
e.cosmeticPet = pet
|
||||
if pet != nil {
|
||||
pet.owner = e.GetID()
|
||||
|
@ -1,22 +1,657 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPackageBuild(t *testing.T) {
|
||||
// Basic test to verify the package builds
|
||||
func TestNewEntity(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
if entity == nil {
|
||||
t.Fatal("NewEntity returned nil")
|
||||
}
|
||||
|
||||
if entity.Spawn == nil {
|
||||
t.Error("Expected Spawn to be initialized")
|
||||
}
|
||||
|
||||
func TestEntityStats(t *testing.T) {
|
||||
if entity.infoStruct == nil {
|
||||
t.Error("Expected InfoStruct to be initialized")
|
||||
}
|
||||
|
||||
if entity.spellEffectManager == nil {
|
||||
t.Error("Expected SpellEffectManager to be initialized")
|
||||
}
|
||||
|
||||
// Check default values
|
||||
if entity.GetMaxSpeed() != 6.0 {
|
||||
t.Errorf("Expected max speed 6.0, got %f", entity.GetMaxSpeed())
|
||||
}
|
||||
|
||||
if entity.GetBaseSpeed() != 0.0 {
|
||||
t.Errorf("Expected base speed 0.0, got %f", entity.GetBaseSpeed())
|
||||
}
|
||||
|
||||
if entity.GetSpeedMultiplier() != 1.0 {
|
||||
t.Errorf("Expected speed multiplier 1.0, got %f", entity.GetSpeedMultiplier())
|
||||
}
|
||||
|
||||
// Check initial states
|
||||
if entity.IsInCombat() {
|
||||
t.Error("Expected entity to not be in combat initially")
|
||||
}
|
||||
|
||||
if entity.IsCasting() {
|
||||
t.Error("Expected entity to not be casting initially")
|
||||
}
|
||||
|
||||
if entity.IsPetDismissing() {
|
||||
t.Error("Expected pet to not be dismissing initially")
|
||||
}
|
||||
|
||||
if entity.HasSeeInvisSpell() {
|
||||
t.Error("Expected entity to not have see invisible spell initially")
|
||||
}
|
||||
|
||||
if entity.HasSeeHideSpell() {
|
||||
t.Error("Expected entity to not have see hidden spell initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityIsEntity(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
if !entity.IsEntity() {
|
||||
t.Error("Expected IsEntity to return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityInfoStruct(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
stats := entity.GetInfoStruct()
|
||||
if stats == nil {
|
||||
t.Error("Expected InfoStruct to be initialized")
|
||||
}
|
||||
|
||||
// Test setting a new info struct
|
||||
newInfo := NewInfoStruct()
|
||||
newInfo.SetName("Test Entity")
|
||||
entity.SetInfoStruct(newInfo)
|
||||
|
||||
if entity.GetInfoStruct().GetName() != "Test Entity" {
|
||||
t.Error("Expected info struct to be updated")
|
||||
}
|
||||
|
||||
// Test that nil info struct is ignored
|
||||
entity.SetInfoStruct(nil)
|
||||
if entity.GetInfoStruct().GetName() != "Test Entity" {
|
||||
t.Error("Expected info struct to remain unchanged when setting nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityClient(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Base Entity should return nil for GetClient
|
||||
client := entity.GetClient()
|
||||
if client != nil {
|
||||
t.Error("Expected GetClient to return nil for base Entity")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityCombatState(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Initial state should be false
|
||||
if entity.IsInCombat() {
|
||||
t.Error("Expected entity to not be in combat initially")
|
||||
}
|
||||
|
||||
// Set combat state to true
|
||||
entity.SetInCombat(true)
|
||||
if !entity.IsInCombat() {
|
||||
t.Error("Expected entity to be in combat after setting to true")
|
||||
}
|
||||
|
||||
// Set combat state to false
|
||||
entity.SetInCombat(false)
|
||||
if entity.IsInCombat() {
|
||||
t.Error("Expected entity to not be in combat after setting to false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityCastingState(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Initial state should be false
|
||||
if entity.IsCasting() {
|
||||
t.Error("Expected entity to not be casting initially")
|
||||
}
|
||||
|
||||
// Set casting state to true
|
||||
entity.SetCasting(true)
|
||||
if !entity.IsCasting() {
|
||||
t.Error("Expected entity to be casting after setting to true")
|
||||
}
|
||||
|
||||
// Set casting state to false
|
||||
entity.SetCasting(false)
|
||||
if entity.IsCasting() {
|
||||
t.Error("Expected entity to not be casting after setting to false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitySpeedMethods(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test max speed
|
||||
entity.SetMaxSpeed(10.0)
|
||||
if entity.GetMaxSpeed() != 10.0 {
|
||||
t.Errorf("Expected max speed 10.0, got %f", entity.GetMaxSpeed())
|
||||
}
|
||||
|
||||
// Test base speed
|
||||
entity.SetBaseSpeed(5.0)
|
||||
if entity.GetBaseSpeed() != 5.0 {
|
||||
t.Errorf("Expected base speed 5.0, got %f", entity.GetBaseSpeed())
|
||||
}
|
||||
|
||||
// Test speed multiplier
|
||||
entity.SetSpeedMultiplier(2.0)
|
||||
if entity.GetSpeedMultiplier() != 2.0 {
|
||||
t.Errorf("Expected speed multiplier 2.0, got %f", entity.GetSpeedMultiplier())
|
||||
}
|
||||
|
||||
// Test effective speed calculation with base speed set
|
||||
effectiveSpeed := entity.CalculateEffectiveSpeed()
|
||||
if effectiveSpeed != 10.0 { // 5.0 * 2.0
|
||||
t.Errorf("Expected effective speed 10.0, got %f", effectiveSpeed)
|
||||
}
|
||||
|
||||
// Test effective speed calculation with zero base speed (should use max speed)
|
||||
entity.SetBaseSpeed(0.0)
|
||||
effectiveSpeed = entity.CalculateEffectiveSpeed()
|
||||
if effectiveSpeed != 20.0 { // 10.0 * 2.0
|
||||
t.Errorf("Expected effective speed 20.0, got %f", effectiveSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityPositionTracking(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test setting and getting last position
|
||||
entity.SetLastPosition(100.0, 200.0, 300.0, 1.5)
|
||||
x, y, z, heading := entity.GetLastPosition()
|
||||
|
||||
if x != 100.0 || y != 200.0 || z != 300.0 || heading != 1.5 {
|
||||
t.Errorf("Expected position (100.0, 200.0, 300.0, 1.5), got (%f, %f, %f, %f)", x, y, z, heading)
|
||||
}
|
||||
|
||||
// Test HasMoved with different positions
|
||||
// Note: This would require setting the spawn's actual position, which depends on the spawn implementation
|
||||
// For now, we just test that the method doesn't panic
|
||||
moved := entity.HasMoved()
|
||||
_ = moved // We can't easily test the actual movement detection without setting up spawn positions
|
||||
}
|
||||
|
||||
func TestEntityStatCalculation(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Set base stats
|
||||
info.SetStr(10.0)
|
||||
info.SetSta(12.0)
|
||||
info.SetAgi(8.0)
|
||||
info.SetWis(15.0)
|
||||
info.SetIntel(20.0)
|
||||
|
||||
// Test individual stat getters (these include bonuses from spell effects)
|
||||
str := entity.GetStr()
|
||||
if str < 10 {
|
||||
t.Errorf("Expected strength >= 10, got %d", str)
|
||||
}
|
||||
|
||||
sta := entity.GetSta()
|
||||
if sta < 12 {
|
||||
t.Errorf("Expected stamina >= 12, got %d", sta)
|
||||
}
|
||||
|
||||
agi := entity.GetAgi()
|
||||
if agi < 8 {
|
||||
t.Errorf("Expected agility >= 8, got %d", agi)
|
||||
}
|
||||
|
||||
wis := entity.GetWis()
|
||||
if wis < 15 {
|
||||
t.Errorf("Expected wisdom >= 15, got %d", wis)
|
||||
}
|
||||
|
||||
intel := entity.GetIntel()
|
||||
if intel < 20 {
|
||||
t.Errorf("Expected intelligence >= 20, got %d", intel)
|
||||
}
|
||||
|
||||
// Test primary stat calculation
|
||||
primaryStat := entity.GetPrimaryStat()
|
||||
if primaryStat < 20 {
|
||||
t.Errorf("Expected primary stat >= 20 (intelligence is highest), got %d", primaryStat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityResistances(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Set base resistances
|
||||
info.SetResistance("heat", 10)
|
||||
info.SetResistance("cold", 15)
|
||||
info.SetResistance("magic", 20)
|
||||
info.SetResistance("mental", 25)
|
||||
info.SetResistance("divine", 30)
|
||||
info.SetResistance("disease", 35)
|
||||
info.SetResistance("poison", 40)
|
||||
|
||||
// Test resistance getters
|
||||
if entity.GetHeatResistance() != 10 {
|
||||
t.Errorf("Expected heat resistance 10, got %d", entity.GetHeatResistance())
|
||||
}
|
||||
|
||||
if entity.GetColdResistance() != 15 {
|
||||
t.Errorf("Expected cold resistance 15, got %d", entity.GetColdResistance())
|
||||
}
|
||||
|
||||
if entity.GetMagicResistance() != 20 {
|
||||
t.Errorf("Expected magic resistance 20, got %d", entity.GetMagicResistance())
|
||||
}
|
||||
|
||||
if entity.GetMentalResistance() != 25 {
|
||||
t.Errorf("Expected mental resistance 25, got %d", entity.GetMentalResistance())
|
||||
}
|
||||
|
||||
if entity.GetDivineResistance() != 30 {
|
||||
t.Errorf("Expected divine resistance 30, got %d", entity.GetDivineResistance())
|
||||
}
|
||||
|
||||
if entity.GetDiseaseResistance() != 35 {
|
||||
t.Errorf("Expected disease resistance 35, got %d", entity.GetDiseaseResistance())
|
||||
}
|
||||
|
||||
if entity.GetPoisonResistance() != 40 {
|
||||
t.Errorf("Expected poison resistance 40, got %d", entity.GetPoisonResistance())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityMaintainedSpells(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Set max concentration
|
||||
info.SetMaxConcentration(10)
|
||||
|
||||
// Test adding maintained spell
|
||||
success := entity.AddMaintainedSpell("Test Spell", 123, 60.0, 3)
|
||||
if !success {
|
||||
t.Error("Expected AddMaintainedSpell to succeed")
|
||||
}
|
||||
|
||||
// Check concentration usage
|
||||
if info.GetCurConcentration() != 3 {
|
||||
t.Errorf("Expected current concentration 3, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test getting maintained spell
|
||||
effect := entity.GetMaintainedSpell(123)
|
||||
if effect == nil {
|
||||
t.Error("Expected to find maintained spell effect")
|
||||
}
|
||||
|
||||
// Test adding spell that would exceed concentration
|
||||
success = entity.AddMaintainedSpell("Another Spell", 124, 60.0, 8)
|
||||
if success {
|
||||
t.Error("Expected AddMaintainedSpell to fail when concentration exceeded")
|
||||
}
|
||||
|
||||
// Test removing maintained spell
|
||||
success = entity.RemoveMaintainedSpell(123)
|
||||
if !success {
|
||||
t.Error("Expected RemoveMaintainedSpell to succeed")
|
||||
}
|
||||
|
||||
// Check concentration was returned
|
||||
if info.GetCurConcentration() != 0 {
|
||||
t.Errorf("Expected current concentration 0, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test removing non-existent spell
|
||||
success = entity.RemoveMaintainedSpell(999)
|
||||
if success {
|
||||
t.Error("Expected RemoveMaintainedSpell to fail for non-existent spell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitySpellEffects(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test adding spell effect
|
||||
success := entity.AddSpellEffect(456, 789, 30.0)
|
||||
if !success {
|
||||
t.Error("Expected AddSpellEffect to succeed")
|
||||
}
|
||||
|
||||
// Test removing spell effect
|
||||
success = entity.RemoveSpellEffect(456)
|
||||
if !success {
|
||||
t.Error("Expected RemoveSpellEffect to succeed")
|
||||
}
|
||||
|
||||
// Test removing non-existent spell effect
|
||||
success = entity.RemoveSpellEffect(999)
|
||||
if success {
|
||||
t.Error("Expected RemoveSpellEffect to fail for non-existent spell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityDetrimentalSpells(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test adding detrimental spell
|
||||
entity.AddDetrimentalSpell(789, 456, 45.0, 1)
|
||||
|
||||
// Test getting detrimental effect
|
||||
effect := entity.GetDetrimentalEffect(789, 456)
|
||||
if effect == nil {
|
||||
t.Error("Expected to find detrimental spell effect")
|
||||
}
|
||||
|
||||
// Test removing detrimental spell
|
||||
success := entity.RemoveDetrimentalSpell(789, 456)
|
||||
if !success {
|
||||
t.Error("Expected RemoveDetrimentalSpell to succeed")
|
||||
}
|
||||
|
||||
// Test removing non-existent detrimental spell
|
||||
success = entity.RemoveDetrimentalSpell(999, 888)
|
||||
if success {
|
||||
t.Error("Expected RemoveDetrimentalSpell to fail for non-existent spell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityBonusSystem(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test adding skill bonus
|
||||
entity.AddSkillBonus(123, 5, 15.0) // Skill ID 5, value 15.0
|
||||
|
||||
// Test adding stat bonus
|
||||
entity.AddStatBonus(124, 1, 5.0) // Stat type 1 (STR), value 5.0
|
||||
|
||||
// Test calculating bonuses
|
||||
entity.CalculateBonuses()
|
||||
|
||||
// The actual bonus calculation depends on the spell effect manager implementation
|
||||
// We just test that the method doesn't panic
|
||||
}
|
||||
|
||||
func TestEntityPetManagement(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
pet := NewEntity()
|
||||
|
||||
// Test setting and getting summon pet
|
||||
entity.SetPet(pet)
|
||||
if entity.GetPet() != pet {
|
||||
t.Error("Expected pet to be set correctly")
|
||||
}
|
||||
|
||||
if pet.GetOwner() != entity.GetID() {
|
||||
t.Error("Expected pet owner to be set correctly")
|
||||
}
|
||||
|
||||
if pet.GetPetType() != PetTypeSummon {
|
||||
t.Error("Expected pet type to be summon")
|
||||
}
|
||||
|
||||
// Test charmed pet
|
||||
charmedPet := NewEntity()
|
||||
entity.SetCharmedPet(charmedPet)
|
||||
if entity.GetCharmedPet() != charmedPet {
|
||||
t.Error("Expected charmed pet to be set correctly")
|
||||
}
|
||||
|
||||
if charmedPet.GetPetType() != PetTypeCharm {
|
||||
t.Error("Expected pet type to be charm")
|
||||
}
|
||||
|
||||
// Test deity pet
|
||||
deityPet := NewEntity()
|
||||
entity.SetDeityPet(deityPet)
|
||||
if entity.GetDeityPet() != deityPet {
|
||||
t.Error("Expected deity pet to be set correctly")
|
||||
}
|
||||
|
||||
if deityPet.GetPetType() != PetTypeDeity {
|
||||
t.Error("Expected pet type to be deity")
|
||||
}
|
||||
|
||||
// Test cosmetic pet
|
||||
cosmeticPet := NewEntity()
|
||||
entity.SetCosmeticPet(cosmeticPet)
|
||||
if entity.GetCosmeticPet() != cosmeticPet {
|
||||
t.Error("Expected cosmetic pet to be set correctly")
|
||||
}
|
||||
|
||||
if cosmeticPet.GetPetType() != PetTypeCosmetic {
|
||||
t.Error("Expected pet type to be cosmetic")
|
||||
}
|
||||
|
||||
// Test pet dismissing state
|
||||
entity.SetPetDismissing(true)
|
||||
if !entity.IsPetDismissing() {
|
||||
t.Error("Expected pet to be dismissing")
|
||||
}
|
||||
|
||||
entity.SetPetDismissing(false)
|
||||
if entity.IsPetDismissing() {
|
||||
t.Error("Expected pet to not be dismissing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityDeity(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test setting and getting deity
|
||||
entity.SetDeity(5)
|
||||
if entity.GetDeity() != 5 {
|
||||
t.Errorf("Expected deity 5, got %d", entity.GetDeity())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityDodgeChance(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test base dodge chance
|
||||
dodgeChance := entity.GetDodgeChance()
|
||||
if dodgeChance != 5.0 {
|
||||
t.Errorf("Expected base dodge chance 5.0, got %f", dodgeChance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitySeeSpells(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test see invisible spell
|
||||
entity.SetSeeInvisSpell(true)
|
||||
if !entity.HasSeeInvisSpell() {
|
||||
t.Error("Expected entity to have see invisible spell")
|
||||
}
|
||||
|
||||
entity.SetSeeInvisSpell(false)
|
||||
if entity.HasSeeInvisSpell() {
|
||||
t.Error("Expected entity to not have see invisible spell")
|
||||
}
|
||||
|
||||
// Test see hidden spell
|
||||
entity.SetSeeHideSpell(true)
|
||||
if !entity.HasSeeHideSpell() {
|
||||
t.Error("Expected entity to have see hidden spell")
|
||||
}
|
||||
|
||||
entity.SetSeeHideSpell(false)
|
||||
if entity.HasSeeHideSpell() {
|
||||
t.Error("Expected entity to not have see hidden spell")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityCleanupMethods(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Set up some spell effects and concentration usage
|
||||
info.SetMaxConcentration(10)
|
||||
entity.AddMaintainedSpell("Test Spell 1", 123, 60.0, 3)
|
||||
entity.AddMaintainedSpell("Test Spell 2", 124, 60.0, 2)
|
||||
entity.AddSpellEffect(456, 789, 30.0)
|
||||
|
||||
// Verify concentration is used
|
||||
if info.GetCurConcentration() != 5 {
|
||||
t.Errorf("Expected current concentration 5, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test deleting all spell effects
|
||||
entity.DeleteSpellEffects(false)
|
||||
|
||||
// Verify concentration was returned
|
||||
if info.GetCurConcentration() != 0 {
|
||||
t.Errorf("Expected current concentration 0 after cleanup, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test RemoveSpells
|
||||
entity.AddMaintainedSpell("Test Spell 3", 125, 60.0, 2)
|
||||
entity.RemoveSpells(false) // Remove all spells
|
||||
|
||||
if info.GetCurConcentration() != 0 {
|
||||
t.Errorf("Expected current concentration 0 after RemoveSpells, got %d", info.GetCurConcentration())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityProcessEffects(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test that ProcessEffects doesn't panic
|
||||
entity.ProcessEffects()
|
||||
}
|
||||
|
||||
func TestEntityClassSystemIntegration(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
|
||||
// Test class getter/setter
|
||||
info.SetClass1(5)
|
||||
if entity.GetClass() != 5 {
|
||||
t.Errorf("Expected class 5, got %d", entity.GetClass())
|
||||
}
|
||||
|
||||
entity.SetClass(10)
|
||||
if entity.GetClass() != 10 {
|
||||
t.Errorf("Expected class 10, got %d", entity.GetClass())
|
||||
}
|
||||
|
||||
// Test level getter
|
||||
info.SetLevel(25)
|
||||
if entity.GetLevel() != 25 {
|
||||
t.Errorf("Expected level 25, got %d", entity.GetLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityConcurrency(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
info := entity.GetInfoStruct()
|
||||
info.SetMaxConcentration(20)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 10
|
||||
|
||||
// Test concurrent access to combat state
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
entity.SetInCombat(true)
|
||||
_ = entity.IsInCombat()
|
||||
entity.SetInCombat(false)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Test concurrent access to casting state
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
entity.SetCasting(true)
|
||||
_ = entity.IsCasting()
|
||||
entity.SetCasting(false)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Test concurrent spell effect operations
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
spellID := int32(1000 + i)
|
||||
go func(id int32) {
|
||||
defer wg.Done()
|
||||
entity.AddSpellEffect(id, 123, 30.0)
|
||||
time.Sleep(time.Millisecond) // Small delay to ensure some overlap
|
||||
entity.RemoveSpellEffect(id)
|
||||
}(spellID)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Test concurrent maintained spell operations
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
spellID := int32(2000 + i)
|
||||
go func(id int32) {
|
||||
defer wg.Done()
|
||||
if entity.AddMaintainedSpell("Concurrent Spell", id, 60.0, 1) {
|
||||
time.Sleep(time.Millisecond)
|
||||
entity.RemoveMaintainedSpell(id)
|
||||
}
|
||||
}(spellID)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestEntityControlEffects(t *testing.T) {
|
||||
entity := NewEntity()
|
||||
|
||||
// Test has control effect - should work without panicking
|
||||
// The actual implementation depends on the spell effect manager
|
||||
hasStun := entity.HasControlEffect(ControlEffectStun)
|
||||
_ = hasStun // We can't easily test the actual value without setting up effects
|
||||
}
|
||||
|
||||
func TestEntityConstants(t *testing.T) {
|
||||
// Test pet type constants
|
||||
if PetTypeSummon != 1 {
|
||||
t.Errorf("Expected PetTypeSummon to be 1, got %d", PetTypeSummon)
|
||||
}
|
||||
|
||||
if PetTypeCharm != 2 {
|
||||
t.Errorf("Expected PetTypeCharm to be 2, got %d", PetTypeCharm)
|
||||
}
|
||||
|
||||
if PetTypeDeity != 3 {
|
||||
t.Errorf("Expected PetTypeDeity to be 3, got %d", PetTypeDeity)
|
||||
}
|
||||
|
||||
if PetTypeCosmetic != 4 {
|
||||
t.Errorf("Expected PetTypeCosmetic to be 4, got %d", PetTypeCosmetic)
|
||||
}
|
||||
|
||||
// Test control effect constants (re-exported from spells package)
|
||||
if ControlEffectStun == 0 && ControlEffectRoot == 0 {
|
||||
t.Error("Control effect constants should be non-zero")
|
||||
}
|
||||
}
|
@ -834,6 +834,9 @@ func (info *InfoStruct) Clone() *InfoStruct {
|
||||
clone := &InfoStruct{}
|
||||
*clone = *info // Copy all fields
|
||||
|
||||
// Reset the mutex in the clone to avoid sharing the same mutex
|
||||
clone.mutex = sync.RWMutex{}
|
||||
|
||||
// Copy the account age bonus array
|
||||
copy(clone.accountAgeBonus[:], info.accountAgeBonus[:])
|
||||
|
||||
|
514
internal/entity/info_struct_test.go
Normal file
514
internal/entity/info_struct_test.go
Normal file
@ -0,0 +1,514 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewInfoStruct(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
if info == nil {
|
||||
t.Fatal("NewInfoStruct returned nil")
|
||||
}
|
||||
|
||||
// Test default values
|
||||
if info.GetName() != "" {
|
||||
t.Errorf("Expected empty name, got %s", info.GetName())
|
||||
}
|
||||
|
||||
if info.GetLevel() != 0 {
|
||||
t.Errorf("Expected level 0, got %d", info.GetLevel())
|
||||
}
|
||||
|
||||
if info.GetMaxConcentration() != 5 {
|
||||
t.Errorf("Expected max concentration 5, got %d", info.GetMaxConcentration())
|
||||
}
|
||||
|
||||
if info.GetCurConcentration() != 0 {
|
||||
t.Errorf("Expected current concentration 0, got %d", info.GetCurConcentration())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructBasicProperties(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test name
|
||||
info.SetName("Test Character")
|
||||
if info.GetName() != "Test Character" {
|
||||
t.Errorf("Expected name 'Test Character', got %s", info.GetName())
|
||||
}
|
||||
|
||||
// Test level
|
||||
info.SetLevel(25)
|
||||
if info.GetLevel() != 25 {
|
||||
t.Errorf("Expected level 25, got %d", info.GetLevel())
|
||||
}
|
||||
|
||||
// Test effective level
|
||||
info.SetEffectiveLevel(30)
|
||||
if info.GetEffectiveLevel() != 30 {
|
||||
t.Errorf("Expected effective level 30, got %d", info.GetEffectiveLevel())
|
||||
}
|
||||
|
||||
// Test class
|
||||
info.SetClass1(5)
|
||||
if info.GetClass1() != 5 {
|
||||
t.Errorf("Expected class 5, got %d", info.GetClass1())
|
||||
}
|
||||
|
||||
// Test race
|
||||
info.SetRace(3)
|
||||
if info.GetRace() != 3 {
|
||||
t.Errorf("Expected race 3, got %d", info.GetRace())
|
||||
}
|
||||
|
||||
// Test gender
|
||||
info.SetGender(1)
|
||||
if info.GetGender() != 1 {
|
||||
t.Errorf("Expected gender 1, got %d", info.GetGender())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructStats(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test strength
|
||||
info.SetStr(15.5)
|
||||
if info.GetStr() != 15.5 {
|
||||
t.Errorf("Expected strength 15.5, got %f", info.GetStr())
|
||||
}
|
||||
|
||||
// Test stamina
|
||||
info.SetSta(20.0)
|
||||
if info.GetSta() != 20.0 {
|
||||
t.Errorf("Expected stamina 20.0, got %f", info.GetSta())
|
||||
}
|
||||
|
||||
// Test agility
|
||||
info.SetAgi(12.75)
|
||||
if info.GetAgi() != 12.75 {
|
||||
t.Errorf("Expected agility 12.75, got %f", info.GetAgi())
|
||||
}
|
||||
|
||||
// Test wisdom
|
||||
info.SetWis(18.25)
|
||||
if info.GetWis() != 18.25 {
|
||||
t.Errorf("Expected wisdom 18.25, got %f", info.GetWis())
|
||||
}
|
||||
|
||||
// Test intelligence
|
||||
info.SetIntel(22.5)
|
||||
if info.GetIntel() != 22.5 {
|
||||
t.Errorf("Expected intelligence 22.5, got %f", info.GetIntel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructConcentration(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test setting max concentration
|
||||
info.SetMaxConcentration(15)
|
||||
if info.GetMaxConcentration() != 15 {
|
||||
t.Errorf("Expected max concentration 15, got %d", info.GetMaxConcentration())
|
||||
}
|
||||
|
||||
// Test adding concentration
|
||||
success := info.AddConcentration(5)
|
||||
if !success {
|
||||
t.Error("Expected AddConcentration to succeed")
|
||||
}
|
||||
|
||||
if info.GetCurConcentration() != 5 {
|
||||
t.Errorf("Expected current concentration 5, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test adding concentration that would exceed maximum
|
||||
success = info.AddConcentration(12)
|
||||
if success {
|
||||
t.Error("Expected AddConcentration to fail when exceeding maximum")
|
||||
}
|
||||
|
||||
if info.GetCurConcentration() != 5 {
|
||||
t.Errorf("Expected current concentration to remain 5, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test adding concentration that exactly reaches maximum
|
||||
success = info.AddConcentration(10)
|
||||
if !success {
|
||||
t.Error("Expected AddConcentration to succeed when exactly reaching maximum")
|
||||
}
|
||||
|
||||
if info.GetCurConcentration() != 15 {
|
||||
t.Errorf("Expected current concentration 15, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test removing concentration
|
||||
info.RemoveConcentration(7)
|
||||
if info.GetCurConcentration() != 8 {
|
||||
t.Errorf("Expected current concentration 8, got %d", info.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Test removing more concentration than available
|
||||
info.RemoveConcentration(20)
|
||||
if info.GetCurConcentration() != 0 {
|
||||
t.Errorf("Expected current concentration to be clamped to 0, got %d", info.GetCurConcentration())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructCoins(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test initial coins
|
||||
if info.GetCoins() != 0 {
|
||||
t.Errorf("Expected initial coins 0, got %d", info.GetCoins())
|
||||
}
|
||||
|
||||
// Test adding copper
|
||||
info.AddCoins(150) // 1 silver and 50 copper
|
||||
totalCopper := info.GetCoins()
|
||||
if totalCopper != 150 {
|
||||
t.Errorf("Expected total coins 150, got %d", totalCopper)
|
||||
}
|
||||
|
||||
// Test adding large amount that converts to higher denominations
|
||||
info.AddCoins(1234567) // Should convert to plat, gold, silver, copper
|
||||
expectedTotal := 150 + 1234567
|
||||
totalCopper = info.GetCoins()
|
||||
if totalCopper != int32(expectedTotal) {
|
||||
t.Errorf("Expected total coins %d, got %d", expectedTotal, totalCopper)
|
||||
}
|
||||
|
||||
// Test removing coins
|
||||
success := info.RemoveCoins(100)
|
||||
if !success {
|
||||
t.Error("Expected RemoveCoins to succeed")
|
||||
}
|
||||
|
||||
expectedTotal -= 100
|
||||
totalCopper = info.GetCoins()
|
||||
if totalCopper != int32(expectedTotal) {
|
||||
t.Errorf("Expected total coins %d after removal, got %d", expectedTotal, totalCopper)
|
||||
}
|
||||
|
||||
// Test removing more coins than available
|
||||
success = info.RemoveCoins(9999999)
|
||||
if success {
|
||||
t.Error("Expected RemoveCoins to fail when removing more than available")
|
||||
}
|
||||
|
||||
// Total should remain unchanged
|
||||
totalCopper = info.GetCoins()
|
||||
if totalCopper != int32(expectedTotal) {
|
||||
t.Errorf("Expected total coins to remain %d, got %d", expectedTotal, totalCopper)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructResistances(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test all resistance types
|
||||
resistanceTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"}
|
||||
expectedValues := []int16{10, 15, 20, 25, 30, 35, 40}
|
||||
|
||||
for i, resistType := range resistanceTypes {
|
||||
expectedValue := expectedValues[i]
|
||||
|
||||
// Set resistance
|
||||
info.SetResistance(resistType, expectedValue)
|
||||
|
||||
// Get resistance
|
||||
actualValue := info.GetResistance(resistType)
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("Expected %s resistance %d, got %d", resistType, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid resistance type
|
||||
unknownResist := info.GetResistance("unknown")
|
||||
if unknownResist != 0 {
|
||||
t.Errorf("Expected unknown resistance type to return 0, got %d", unknownResist)
|
||||
}
|
||||
|
||||
// Setting invalid resistance type should not panic
|
||||
info.SetResistance("unknown", 50) // Should be ignored
|
||||
}
|
||||
|
||||
func TestInfoStructResetEffects(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Set some base values first (these would normally be set during character creation)
|
||||
info.str = 10.0
|
||||
info.strBase = 10.0
|
||||
info.sta = 12.0
|
||||
info.staBase = 12.0
|
||||
info.heat = 15
|
||||
info.heatBase = 15
|
||||
|
||||
// Modify current values to simulate bonuses
|
||||
info.SetStr(20.0)
|
||||
info.SetSta(25.0)
|
||||
info.SetResistance("heat", 30)
|
||||
|
||||
// Reset effects
|
||||
info.ResetEffects()
|
||||
|
||||
// Check that values were reset to base
|
||||
if info.GetStr() != 10.0 {
|
||||
t.Errorf("Expected strength to reset to 10.0, got %f", info.GetStr())
|
||||
}
|
||||
|
||||
if info.GetSta() != 12.0 {
|
||||
t.Errorf("Expected stamina to reset to 12.0, got %f", info.GetSta())
|
||||
}
|
||||
|
||||
if info.GetResistance("heat") != 15 {
|
||||
t.Errorf("Expected heat resistance to reset to 15, got %d", info.GetResistance("heat"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructCalculatePrimaryStat(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Set all stats to different values
|
||||
info.SetStr(10.0)
|
||||
info.SetSta(15.0)
|
||||
info.SetAgi(8.0)
|
||||
info.SetWis(20.0) // This should be the highest
|
||||
info.SetIntel(12.0)
|
||||
|
||||
primaryStat := info.CalculatePrimaryStat()
|
||||
if primaryStat != 20.0 {
|
||||
t.Errorf("Expected primary stat 20.0 (wisdom), got %f", primaryStat)
|
||||
}
|
||||
|
||||
// Test with intelligence being highest
|
||||
info.SetIntel(25.0)
|
||||
primaryStat = info.CalculatePrimaryStat()
|
||||
if primaryStat != 25.0 {
|
||||
t.Errorf("Expected primary stat 25.0 (intelligence), got %f", primaryStat)
|
||||
}
|
||||
|
||||
// Test with all stats equal
|
||||
info.SetStr(30.0)
|
||||
info.SetSta(30.0)
|
||||
info.SetAgi(30.0)
|
||||
info.SetWis(30.0)
|
||||
info.SetIntel(30.0)
|
||||
|
||||
primaryStat = info.CalculatePrimaryStat()
|
||||
if primaryStat != 30.0 {
|
||||
t.Errorf("Expected primary stat 30.0 (all equal), got %f", primaryStat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructClone(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Set some values
|
||||
info.SetName("Original Character")
|
||||
info.SetLevel(15)
|
||||
info.SetStr(20.0)
|
||||
info.SetResistance("magic", 25)
|
||||
info.AddConcentration(3)
|
||||
|
||||
// Clone the info struct
|
||||
clone := info.Clone()
|
||||
if clone == nil {
|
||||
t.Fatal("Clone returned nil")
|
||||
}
|
||||
|
||||
// Verify clone has same values
|
||||
if clone.GetName() != "Original Character" {
|
||||
t.Errorf("Expected cloned name 'Original Character', got %s", clone.GetName())
|
||||
}
|
||||
|
||||
if clone.GetLevel() != 15 {
|
||||
t.Errorf("Expected cloned level 15, got %d", clone.GetLevel())
|
||||
}
|
||||
|
||||
if clone.GetStr() != 20.0 {
|
||||
t.Errorf("Expected cloned strength 20.0, got %f", clone.GetStr())
|
||||
}
|
||||
|
||||
if clone.GetResistance("magic") != 25 {
|
||||
t.Errorf("Expected cloned magic resistance 25, got %d", clone.GetResistance("magic"))
|
||||
}
|
||||
|
||||
if clone.GetCurConcentration() != 3 {
|
||||
t.Errorf("Expected cloned concentration 3, got %d", clone.GetCurConcentration())
|
||||
}
|
||||
|
||||
// Verify that modifying the clone doesn't affect the original
|
||||
clone.SetName("Cloned Character")
|
||||
clone.SetLevel(20)
|
||||
|
||||
if info.GetName() != "Original Character" {
|
||||
t.Error("Original name was modified when clone was changed")
|
||||
}
|
||||
|
||||
if info.GetLevel() != 15 {
|
||||
t.Error("Original level was modified when clone was changed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructGetUptime(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test uptime (currently returns 0 as it's not implemented)
|
||||
uptime := info.GetUptime()
|
||||
if uptime != time.Duration(0) {
|
||||
t.Errorf("Expected uptime to be 0 (not implemented), got %v", uptime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructConcurrency(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
info.SetMaxConcentration(100)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numGoroutines := 20
|
||||
|
||||
// Test concurrent access to basic properties
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(index int) {
|
||||
defer wg.Done()
|
||||
|
||||
// Each goroutine sets unique values
|
||||
info.SetName("Character" + string(rune('A'+index)))
|
||||
_ = info.GetName()
|
||||
|
||||
info.SetLevel(int16(10 + index))
|
||||
_ = info.GetLevel()
|
||||
|
||||
info.SetStr(float32(10.0 + float32(index)))
|
||||
_ = info.GetStr()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Test concurrent concentration operations
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Try to add concentration
|
||||
if info.AddConcentration(1) {
|
||||
// If successful, remove it after a short delay
|
||||
time.Sleep(time.Microsecond)
|
||||
info.RemoveConcentration(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// After all operations, concentration should be back to 0
|
||||
// (This might not always be true due to race conditions, but shouldn't crash)
|
||||
_ = info.GetCurConcentration()
|
||||
|
||||
// Test concurrent coin operations
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(amount int32) {
|
||||
defer wg.Done()
|
||||
|
||||
info.AddCoins(amount)
|
||||
_ = info.GetCoins()
|
||||
|
||||
// Try to remove some coins
|
||||
info.RemoveCoins(amount / 2)
|
||||
}(int32(100 + i))
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Test concurrent resistance operations
|
||||
wg.Add(numGoroutines)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(value int16) {
|
||||
defer wg.Done()
|
||||
|
||||
info.SetResistance("heat", value)
|
||||
_ = info.GetResistance("heat")
|
||||
|
||||
info.SetResistance("cold", value+1)
|
||||
_ = info.GetResistance("cold")
|
||||
}(int16(i))
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestInfoStructLargeValues(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test with large coin amounts
|
||||
largeCoinAmount := int32(2000000000) // 2 billion copper
|
||||
info.AddCoins(largeCoinAmount)
|
||||
|
||||
totalCoins := info.GetCoins()
|
||||
if totalCoins != largeCoinAmount {
|
||||
t.Errorf("Expected large coin amount %d, got %d", largeCoinAmount, totalCoins)
|
||||
}
|
||||
|
||||
// Test removing large amounts
|
||||
success := info.RemoveCoins(largeCoinAmount)
|
||||
if !success {
|
||||
t.Error("Expected to be able to remove large coin amount")
|
||||
}
|
||||
|
||||
if info.GetCoins() != 0 {
|
||||
t.Errorf("Expected coins to be 0 after removing all, got %d", info.GetCoins())
|
||||
}
|
||||
|
||||
// Test with maximum values
|
||||
info.SetMaxConcentration(32767) // Max int16
|
||||
info.SetLevel(32767)
|
||||
|
||||
if info.GetMaxConcentration() != 32767 {
|
||||
t.Errorf("Expected max concentration 32767, got %d", info.GetMaxConcentration())
|
||||
}
|
||||
|
||||
if info.GetLevel() != 32767 {
|
||||
t.Errorf("Expected level 32767, got %d", info.GetLevel())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoStructEdgeCases(t *testing.T) {
|
||||
info := NewInfoStruct()
|
||||
|
||||
// Test negative values
|
||||
info.SetStr(-5.0)
|
||||
if info.GetStr() != -5.0 {
|
||||
t.Errorf("Expected negative strength -5.0, got %f", info.GetStr())
|
||||
}
|
||||
|
||||
// Test zero values
|
||||
info.SetMaxConcentration(0)
|
||||
success := info.AddConcentration(1)
|
||||
if success {
|
||||
t.Error("Expected AddConcentration to fail with max concentration 0")
|
||||
}
|
||||
|
||||
// Test very small float values
|
||||
info.SetAgi(0.001)
|
||||
if info.GetAgi() != 0.001 {
|
||||
t.Errorf("Expected small agility 0.001, got %f", info.GetAgi())
|
||||
}
|
||||
|
||||
// Test empty string name
|
||||
info.SetName("")
|
||||
if info.GetName() != "" {
|
||||
t.Errorf("Expected empty name, got '%s'", info.GetName())
|
||||
}
|
||||
|
||||
// Test very long name
|
||||
longName := string(make([]byte, 1000))
|
||||
for i := range longName {
|
||||
longName = longName[:i] + "A" + longName[i+1:]
|
||||
}
|
||||
info.SetName(longName)
|
||||
if info.GetName() != longName {
|
||||
t.Error("Expected to handle very long names")
|
||||
}
|
||||
}
|
@ -252,8 +252,8 @@ func NextID() int32 {
|
||||
for {
|
||||
id := atomic.AddInt32(&nextSpawnID, 1)
|
||||
|
||||
// Handle wraparound
|
||||
if id == 0xFFFFFFFE {
|
||||
// Handle wraparound (using math.MaxInt32 - 1)
|
||||
if id >= 2147483646 {
|
||||
atomic.StoreInt32(&nextSpawnID, 1)
|
||||
continue
|
||||
}
|
||||
@ -623,7 +623,7 @@ func (s *Spawn) SetName(name string) {
|
||||
|
||||
// GetLevel returns the spawn's level
|
||||
func (s *Spawn) GetLevel() int16 {
|
||||
return s.appearance.Level
|
||||
return int16(s.appearance.Level)
|
||||
}
|
||||
|
||||
// SetLevel updates the spawn's level and marks info as changed
|
||||
@ -631,7 +631,7 @@ func (s *Spawn) SetLevel(level int16) {
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
|
||||
s.appearance.Level = level
|
||||
s.appearance.Level = int8(level)
|
||||
s.infoChanged.Store(true)
|
||||
s.changed.Store(true)
|
||||
s.addChangedZoneSpawn()
|
||||
|
@ -154,7 +154,7 @@ func (st *SpellTargeting) getAETargets(luaSpell *LuaSpell, result *TargetingResu
|
||||
// 5. Add valid targets to the list
|
||||
|
||||
// For now, implement basic logic
|
||||
spellData := luaSpell.Spell.GetSpellData()
|
||||
_ = luaSpell.Spell.GetSpellData() // TODO: Use spell data when needed
|
||||
maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists
|
||||
if maxTargets <= 0 {
|
||||
maxTargets = 10 // Default limit
|
||||
|
Loading…
x
Reference in New Issue
Block a user