package entity import ( "math" "sync" "sync/atomic" "eq2emu/internal/common" "eq2emu/internal/spawn" ) // Combat and pet types const ( PetTypeSummon = 1 PetTypeCharm = 2 PetTypeDeity = 3 PetTypeCosmetic = 4 ) // Entity represents a combat-capable spawn (NPCs and Players) // Extends the base Spawn with combat, magic, and equipment systems type Entity struct { *spawn.Spawn // Embedded spawn for basic functionality // Core entity information infoStruct *InfoStruct // All entity statistics and information // Combat state inCombat atomic.Bool // Whether currently in combat casting atomic.Bool // Whether currently casting maxSpeed float32 // Maximum movement speed baseSpeed float32 // Base movement speed speedMultiplier float32 // Speed multiplier // Position tracking for movement lastX float32 // Last known X position lastY float32 // Last known Y position lastZ float32 // Last known Z position lastHeading float32 // Last known heading // Regeneration regenHpRate int16 // Health regeneration rate regenPowerRate int16 // Power regeneration rate // Spell and effect management spellEffectManager *SpellEffectManager // Pet system pet *Entity // Summon pet charmedPet *Entity // Charmed pet deityPet *Entity // Deity pet cosmeticPet *Entity // Cosmetic pet owner int32 // Owner entity ID (if this is a pet) petType int8 // Type of pet petSpellID int32 // Spell ID used to create/control pet petSpellTier int8 // Tier of pet spell petDismissing atomic.Bool // Whether pet is being dismissed // Group and social // TODO: Add GroupMemberInfo when group system is implemented // groupMemberInfo *GroupMemberInfo // Trading // TODO: Add Trade when trade system is implemented // trade *Trade // Deity and alignment deity int8 // Deity ID // Equipment and appearance features common.CharFeatures // Character appearance features equipment common.EQ2Equipment // Equipment appearance // Threat and hate management threatTransferPercent float32 // Percentage of threat to transfer // Spell-related flags hasSeeInvisSpell atomic.Bool // Has see invisible spell hasSeeHideSpell atomic.Bool // Has see hidden spell // Proc system // TODO: Add proc list when implemented // procList []Proc // Thread safety spellEffectMutex sync.RWMutex maintainedMutex sync.RWMutex detrimentalMutex sync.RWMutex commandMutex sync.Mutex bonusCalculationMutex sync.RWMutex petMutex sync.RWMutex } // NewEntity creates a new Entity with default values // Must be called by subclasses (NPC, Player) for proper initialization func NewEntity() *Entity { e := &Entity{ Spawn: spawn.NewSpawn(), infoStruct: NewInfoStruct(), maxSpeed: 6.0, baseSpeed: 0.0, speedMultiplier: 1.0, lastX: -1, lastY: -1, lastZ: -1, lastHeading: -1, regenHpRate: 0, regenPowerRate: 0, spellEffectManager: NewSpellEffectManager(), pet: nil, charmedPet: nil, deityPet: nil, cosmeticPet: nil, owner: 0, petType: 0, petSpellID: 0, petSpellTier: 0, deity: 0, threatTransferPercent: 0.0, } // Initialize features and equipment e.features = common.CharFeatures{} e.equipment = common.EQ2Equipment{} // Set initial state e.inCombat.Store(false) e.casting.Store(false) e.petDismissing.Store(false) e.hasSeeInvisSpell.Store(false) e.hasSeeHideSpell.Store(false) return e } // IsEntity returns true (implements Spawn interface) func (e *Entity) IsEntity() bool { return true } // GetInfoStruct returns the entity's info structure func (e *Entity) GetInfoStruct() *InfoStruct { return e.infoStruct } // SetInfoStruct updates the entity's info structure func (e *Entity) SetInfoStruct(info *InfoStruct) { if info != nil { e.infoStruct = info } } // GetClient returns the client for this entity (overridden by Player) func (e *Entity) GetClient() any { return nil } // Combat state methods // IsInCombat returns whether the entity is currently in combat func (e *Entity) IsInCombat() bool { return e.inCombat.Load() } // SetInCombat updates the combat state func (e *Entity) SetInCombat(inCombat bool) { e.inCombat.Store(inCombat) } // IsCasting returns whether the entity is currently casting func (e *Entity) IsCasting() bool { return e.casting.Load() } // SetCasting updates the casting state func (e *Entity) SetCasting(casting bool) { e.casting.Store(casting) } // Speed and movement methods // GetMaxSpeed returns the maximum movement speed func (e *Entity) GetMaxSpeed() float32 { return e.maxSpeed } // SetMaxSpeed updates the maximum movement speed func (e *Entity) SetMaxSpeed(speed float32) { e.maxSpeed = speed } // GetBaseSpeed returns the base movement speed func (e *Entity) GetBaseSpeed() float32 { return e.baseSpeed } // SetBaseSpeed updates the base movement speed func (e *Entity) SetBaseSpeed(speed float32) { e.baseSpeed = speed } // GetSpeedMultiplier returns the speed multiplier func (e *Entity) GetSpeedMultiplier() float32 { return e.speedMultiplier } // SetSpeedMultiplier updates the speed multiplier func (e *Entity) SetSpeedMultiplier(multiplier float32) { e.speedMultiplier = multiplier } // CalculateEffectiveSpeed calculates the current effective speed func (e *Entity) CalculateEffectiveSpeed() float32 { baseSpeed := e.baseSpeed if baseSpeed <= 0 { baseSpeed = e.maxSpeed } return baseSpeed * e.speedMultiplier } // Position tracking methods // GetLastPosition returns the last known position func (e *Entity) GetLastPosition() (float32, float32, float32, float32) { return e.lastX, e.lastY, e.lastZ, e.lastHeading } // SetLastPosition updates the last known position func (e *Entity) SetLastPosition(x, y, z, heading float32) { e.lastX = x e.lastY = y e.lastZ = z e.lastHeading = heading } // HasMoved checks if the entity has moved since last position update func (e *Entity) HasMoved() bool { currentX := e.GetX() currentY := e.GetY() currentZ := e.GetZ() currentHeading := e.GetHeading() const epsilon = 0.01 // Small threshold for floating point comparison return math.Abs(float64(currentX-e.lastX)) > epsilon || math.Abs(float64(currentY-e.lastY)) > epsilon || math.Abs(float64(currentZ-e.lastZ)) > epsilon || math.Abs(float64(currentHeading-e.lastHeading)) > epsilon } // Stat calculation methods // GetStr returns the effective strength stat func (e *Entity) GetStr() int16 { base := int16(e.infoStruct.GetStr()) 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, 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, 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, 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, int16(e.infoStruct.GetRace()), 0)) // Stat type 5 = INT return base + bonus } // GetPrimaryStat returns the highest primary stat value func (e *Entity) GetPrimaryStat() int16 { str := e.GetStr() sta := e.GetSta() agi := e.GetAgi() wis := e.GetWis() intel := e.GetIntel() primary := str if sta > primary { primary = sta } if agi > primary { primary = agi } if wis > primary { primary = wis } if intel > primary { primary = intel } return primary } // Resistance methods // GetHeatResistance returns heat resistance func (e *Entity) GetHeatResistance() int16 { return e.infoStruct.GetResistance("heat") } // GetColdResistance returns cold resistance func (e *Entity) GetColdResistance() int16 { return e.infoStruct.GetResistance("cold") } // GetMagicResistance returns magic resistance func (e *Entity) GetMagicResistance() int16 { return e.infoStruct.GetResistance("magic") } // GetMentalResistance returns mental resistance func (e *Entity) GetMentalResistance() int16 { return e.infoStruct.GetResistance("mental") } // GetDivineResistance returns divine resistance func (e *Entity) GetDivineResistance() int16 { return e.infoStruct.GetResistance("divine") } // GetDiseaseResistance returns disease resistance func (e *Entity) GetDiseaseResistance() int16 { return e.infoStruct.GetResistance("disease") } // GetPoisonResistance returns poison resistance func (e *Entity) GetPoisonResistance() int16 { return e.infoStruct.GetResistance("poison") } // Spell effect management methods // AddMaintainedSpell adds a maintained spell effect func (e *Entity) AddMaintainedSpell(name string, spellID int32, duration float32, concentration int8) bool { // Check if we have enough concentration if !e.infoStruct.AddConcentration(int16(concentration)) { return false } effect := NewMaintainedEffects(name, spellID, duration) effect.ConcUsed = concentration e.maintainedMutex.Lock() defer e.maintainedMutex.Unlock() if !e.spellEffectManager.AddMaintainedEffect(effect) { // Failed to add, return concentration e.infoStruct.RemoveConcentration(int16(concentration)) return false } return true } // RemoveMaintainedSpell removes a maintained spell effect func (e *Entity) RemoveMaintainedSpell(spellID int32) bool { e.maintainedMutex.Lock() defer e.maintainedMutex.Unlock() // Get the effect to check concentration usage effect := e.spellEffectManager.GetMaintainedEffect(spellID) if effect == nil { return false } // Return concentration e.infoStruct.RemoveConcentration(int16(effect.ConcUsed)) return e.spellEffectManager.RemoveMaintainedEffect(spellID) } // GetMaintainedSpell retrieves a maintained spell effect func (e *Entity) GetMaintainedSpell(spellID int32) *MaintainedEffects { e.maintainedMutex.RLock() defer e.maintainedMutex.RUnlock() return e.spellEffectManager.GetMaintainedEffect(spellID) } // AddSpellEffect adds a temporary spell effect func (e *Entity) AddSpellEffect(spellID int32, casterID int32, duration float32) bool { effect := NewSpellEffects(spellID, casterID, duration) e.spellEffectMutex.Lock() defer e.spellEffectMutex.Unlock() return e.spellEffectManager.AddSpellEffect(effect) } // RemoveSpellEffect removes a spell effect func (e *Entity) RemoveSpellEffect(spellID int32) bool { e.spellEffectMutex.Lock() defer e.spellEffectMutex.Unlock() return e.spellEffectManager.RemoveSpellEffect(spellID) } // AddDetrimentalSpell adds a detrimental effect func (e *Entity) AddDetrimentalSpell(spellID int32, casterID int32, duration float32, detType int8) { effect := NewDetrimentalEffects(spellID, casterID, duration) effect.DetType = detType e.detrimentalMutex.Lock() defer e.detrimentalMutex.Unlock() e.spellEffectManager.AddDetrimentalEffect(*effect) } // RemoveDetrimentalSpell removes a detrimental effect func (e *Entity) RemoveDetrimentalSpell(spellID int32, casterID int32) bool { e.detrimentalMutex.Lock() defer e.detrimentalMutex.Unlock() return e.spellEffectManager.RemoveDetrimentalEffect(spellID, casterID) } // GetDetrimentalEffect retrieves a detrimental effect func (e *Entity) GetDetrimentalEffect(spellID int32, casterID int32) *DetrimentalEffects { e.detrimentalMutex.RLock() defer e.detrimentalMutex.RUnlock() return e.spellEffectManager.GetDetrimentalEffect(spellID, casterID) } // HasControlEffect checks if the entity has a specific control effect func (e *Entity) HasControlEffect(controlType int8) bool { return e.spellEffectManager.HasControlEffect(controlType) } // Bonus system methods // 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+ e.spellEffectManager.AddBonus(bonus) } // AddStatBonus adds a stat bonus func (e *Entity) AddStatBonus(spellID int32, statType int16, value float32) { bonus := NewBonusValues(spellID, statType, value) e.spellEffectManager.AddBonus(bonus) } // CalculateBonuses recalculates all stat bonuses and updates InfoStruct func (e *Entity) CalculateBonuses() { e.bonusCalculationMutex.Lock() defer e.bonusCalculationMutex.Unlock() // Reset effects to base values e.infoStruct.ResetEffects() entityClass := int64(1 << e.infoStruct.GetClass1()) // Convert class to bitmask race := int16(e.infoStruct.GetRace()) factionID := int32(e.GetFactionID()) // Apply stat bonuses strBonus := e.spellEffectManager.GetBonusValue(1, entityClass, race, factionID) staBonus := e.spellEffectManager.GetBonusValue(2, entityClass, race, factionID) agiBonus := e.spellEffectManager.GetBonusValue(3, entityClass, race, factionID) wisBonus := e.spellEffectManager.GetBonusValue(4, entityClass, race, factionID) intelBonus := e.spellEffectManager.GetBonusValue(5, entityClass, race, factionID) // Update InfoStruct with bonuses e.infoStruct.SetStr(e.infoStruct.GetStr() + strBonus) e.infoStruct.SetSta(e.infoStruct.GetSta() + staBonus) e.infoStruct.SetAgi(e.infoStruct.GetAgi() + agiBonus) e.infoStruct.SetWis(e.infoStruct.GetWis() + wisBonus) e.infoStruct.SetIntel(e.infoStruct.GetIntel() + intelBonus) // Apply resistance bonuses heatBonus := int16(e.spellEffectManager.GetBonusValue(10, entityClass, race, factionID)) coldBonus := int16(e.spellEffectManager.GetBonusValue(11, entityClass, race, factionID)) magicBonus := int16(e.spellEffectManager.GetBonusValue(12, entityClass, race, factionID)) mentalBonus := int16(e.spellEffectManager.GetBonusValue(13, entityClass, race, factionID)) divineBonus := int16(e.spellEffectManager.GetBonusValue(14, entityClass, race, factionID)) diseaseBonus := int16(e.spellEffectManager.GetBonusValue(15, entityClass, race, factionID)) poisonBonus := int16(e.spellEffectManager.GetBonusValue(16, entityClass, race, factionID)) e.infoStruct.SetResistance("heat", e.infoStruct.GetResistance("heat")+heatBonus) e.infoStruct.SetResistance("cold", e.infoStruct.GetResistance("cold")+coldBonus) e.infoStruct.SetResistance("magic", e.infoStruct.GetResistance("magic")+magicBonus) e.infoStruct.SetResistance("mental", e.infoStruct.GetResistance("mental")+mentalBonus) e.infoStruct.SetResistance("divine", e.infoStruct.GetResistance("divine")+divineBonus) e.infoStruct.SetResistance("disease", e.infoStruct.GetResistance("disease")+diseaseBonus) e.infoStruct.SetResistance("poison", e.infoStruct.GetResistance("poison")+poisonBonus) } // Pet management methods // 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() pet.petType = PetTypeSummon } } // 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() pet.petType = PetTypeCharm } } // 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() pet.petType = PetTypeDeity } } // 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() pet.petType = PetTypeCosmetic } } // GetOwner returns the owner entity ID (if this is a pet) func (e *Entity) GetOwner() int32 { return e.owner } // GetPetType returns the type of pet this entity is func (e *Entity) GetPetType() int8 { return e.petType } // IsPetDismissing returns whether the pet is being dismissed func (e *Entity) IsPetDismissing() bool { return e.petDismissing.Load() } // SetPetDismissing sets whether the pet is being dismissed func (e *Entity) SetPetDismissing(dismissing bool) { e.petDismissing.Store(dismissing) } // Utility methods // GetDeity returns the deity ID func (e *Entity) GetDeity() int8 { return e.deity } // SetDeity updates the deity ID func (e *Entity) SetDeity(deity int8) { e.deity = deity } // GetDodgeChance calculates the dodge chance (to be overridden by subclasses) func (e *Entity) GetDodgeChance() float32 { // Base implementation, should be overridden return 5.0 // 5% base dodge chance } // HasSeeInvisSpell returns whether the entity has see invisible func (e *Entity) HasSeeInvisSpell() bool { return e.hasSeeInvisSpell.Load() } // SetSeeInvisSpell updates the see invisible state func (e *Entity) SetSeeInvisSpell(hasSpell bool) { e.hasSeeInvisSpell.Store(hasSpell) } // HasSeeHideSpell returns whether the entity has see hidden func (e *Entity) HasSeeHideSpell() bool { return e.hasSeeHideSpell.Load() } // SetSeeHideSpell updates the see hidden state func (e *Entity) SetSeeHideSpell(hasSpell bool) { e.hasSeeHideSpell.Store(hasSpell) } // Cleanup methods // DeleteSpellEffects removes all spell effects func (e *Entity) DeleteSpellEffects(removeClient bool) { e.spellEffectMutex.Lock() e.maintainedMutex.Lock() e.detrimentalMutex.Lock() defer e.spellEffectMutex.Unlock() defer e.maintainedMutex.Unlock() defer e.detrimentalMutex.Unlock() // Clear all maintained effects and return concentration effects := e.spellEffectManager.GetAllMaintainedEffects() for _, effect := range effects { e.infoStruct.RemoveConcentration(int16(effect.ConcUsed)) } e.spellEffectManager.ClearAllEffects() } // RemoveSpells removes spell effects, optionally only unfriendly ones func (e *Entity) RemoveSpells(unfriendlyOnly bool) { // TODO: Implement when we can determine friendly vs unfriendly spells if !unfriendlyOnly { e.DeleteSpellEffects(false) } } // Update methods // ProcessEffects handles periodic effect processing func (e *Entity) ProcessEffects() { // Clean up expired effects e.spellEffectManager.CleanupExpiredEffects() // TODO: Apply periodic effect damage/healing // TODO: Handle effect-based stat changes } // Class system integration adapters // GetClass returns the entity's primary class (ClassAware interface compatibility) func (e *Entity) GetClass() int8 { return e.infoStruct.GetClass1() } // SetClass sets the entity's primary class (ClassAware interface compatibility) func (e *Entity) SetClass(classID int8) { e.infoStruct.SetClass1(classID) } // GetLevel returns the entity's level (EntityWithClass interface compatibility) func (e *Entity) GetLevel() int8 { return int8(e.infoStruct.GetLevel()) } // TODO: Additional methods to implement: // - Combat calculation methods (damage, healing, etc.) // - Equipment bonus application methods // - Spell casting methods // - Threat and hate management methods // - Group combat methods // - Many more combat and magic related methods