eq2go/internal/entity/entity.go

761 lines
21 KiB
Go

package entity
import (
"math"
"sync"
"sync/atomic"
"eq2emu/internal/common"
"eq2emu/internal/spawn"
"eq2emu/internal/spells"
)
// 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 *spells.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: spells.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 := spells.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) *spells.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 := spells.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 := spells.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) *spells.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 := 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 := spells.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())
}
// Health and Power methods (delegate to underlying spawn)
// GetHP returns the entity's current hit points
func (e *Entity) GetHP() int32 {
return e.Spawn.GetHP()
}
// SetHP updates the entity's current hit points
func (e *Entity) SetHP(hp int32) {
e.Spawn.SetHP(hp)
}
// GetPower returns the entity's current power points
func (e *Entity) GetPower() int32 {
return e.Spawn.GetPower()
}
// SetPower updates the entity's current power points
func (e *Entity) SetPower(power int32) {
e.Spawn.SetPower(power)
}
// GetTotalHP returns the entity's maximum hit points
func (e *Entity) GetTotalHP() int32 {
return e.Spawn.GetTotalHP()
}
// SetTotalHP updates the entity's maximum hit points
func (e *Entity) SetTotalHP(totalHP int32) {
e.Spawn.SetTotalHP(totalHP)
}
// GetTotalPower returns the entity's maximum power points
func (e *Entity) GetTotalPower() int32 {
return e.Spawn.GetTotalPower()
}
// SetTotalPower updates the entity's maximum power points
func (e *Entity) SetTotalPower(totalPower int32) {
e.Spawn.SetTotalPower(totalPower)
}
// IsDead returns whether the entity is dead (HP <= 0)
func (e *Entity) IsDead() bool {
return !e.Spawn.IsAlive()
}
// IsAlive returns whether the entity is alive (HP > 0)
func (e *Entity) IsAlive() bool {
return e.Spawn.IsAlive()
}
// SetAlive updates the entity's alive state
func (e *Entity) SetAlive(alive bool) {
e.Spawn.SetAlive(alive)
}
// 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