package ai import ( "fmt" "time" ) // BaseBrain implements the basic AI brain functionality type BaseBrain struct { npc NPC logger Logger brainType int8 state *BrainState hateList *HateList encounter *EncounterList statistics *BrainStatistics } // NewBaseBrain creates a new base brain func NewBaseBrain(npc NPC, logger Logger) *BaseBrain { return &BaseBrain{ npc: npc, logger: logger, brainType: BrainTypeDefault, state: NewBrainState(), hateList: NewHateList(), encounter: NewEncounterList(), statistics: NewBrainStatistics(), } } // Think implements the main AI logic func (bb *BaseBrain) Think() error { if bb.npc == nil { return fmt.Errorf("brain has no body") } // Update last think time currentTime := time.Now().UnixMilli() bb.state.SetLastThink(currentTime) bb.statistics.ThinkCycles++ // Skip thinking if mezzzed, stunned, feared, or dazed if bb.npc.IsMezzedOrStunned() || bb.npc.IsFeared() || bb.npc.IsDazed() { return nil } // Check if we need to runback if bb.npc.IsRunningBack() || bb.npc.ShouldCallRunback() { return bb.handleRunback() } // Get most hated target mostHatedID := bb.hateList.GetMostHated() var target Entity if mostHatedID > 0 { target = bb.getEntityByID(mostHatedID) if target == nil { // Target no longer exists, remove from hate list bb.hateList.RemoveHate(mostHatedID) mostHatedID = bb.hateList.GetMostHated() if mostHatedID > 0 { target = bb.getEntityByID(mostHatedID) } } } // Handle combat state if target != nil && bb.npc.GetInCombat() { return bb.handleCombat(target) } else if bb.npc.GetInCombat() { // No target but still in combat - exit combat bb.npc.InCombat(false) bb.npc.SetTarget(nil) bb.state.SetState(AIStateIdle) if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic { bb.logger.LogDebug("NPC %s exiting combat - no targets", bb.npc.GetName()) } return nil } // Handle idle state return bb.handleIdle() } // handleCombat handles combat AI logic func (bb *BaseBrain) handleCombat(target Entity) error { if target == nil { return nil } // Set target if not already set if bb.npc.GetTarget() != target { bb.npc.SetTarget(target) bb.npc.FaceTarget(target, false) } distance := bb.npc.GetDistance(target) // Check if target is too far away maxRange := bb.getMaxCombatRange() if distance > maxRange { // Try to move closer or runback if too far if distance > MaxChaseDistance { if bb.npc.ShouldCallRunback() { bb.npc.Runback(bb.npc.GetRunbackDistance()) return nil } } else { bb.MoveCloser(target) } return nil } bb.state.SetState(AIStateCombat) // Try to cast spells first if we have line of sight if bb.npc.CheckLoS(target) && !bb.npc.IsCasting() { if bb.HasRecovered() && bb.ProcessSpell(target, distance) { return nil } } // Try melee combat bb.ProcessMelee(target, distance) return nil } // handleIdle handles idle AI logic func (bb *BaseBrain) handleIdle() error { bb.state.SetState(AIStateIdle) // Set slower think tick when idle bb.state.SetThinkTick(SlowThinkTick) return nil } // handleRunback handles runback logic func (bb *BaseBrain) handleRunback() error { // Clear hate and encounter when running back bb.hateList.Clear() bb.encounter.Clear() // Set target to nil bb.npc.SetTarget(nil) bb.npc.InCombat(false) bb.state.SetState(AIStateRunback) if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s running back", bb.npc.GetName()) } return nil } // ProcessSpell processes spell casting for the brain func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool { if bb.npc == nil || target == nil { return false } // Check if we can cast if bb.npc.IsCasting() || !bb.HasRecovered() { return false } // Get next spell to cast spell := bb.npc.GetNextSpell(target, distance) if spell == nil { return false } // Check range if distance < spell.GetMinRange() || distance > spell.GetRange() { return false } // Cast the spell if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s casting spell %s on %s", bb.npc.GetName(), spell.GetName(), target.GetName()) } // Set spell recovery time recoveryTime := time.Now().UnixMilli() + int64(spell.GetRecoveryTime()*RecoveryTimeMultiple) + int64(SpellRecoveryBuffer) bb.state.SetSpellRecovery(recoveryTime) bb.state.SetState(AIStateCasting) bb.statistics.SpellsCast++ return true } // ProcessMelee processes melee combat for the brain func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) { if bb.npc == nil || target == nil { return } // Check if we can attack if !bb.npc.AttackAllowed(target) { return } // Try primary weapon if bb.npc.PrimaryWeaponReady() { if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s melee attacking %s with primary weapon", bb.npc.GetName(), target.GetName()) } bb.npc.MeleeAttack(target, distance, true) bb.npc.SetPrimaryLastAttackTime(time.Now().UnixMilli()) bb.statistics.MeleeAttacks++ return } // Try secondary weapon if bb.npc.SecondaryWeaponReady() { if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s melee attacking %s with secondary weapon", bb.npc.GetName(), target.GetName()) } bb.npc.MeleeAttack(target, distance, false) bb.npc.SetSecondaryLastAttackTime(time.Now().UnixMilli()) bb.statistics.MeleeAttacks++ } } // MoveCloser moves the NPC closer to the target func (bb *BaseBrain) MoveCloser(target Entity) { if bb.npc == nil || target == nil { return } // Face the target bb.npc.FaceTarget(target, false) // Calculate running location towards target bb.npc.CalculateRunningLocation(true) bb.state.SetState(AIStateMoving) if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s moving closer to %s", bb.npc.GetName(), target.GetName()) } } // Brain interface implementation // GetBrainType returns the brain type func (bb *BaseBrain) GetBrainType() int8 { return bb.brainType } // IsActive returns whether the brain is active func (bb *BaseBrain) IsActive() bool { return bb.state.IsActive() } // SetActive sets the brain's active state func (bb *BaseBrain) SetActive(active bool) { bb.state.SetActive(active) } // GetLastThink returns the timestamp of the last think cycle func (bb *BaseBrain) GetLastThink() int64 { return bb.state.GetLastThink() } // SetLastThink sets the timestamp of the last think cycle func (bb *BaseBrain) SetLastThink(timestamp int64) { bb.state.SetLastThink(timestamp) } // GetThinkTick returns the think tick interval func (bb *BaseBrain) GetThinkTick() int32 { return bb.state.GetThinkTick() } // SetThinkTick sets the think tick interval func (bb *BaseBrain) SetThinkTick(tick int32) { bb.state.SetThinkTick(tick) } // Hate management // AddHate adds hate for an entity func (bb *BaseBrain) AddHate(entityID, hate int32) { bb.hateList.AddHate(entityID, hate) bb.statistics.HateEvents++ // Enter combat if not already if !bb.npc.GetInCombat() { bb.npc.InCombat(true) bb.state.SetThinkTick(FastThinkTick) } if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s gained %d hate for entity %d", bb.npc.GetName(), hate, entityID) } } // GetHate returns hate for an entity func (bb *BaseBrain) GetHate(entityID int32) int32 { return bb.hateList.GetHate(entityID) } // GetMostHated returns the most hated entity ID func (bb *BaseBrain) GetMostHated() int32 { return bb.hateList.GetMostHated() } // ClearHate clears all hate func (bb *BaseBrain) ClearHate() { bb.hateList.Clear() if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic { bb.logger.LogDebug("NPC %s cleared all hate", bb.npc.GetName()) } } // ClearHateForEntity clears hate for a specific entity func (bb *BaseBrain) ClearHateForEntity(entityID int32) { bb.hateList.RemoveHate(entityID) if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s cleared hate for entity %d", bb.npc.GetName(), entityID) } } // Encounter management // AddToEncounter adds an entity to the encounter func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool { success := bb.encounter.AddEntity(entityID, characterID, isPlayer, isBot) if success { bb.statistics.EncounterEvents++ if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s added entity %d to encounter", bb.npc.GetName(), entityID) } } return success } // RemoveFromEncounter removes an entity from the encounter func (bb *BaseBrain) RemoveFromEncounter(entityID int32) { bb.encounter.RemoveEntity(entityID) if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s removed entity %d from encounter", bb.npc.GetName(), entityID) } } // IsInEncounter checks if an entity is in the encounter func (bb *BaseBrain) IsInEncounter(entityID int32) bool { return bb.encounter.IsEntityInEncounter(entityID) } // ClearEncounter clears the encounter func (bb *BaseBrain) ClearEncounter() { bb.encounter.Clear() if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic { bb.logger.LogDebug("NPC %s cleared encounter", bb.npc.GetName()) } } // GetEncounterSize returns the encounter size func (bb *BaseBrain) GetEncounterSize() int { return bb.encounter.Size() } // State management // GetState returns the current AI state func (bb *BaseBrain) GetState() int32 { return bb.state.GetState() } // SetState sets the current AI state func (bb *BaseBrain) SetState(state int32) { bb.state.SetState(state) } // HasRecovered checks if the brain has recovered from spell casting func (bb *BaseBrain) HasRecovered() bool { return bb.state.HasRecovered() } // SetSpellRecovery sets the spell recovery time func (bb *BaseBrain) SetSpellRecovery(timestamp int64) { bb.state.SetSpellRecovery(timestamp) } // GetStatistics returns brain statistics func (bb *BaseBrain) GetStatistics() *BrainStatistics { return bb.statistics } // GetDebugLevel returns the debug level func (bb *BaseBrain) GetDebugLevel() int8 { return bb.state.GetDebugLevel() } // SetDebugLevel sets the debug level func (bb *BaseBrain) SetDebugLevel(level int8) { bb.state.SetDebugLevel(level) } // Helper methods // getEntityByID gets an entity by ID (placeholder - would be implemented by zone) func (bb *BaseBrain) getEntityByID(entityID int32) Entity { // This would typically call into the zone system to get the entity // For now, return nil - concrete implementations should override this return nil } // getMaxCombatRange returns the maximum combat range func (bb *BaseBrain) getMaxCombatRange() float32 { return MaxCombatRange } // Pet Brain Implementations // CombatPetBrain extends the base brain for combat pets type CombatPetBrain struct { *BaseBrain } // NewCombatPetBrain creates a new combat pet brain func NewCombatPetBrain(npc NPC, logger Logger) *CombatPetBrain { brain := &CombatPetBrain{ BaseBrain: NewBaseBrain(npc, logger), } brain.brainType = BrainTypeCombatPet return brain } // Think implements pet-specific AI logic func (cpb *CombatPetBrain) Think() error { // Call parent Think() for default combat behavior if err := cpb.BaseBrain.Think(); err != nil { return err } // Additional pet-specific logic if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() { return nil } if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed { cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName()) } // Check if owner has stay command set owner := cpb.npc.GetOwner() if owner != nil && owner.IsPlayer() { // TODO: Check player's pet movement setting // if player.GetInfoStruct().GetPetMovement() == PetMovementStay { // return nil // } } // Follow owner if owner != nil { cpb.npc.SetTarget(owner) distance := cpb.npc.GetDistance(owner) maxRange := cpb.getMaxCombatRange() if distance > maxRange { cpb.MoveCloser(owner) } } return nil } // NonCombatPetBrain handles non-combat pets (cosmetic pets) type NonCombatPetBrain struct { *BaseBrain } // NewNonCombatPetBrain creates a new non-combat pet brain func NewNonCombatPetBrain(npc NPC, logger Logger) *NonCombatPetBrain { brain := &NonCombatPetBrain{ BaseBrain: NewBaseBrain(npc, logger), } brain.brainType = BrainTypeNonCombatPet return brain } // Think implements non-combat pet AI (just following) func (ncpb *NonCombatPetBrain) Think() error { // Non-combat pets don't do combat AI if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() { return nil } if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed { ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName()) } // Just follow owner owner := ncpb.npc.GetOwner() if owner != nil { ncpb.npc.SetTarget(owner) distance := ncpb.npc.GetDistance(owner) maxRange := ncpb.getMaxCombatRange() if distance > maxRange { ncpb.MoveCloser(owner) } } return nil } // BlankBrain provides a minimal AI that does nothing type BlankBrain struct { *BaseBrain } // NewBlankBrain creates a new blank brain func NewBlankBrain(npc NPC, logger Logger) *BlankBrain { brain := &BlankBrain{ BaseBrain: NewBaseBrain(npc, logger), } brain.brainType = BrainTypeBlank brain.SetThinkTick(BlankBrainTick) // Very slow tick return brain } // Think does nothing for blank brains func (bb *BlankBrain) Think() error { // Blank brain does nothing return nil } // DumbFirePetBrain handles dumbfire pets (temporary combat pets) type DumbFirePetBrain struct { *BaseBrain expireTime int64 } // NewDumbFirePetBrain creates a new dumbfire pet brain func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logger) *DumbFirePetBrain { brain := &DumbFirePetBrain{ BaseBrain: NewBaseBrain(npc, logger), expireTime: time.Now().UnixMilli() + int64(expireTimeMS), } brain.brainType = BrainTypeDumbFire // Add maximum hate for the target if target != nil { brain.AddHate(target.GetID(), MaxHateValue) } return brain } // AddHate only allows hate for the initial target func (dfpb *DumbFirePetBrain) AddHate(entityID int32, hate int32) { // Only add hate if we don't already have a target if dfpb.GetMostHated() == 0 { dfpb.BaseBrain.AddHate(entityID, hate) } } // Think implements dumbfire pet AI func (dfpb *DumbFirePetBrain) Think() error { // Check if expired if time.Now().UnixMilli() > dfpb.expireTime { if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { if dfpb.logger != nil { dfpb.logger.LogDebug("Dumbfire pet %s expired", dfpb.npc.GetName()) } dfpb.npc.KillSpawn(dfpb.npc) } return nil } // Get target targetID := dfpb.GetMostHated() if targetID == 0 { // No target, kill self if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { if dfpb.logger != nil { dfpb.logger.LogDebug("Dumbfire pet %s has no target", dfpb.npc.GetName()) } dfpb.npc.KillSpawn(dfpb.npc) } return nil } target := dfpb.getEntityByID(targetID) if target == nil { // Target no longer exists, kill self if dfpb.npc != nil && dfpb.npc.GetHP() > 0 { dfpb.npc.KillSpawn(dfpb.npc) } return nil } // Skip if mezzed or stunned if dfpb.npc.IsMezzedOrStunned() { return nil } // Set target if not already set if dfpb.npc.GetTarget() != target { dfpb.npc.SetTarget(target) dfpb.npc.FaceTarget(target, false) } // Enter combat if not already if !dfpb.npc.GetInCombat() { dfpb.npc.CalculateRunningLocation(true) dfpb.npc.InCombat(true) } distance := dfpb.npc.GetDistance(target) // Try to cast spells if we have line of sight if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() && (!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) { if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed { dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s", dfpb.npc.GetName(), target.GetName()) } dfpb.npc.FaceTarget(target, false) dfpb.ProcessMelee(target, distance) } return nil } // GetExpireTime returns when this dumbfire pet will expire func (dfpb *DumbFirePetBrain) GetExpireTime() int64 { return dfpb.expireTime } // SetExpireTime sets when this dumbfire pet will expire func (dfpb *DumbFirePetBrain) SetExpireTime(expireTime int64) { dfpb.expireTime = expireTime } // IsExpired checks if the dumbfire pet has expired func (dfpb *DumbFirePetBrain) IsExpired() bool { return time.Now().UnixMilli() > dfpb.expireTime } // ExtendExpireTime extends the expire time by the given duration func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) { dfpb.expireTime += int64(durationMS) }