From 7ce87100e6b4251e769ee93f210b11bfab4fd0ec Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 7 Aug 2025 18:06:48 -0500 Subject: [PATCH] modernize, improve entities. First pass --- internal/entity/README.md | 167 ---------------------------- internal/entity/benchmark_test.go | 97 ++++++++-------- internal/entity/concurrency_test.go | 136 +++++++++++----------- internal/entity/doc.go | 41 +++++++ internal/entity/entity.go | 19 ++-- internal/entity/entity_test.go | 12 +- internal/entity/info_struct_test.go | 24 ++-- internal/entity/spell_effects.go | 37 ------ 8 files changed, 186 insertions(+), 347 deletions(-) delete mode 100644 internal/entity/README.md create mode 100644 internal/entity/doc.go delete mode 100644 internal/entity/spell_effects.go diff --git a/internal/entity/README.md b/internal/entity/README.md deleted file mode 100644 index 3551428..0000000 --- a/internal/entity/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# Entity Package - -The Entity package provides the core combat and magic systems for EverQuest II server emulation. It extends the base Spawn system with combat capabilities, spell effects, and character statistics management. - -## Overview - -The Entity system is built on three main components: - -1. **InfoStruct** - Comprehensive character statistics and information -2. **SpellEffectManager** - Manages all spell effects, buffs, debuffs, and bonuses -3. **Entity** - Combat-capable spawn with spell casting and pet management - -## Architecture - -``` -Spawn (base class) - └── Entity (combat-capable) - ├── Player (player characters) - └── NPC (non-player characters) -``` - -## Core Components - -### InfoStruct - -Contains all character statistics including: -- Primary attributes (STR, STA, AGI, WIS, INT) -- Combat stats (attack, mitigation, avoidance) -- Resistances (heat, cold, magic, mental, divine, disease, poison) -- Experience points and currency -- Equipment and weapon information -- Group and encounter settings - -**Thread Safety**: All access methods use RWMutex for safe concurrent access. - -### SpellEffectManager - -Manages four types of spell effects: - -1. **Maintained Effects** - Buffs that consume concentration -2. **Spell Effects** - Temporary buffs/debuffs with durations -3. **Detrimental Effects** - Debuffs and harmful effects -4. **Bonus Values** - Stat modifications from various sources - -**Key Features**: -- Automatic expiration handling -- Control effect tracking (stun, root, mez, etc.) -- Bonus calculations with class/race/faction requirements -- Thread-safe effect management - -### Entity - -Combat-capable spawn that extends base Spawn functionality: - -**Combat Systems**: -- Health/Power/Savagery management -- Combat state tracking (in combat, casting) -- Damage resistance calculations -- Speed and movement modifiers - -**Magic Systems**: -- Spell effect application and removal -- Concentration-based maintained spells -- Bonus stat calculations -- Control effect immunity - -**Pet Systems**: -- Multiple pet types (summon, charm, deity, cosmetic) -- Pet ownership and dismissal -- Pet spell tracking - -## Usage Examples - -### Creating an Entity - -```go -entity := NewEntity() -entity.GetInfoStruct().SetName("TestEntity") -entity.GetInfoStruct().SetLevel(50) -entity.GetInfoStruct().SetStr(100.0) -``` - -### Managing Spell Effects - -```go -// Add a maintained spell (buff) -success := entity.AddMaintainedSpell("Heroic Strength", 12345, 300.0, 2) - -// Add a temporary effect -entity.AddSpellEffect(54321, casterID, 60.0) - -// Add a detrimental effect -entity.AddDetrimentalSpell(99999, attackerID, 30.0, 1) -``` - -### Stat Calculations - -```go -// Get effective stats (base + bonuses) -str := entity.GetStr() -sta := entity.GetSta() -primary := entity.GetPrimaryStat() - -// Recalculate all bonuses -entity.CalculateBonuses() -``` - -### Pet Management - -```go -// Set a summon pet -entity.SetPet(petEntity) - -// Check pet status -if entity.GetPet() != nil && !entity.IsPetDismissing() { - // Pet is active -} -``` - -## Constants and Enums - -### Pet Types -- `PetTypeSummon` - Summoned pets -- `PetTypeCharm` - Charmed creatures -- `PetTypeDeity` - Deity pets -- `PetTypeCosmetic` - Cosmetic pets - -### Control Effects -- `ControlEffectStun` - Cannot move or act -- `ControlEffectRoot` - Cannot move -- `ControlEffectMez` - Mesmerized -- `ControlEffectDaze` - Dazed -- `ControlEffectFear` - Feared -- `ControlEffectSlow` - Movement slowed -- `ControlEffectSnare` - Movement impaired -- `ControlEffectCharm` - Mind controlled - -## Thread Safety - -All Entity operations are thread-safe using: -- `sync.RWMutex` for read/write operations -- `sync.atomic` for simple state flags -- Separate mutexes for different subsystems to minimize lock contention - -## Integration with Spawn System - -The Entity extends the base Spawn class and requires: -- `spawn.NewSpawn()` for initialization -- Access to Spawn position and basic methods -- Integration with zone update systems - -## Future Extensions - -Areas marked with TODO comments for future implementation: -- Complete item and equipment systems -- Combat calculation methods -- Threat and hate management -- Group combat mechanics -- Spell casting systems -- LUA script integration - -## Files - -- `entity.go` - Main Entity class implementation -- `info_struct.go` - Character statistics and information -- `spell_effects.go` - Spell effect management system -- `README.md` - This documentation file \ No newline at end of file diff --git a/internal/entity/benchmark_test.go b/internal/entity/benchmark_test.go index f11c3d1..d77555a 100644 --- a/internal/entity/benchmark_test.go +++ b/internal/entity/benchmark_test.go @@ -20,7 +20,7 @@ func BenchmarkEntityCreation(b *testing.B) { // 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) @@ -28,7 +28,7 @@ func BenchmarkEntityCombatState(b *testing.B) { entity.SetInCombat(false) } }) - + b.Run("Parallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -43,7 +43,7 @@ func BenchmarkEntityCombatState(b *testing.B) { // 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) @@ -51,7 +51,7 @@ func BenchmarkEntityCastingState(b *testing.B) { entity.SetCasting(false) } }) - + b.Run("Parallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -67,14 +67,14 @@ func BenchmarkEntityCastingState(b *testing.B) { 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() @@ -84,7 +84,7 @@ func BenchmarkEntityStatCalculations(b *testing.B) { _ = entity.GetIntel() } }) - + b.Run("GetStatsParallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -96,13 +96,13 @@ func BenchmarkEntityStatCalculations(b *testing.B) { } }) }) - + 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() @@ -113,7 +113,7 @@ func BenchmarkEntityStatCalculations(b *testing.B) { // 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) @@ -121,7 +121,7 @@ func BenchmarkEntitySpellEffects(b *testing.B) { entity.RemoveSpellEffect(spellID) } }) - + b.Run("AddRemoveSpellEffectParallel", func(b *testing.B) { var counter int64 b.RunParallel(func(pb *testing.PB) { @@ -140,7 +140,7 @@ 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) @@ -149,7 +149,7 @@ func BenchmarkEntityMaintainedSpells(b *testing.B) { } } }) - + b.Run("AddRemoveMaintainedSpellParallel", func(b *testing.B) { var counter int64 b.RunParallel(func(pb *testing.PB) { @@ -167,14 +167,14 @@ func BenchmarkEntityMaintainedSpells(b *testing.B) { // 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() { @@ -183,14 +183,14 @@ func BenchmarkInfoStructBasicOps(b *testing.B) { } }) }) - + 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) @@ -199,7 +199,7 @@ func BenchmarkInfoStructBasicOps(b *testing.B) { info.SetAgi(val) info.SetWis(val) info.SetIntel(val) - + _ = info.GetStr() _ = info.GetSta() _ = info.GetAgi() @@ -213,7 +213,7 @@ func BenchmarkInfoStructBasicOps(b *testing.B) { 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) @@ -222,7 +222,7 @@ func BenchmarkInfoStructConcentration(b *testing.B) { } } }) - + b.Run("AddRemoveConcentrationParallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -238,7 +238,7 @@ func BenchmarkInfoStructConcentration(b *testing.B) { // 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) @@ -246,7 +246,7 @@ func BenchmarkInfoStructCoins(b *testing.B) { info.RemoveCoins(amount / 2) } }) - + b.Run("AddRemoveCoinsParallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -256,7 +256,7 @@ func BenchmarkInfoStructCoins(b *testing.B) { } }) }) - + b.Run("GetCoins", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = info.GetCoins() @@ -268,7 +268,7 @@ func BenchmarkInfoStructCoins(b *testing.B) { 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)] @@ -277,7 +277,7 @@ func BenchmarkInfoStructResistances(b *testing.B) { _ = info.GetResistance(resistType) } }) - + b.Run("SetGetResistancesParallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -293,7 +293,7 @@ func BenchmarkInfoStructResistances(b *testing.B) { // BenchmarkInfoStructClone measures clone operations func BenchmarkInfoStructClone(b *testing.B) { info := NewInfoStruct() - + // Set up some state to clone info.SetName("Original Character") info.SetLevel(50) @@ -304,9 +304,9 @@ func BenchmarkInfoStructClone(b *testing.B) { info.SetIntel(105.0) info.AddConcentration(5) info.AddCoins(50000) - + b.ResetTimer() - + for i := 0; i < b.N; i++ { clone := info.Clone() _ = clone @@ -316,7 +316,7 @@ func BenchmarkInfoStructClone(b *testing.B) { // 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() @@ -325,7 +325,7 @@ func BenchmarkEntityPetManagement(b *testing.B) { entity.SetPet(nil) } }) - + b.Run("SetGetPetParallel", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { @@ -336,23 +336,23 @@ func BenchmarkEntityPetManagement(b *testing.B) { } }) }) - + 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) @@ -366,7 +366,7 @@ func BenchmarkEntityPetManagement(b *testing.B) { func BenchmarkConcurrentWorkload(b *testing.B) { numEntities := 100 entities := make([]*Entity, numEntities) - + // Create entities for i := 0; i < numEntities; i++ { entities[i] = NewEntity() @@ -375,15 +375,15 @@ func BenchmarkConcurrentWorkload(b *testing.B) { 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) @@ -430,7 +430,7 @@ func BenchmarkMemoryAllocation(b *testing.B) { _ = entity } }) - + b.Run("InfoStructAllocation", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -438,15 +438,15 @@ func BenchmarkMemoryAllocation(b *testing.B) { _ = 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 @@ -459,7 +459,7 @@ 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() { @@ -471,7 +471,7 @@ func BenchmarkContention(b *testing.B) { } }) }) - + b.Run("HighContentionSpellEffects", func(b *testing.B) { var spellCounter int64 b.RunParallel(func(pb *testing.PB) { @@ -488,11 +488,11 @@ func BenchmarkContention(b *testing.B) { // 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) @@ -503,4 +503,3 @@ func BenchmarkScalability(b *testing.B) { }) } } - diff --git a/internal/entity/concurrency_test.go b/internal/entity/concurrency_test.go index 506ac68..c288e19 100644 --- a/internal/entity/concurrency_test.go +++ b/internal/entity/concurrency_test.go @@ -20,12 +20,12 @@ func TestEntityConcurrencyStress(t *testing.T) { // 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) @@ -33,7 +33,7 @@ func TestEntityConcurrencyStress(t *testing.T) { atomic.AddInt64(&combatOps, 1) } entity.SetInCombat(false) - + // Casting state operations entity.SetCasting(true) if entity.IsCasting() { @@ -44,26 +44,26 @@ func TestEntityConcurrencyStress(t *testing.T) { }() } 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) } @@ -71,28 +71,28 @@ func TestEntityConcurrencyStress(t *testing.T) { }(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) } @@ -103,10 +103,10 @@ func TestEntityConcurrencyStress(t *testing.T) { }(i) } wg.Wait() - - t.Logf("Maintained spell adds: %d, removes: %d, concentration failures: %d", + + 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 { @@ -117,23 +117,23 @@ func TestEntityConcurrencyStress(t *testing.T) { // 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() @@ -141,31 +141,31 @@ func TestEntityConcurrencyStress(t *testing.T) { _ = 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: @@ -185,13 +185,13 @@ func TestEntityConcurrencyStress(t *testing.T) { _ = entity.GetCosmeticPet() entity.SetCosmeticPet(nil) } - + atomic.AddInt64(&petOps, 1) } }() } wg.Wait() - + t.Logf("Pet operations: %d", petOps) }) } @@ -208,23 +208,23 @@ func TestInfoStructConcurrencyStress(t *testing.T) { // 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() @@ -233,28 +233,28 @@ func TestInfoStructConcurrencyStress(t *testing.T) { }(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 { @@ -264,10 +264,10 @@ func TestInfoStructConcurrencyStress(t *testing.T) { }() } wg.Wait() - - t.Logf("Concentration adds: %d, failures: %d, removes: %d", + + t.Logf("Concentration adds: %d, failures: %d, removes: %d", addSuccesses, addFailures, removes) - + // Verify final state finalConc := info.GetCurConcentration() if finalConc < 0 || finalConc > info.GetMaxConcentration() { @@ -278,19 +278,19 @@ func TestInfoStructConcurrencyStress(t *testing.T) { // 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) { @@ -298,17 +298,17 @@ func TestInfoStructConcurrencyStress(t *testing.T) { } else { atomic.AddInt64(&removeFailures, 1) } - + // Read total coins _ = info.GetCoins() } }(i) } wg.Wait() - - t.Logf("Coin adds: %d, remove successes: %d, failures: %d", + + t.Logf("Coin adds: %d, remove successes: %d, failures: %d", addOps, removeSuccesses, removeFailures) - + // Verify coins are non-negative finalCoins := info.GetCoins() if finalCoins < 0 { @@ -320,16 +320,16 @@ func TestInfoStructConcurrencyStress(t *testing.T) { 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) @@ -337,25 +337,25 @@ func TestInfoStructConcurrencyStress(t *testing.T) { }(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 { @@ -370,7 +370,7 @@ func TestInfoStructConcurrencyStress(t *testing.T) { }() } wg.Wait() - + t.Logf("Clone operations: %d", cloneOps) }) } @@ -393,24 +393,24 @@ func TestRaceConditionDetection(t *testing.T) { 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 @@ -425,21 +425,21 @@ func TestRaceConditionDetection(t *testing.T) { 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") } -} \ No newline at end of file +} diff --git a/internal/entity/doc.go b/internal/entity/doc.go new file mode 100644 index 0000000..0e13f8d --- /dev/null +++ b/internal/entity/doc.go @@ -0,0 +1,41 @@ +// Package entity provides the core combat and magic systems for EverQuest II server emulation. +// It extends the base Spawn system with combat capabilities, spell effects, and character statistics management. +// +// Basic Usage: +// +// entity := entity.NewEntity() +// entity.GetInfoStruct().SetName("TestEntity") +// entity.GetInfoStruct().SetLevel(50) +// entity.GetInfoStruct().SetStr(100.0) +// +// Managing Spell Effects: +// +// // Add a maintained spell (buff) +// success := entity.AddMaintainedSpell("Heroic Strength", 12345, 300.0, 2) +// +// // Add a temporary effect +// entity.AddSpellEffect(54321, casterID, 60.0) +// +// // Add a detrimental effect +// entity.AddDetrimentalSpell(99999, attackerID, 30.0, 1) +// +// Stat Calculations: +// +// // Get effective stats (base + bonuses) +// str := entity.GetStr() +// sta := entity.GetSta() +// primary := entity.GetPrimaryStat() +// +// // Recalculate all bonuses +// entity.CalculateBonuses() +// +// Pet Management: +// +// // Set a summon pet +// entity.SetPet(petEntity) +// +// // Check pet status +// if entity.GetPet() != nil && !entity.IsPetDismissing() { +// // Pet is active +// } +package entity diff --git a/internal/entity/entity.go b/internal/entity/entity.go index 0705db5..b554754 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -7,6 +7,7 @@ import ( "eq2emu/internal/common" "eq2emu/internal/spawn" + "eq2emu/internal/spells" ) // Combat and pet types @@ -43,7 +44,7 @@ type Entity struct { regenPowerRate int16 // Power regeneration rate // Spell and effect management - spellEffectManager *SpellEffectManager + spellEffectManager *spells.SpellEffectManager // Pet system pet *Entity // Summon pet @@ -106,7 +107,7 @@ func NewEntity() *Entity { lastHeading: -1, regenHpRate: 0, regenPowerRate: 0, - spellEffectManager: NewSpellEffectManager(), + spellEffectManager: spells.NewSpellEffectManager(), pet: nil, charmedPet: nil, deityPet: nil, @@ -357,7 +358,7 @@ func (e *Entity) AddMaintainedSpell(name string, spellID int32, duration float32 return false } - effect := NewMaintainedEffects(name, spellID, duration) + effect := spells.NewMaintainedEffects(name, spellID, duration) effect.ConcUsed = concentration e.maintainedMutex.Lock() @@ -390,7 +391,7 @@ func (e *Entity) RemoveMaintainedSpell(spellID int32) bool { } // GetMaintainedSpell retrieves a maintained spell effect -func (e *Entity) GetMaintainedSpell(spellID int32) *MaintainedEffects { +func (e *Entity) GetMaintainedSpell(spellID int32) *spells.MaintainedEffects { e.maintainedMutex.RLock() defer e.maintainedMutex.RUnlock() @@ -399,7 +400,7 @@ func (e *Entity) GetMaintainedSpell(spellID int32) *MaintainedEffects { // AddSpellEffect adds a temporary spell effect func (e *Entity) AddSpellEffect(spellID int32, casterID int32, duration float32) bool { - effect := NewSpellEffects(spellID, casterID, duration) + effect := spells.NewSpellEffects(spellID, casterID, duration) e.spellEffectMutex.Lock() defer e.spellEffectMutex.Unlock() @@ -417,7 +418,7 @@ func (e *Entity) RemoveSpellEffect(spellID int32) bool { // AddDetrimentalSpell adds a detrimental effect func (e *Entity) AddDetrimentalSpell(spellID int32, casterID int32, duration float32, detType int8) { - effect := NewDetrimentalEffects(spellID, casterID, duration) + effect := spells.NewDetrimentalEffects(spellID, casterID, duration) effect.DetType = detType e.detrimentalMutex.Lock() @@ -435,7 +436,7 @@ func (e *Entity) RemoveDetrimentalSpell(spellID int32, casterID int32) bool { } // GetDetrimentalEffect retrieves a detrimental effect -func (e *Entity) GetDetrimentalEffect(spellID int32, casterID int32) *DetrimentalEffects { +func (e *Entity) GetDetrimentalEffect(spellID int32, casterID int32) *spells.DetrimentalEffects { e.detrimentalMutex.RLock() defer e.detrimentalMutex.RUnlock() @@ -451,13 +452,13 @@ func (e *Entity) HasControlEffect(controlType int8) bool { // AddSkillBonus adds a skill-related bonus func (e *Entity) AddSkillBonus(spellID int32, skillID int32, value float32) { - bonus := NewBonusValues(spellID, int16(skillID+100), value) // Skill bonuses use type 100+ + bonus := spells.NewBonusValues(spellID, int16(skillID+100), value) // Skill bonuses use type 100+ e.spellEffectManager.AddBonus(bonus) } // AddStatBonus adds a stat bonus func (e *Entity) AddStatBonus(spellID int32, statType int16, value float32) { - bonus := NewBonusValues(spellID, statType, value) + bonus := spells.NewBonusValues(spellID, statType, value) e.spellEffectManager.AddBonus(bonus) } diff --git a/internal/entity/entity_test.go b/internal/entity/entity_test.go index 4a850d0..d4b127f 100644 --- a/internal/entity/entity_test.go +++ b/internal/entity/entity_test.go @@ -4,6 +4,8 @@ import ( "sync" "testing" "time" + + "eq2emu/internal/spells" ) func TestNewEntity(t *testing.T) { @@ -68,7 +70,7 @@ func TestEntityIsEntity(t *testing.T) { func TestEntityInfoStruct(t *testing.T) { entity := NewEntity() - + stats := entity.GetInfoStruct() if stats == nil { t.Error("Expected InfoStruct to be initialized") @@ -92,7 +94,7 @@ func TestEntityInfoStruct(t *testing.T) { func TestEntityClient(t *testing.T) { entity := NewEntity() - + // Base Entity should return nil for GetClient client := entity.GetClient() if client != nil { @@ -628,7 +630,7 @@ func TestEntityControlEffects(t *testing.T) { // Test has control effect - should work without panicking // The actual implementation depends on the spell effect manager - hasStun := entity.HasControlEffect(ControlEffectStun) + hasStun := entity.HasControlEffect(spells.ControlEffectStun) _ = hasStun // We can't easily test the actual value without setting up effects } @@ -651,7 +653,7 @@ func TestEntityConstants(t *testing.T) { } // Test control effect constants (re-exported from spells package) - if ControlEffectStun == 0 && ControlEffectRoot == 0 { + if spells.ControlEffectStun == 0 && spells.ControlEffectRoot == 0 { t.Error("Control effect constants should be non-zero") } -} \ No newline at end of file +} diff --git a/internal/entity/info_struct_test.go b/internal/entity/info_struct_test.go index a3699bb..93ba60c 100644 --- a/internal/entity/info_struct_test.go +++ b/internal/entity/info_struct_test.go @@ -374,14 +374,14 @@ func TestInfoStructConcurrency(t *testing.T) { 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) @@ -393,7 +393,7 @@ func TestInfoStructConcurrency(t *testing.T) { 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 @@ -413,10 +413,10 @@ func TestInfoStructConcurrency(t *testing.T) { 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)) @@ -428,10 +428,10 @@ func TestInfoStructConcurrency(t *testing.T) { 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)) @@ -445,7 +445,7 @@ func TestInfoStructLargeValues(t *testing.T) { // 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) @@ -464,11 +464,11 @@ func TestInfoStructLargeValues(t *testing.T) { // 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()) } @@ -511,4 +511,4 @@ func TestInfoStructEdgeCases(t *testing.T) { if info.GetName() != longName { t.Error("Expected to handle very long names") } -} \ No newline at end of file +} diff --git a/internal/entity/spell_effects.go b/internal/entity/spell_effects.go deleted file mode 100644 index 7c4273f..0000000 --- a/internal/entity/spell_effects.go +++ /dev/null @@ -1,37 +0,0 @@ -package entity - -// DEPRECATED: This file now imports spell effect structures from the spells package. -// The spell effect management has been moved to internal/spells for better organization. - -import ( - "eq2emu/internal/spells" -) - -// Re-export spell effect types for backward compatibility -// These will eventually be removed in favor of direct imports from spells package - -type BonusValues = spells.BonusValues -type MaintainedEffects = spells.MaintainedEffects -type SpellEffects = spells.SpellEffects -type DetrimentalEffects = spells.DetrimentalEffects -type SpellEffectManager = spells.SpellEffectManager - -// Re-export constructor functions -var NewBonusValues = spells.NewBonusValues -var NewMaintainedEffects = spells.NewMaintainedEffects -var NewSpellEffects = spells.NewSpellEffects -var NewDetrimentalEffects = spells.NewDetrimentalEffects -var NewSpellEffectManager = spells.NewSpellEffectManager - -// Re-export constants -const ( - ControlEffectStun = spells.ControlEffectStun - ControlEffectRoot = spells.ControlEffectRoot - ControlEffectMez = spells.ControlEffectMez - ControlEffectDaze = spells.ControlEffectDaze - ControlEffectFear = spells.ControlEffectFear - ControlEffectSlow = spells.ControlEffectSlow - ControlEffectSnare = spells.ControlEffectSnare - ControlEffectCharm = spells.ControlEffectCharm - ControlMaxEffects = spells.ControlMaxEffects -)