package ai import ( "fmt" "sync" "time" ) // Brain interface defines the core AI functionality type Brain interface { // Core AI methods Think() error GetBrainType() int8 // State management IsActive() bool SetActive(bool) GetState() int32 SetState(int32) // Timing GetThinkTick() int32 SetThinkTick(int32) GetLastThink() int64 SetLastThink(int64) // Body management GetBody() NPC SetBody(NPC) // Hate management AddHate(entityID int32, hate int32) GetHate(entityID int32) int32 ClearHate() ClearHateForEntity(entityID int32) GetMostHated() int32 GetHatePercentage(entityID int32) int8 GetHateList() map[int32]*HateEntry // Encounter management AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool IsEntityInEncounter(entityID int32) bool IsPlayerInEncounter(characterID int32) bool HasPlayerInEncounter() bool GetEncounterSize() int ClearEncounter() CheckLootAllowed(entityID int32) bool // Combat methods ProcessSpell(target Entity, distance float32) bool ProcessMelee(target Entity, distance float32) CheckBuffs() bool HasRecovered() bool MoveCloser(target Spawn) // Statistics GetStatistics() *BrainStatistics ResetStatistics() } // BaseBrain provides the default AI implementation type BaseBrain struct { npc NPC // The NPC this brain controls brainType int8 // Type of brain state *BrainState // Brain state management hateList *HateList // Hate management encounterList *EncounterList // Encounter management statistics *BrainStatistics // Performance statistics logger Logger // Logger interface mutex sync.RWMutex // Thread safety } // NewBaseBrain creates a new base brain func NewBaseBrain(npc NPC, logger Logger) *BaseBrain { return &BaseBrain{ npc: npc, brainType: BrainTypeDefault, state: NewBrainState(), hateList: NewHateList(), encounterList: NewEncounterList(), statistics: NewBrainStatistics(), logger: logger, } } // Think implements the main AI logic func (bb *BaseBrain) Think() error { if !bb.IsActive() { return nil } startTime := time.Now() defer func() { // Update statistics bb.mutex.Lock() bb.statistics.ThinkCycles++ thinkTime := float64(time.Since(startTime).Nanoseconds()) / 1000000.0 // Convert to milliseconds bb.statistics.AverageThinkTime = (bb.statistics.AverageThinkTime + thinkTime) / 2.0 bb.statistics.LastThinkTime = time.Now().UnixMilli() bb.mutex.Unlock() bb.state.SetLastThink(time.Now().UnixMilli()) }() if bb.npc == nil { return fmt.Errorf("brain has no body") } // Handle pet ID registration for players if bb.npc.IsPet() && bb.npc.GetOwner() != nil && bb.npc.GetOwner().IsPlayer() { // TODO: Register pet ID with player's info struct } // Get the most hated target mostHatedID := bb.hateList.GetMostHated() var target Entity if mostHatedID > 0 { target = bb.getEntityByID(mostHatedID) // Remove dead targets from hate list if target != nil && target.GetHP() <= 0 { bb.hateList.RemoveHate(mostHatedID) target = nil // Try again to get most hated mostHatedID = bb.hateList.GetMostHated() if mostHatedID > 0 { target = bb.getEntityByID(mostHatedID) } } } // Skip if mezzed, stunned, or feared if bb.npc.IsMezzedOrStunned() { return nil } // Get runback distance runbackDistance := bb.npc.GetRunbackDistance() if target != nil { // We have a target to fight if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s has target %s", bb.npc.GetName(), target.GetName()) } // Set target if not already set if bb.npc.GetTarget() != target { bb.npc.SetTarget(target) } // Face the target bb.npc.FaceTarget(target, false) // Enter combat if not already in combat if !bb.npc.GetInCombat() { bb.npc.ClearRunningLocations() bb.npc.InCombat(true) bb.npc.SetCastOnAggroCompleted(false) // TODO: Call spawn script for aggro } // Check chase distance and water restrictions if bb.shouldBreakPursuit(target, runbackDistance) { // Break pursuit - clear hate and encounter if bb.logger != nil { bb.logger.LogDebug("NPC %s breaking pursuit (distance: %.2f)", bb.npc.GetName(), runbackDistance) } // TODO: Send encounter break messages to players bb.hateList.Clear() bb.encounterList.Clear() } else { // Continue combat distance := bb.npc.GetDistance(target) // Try to cast spells first, then melee if !bb.npc.IsCasting() && (!bb.HasRecovered() || !bb.ProcessSpell(target, distance)) { if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s attempting melee on %s", bb.npc.GetName(), target.GetName()) } bb.npc.FaceTarget(target, false) bb.ProcessMelee(target, distance) } } } else { // No target - handle out of combat behavior if bb.npc.GetInCombat() { bb.npc.InCombat(false) // Restore HP for non-player pets if !bb.npc.IsPet() || (bb.npc.IsPet() && bb.npc.GetOwner() != nil && !bb.npc.GetOwner().IsPlayer()) { bb.npc.SetHP(bb.npc.GetTotalHP()) } } // Check for buffs when not in combat bb.CheckBuffs() // Handle runback if needed if !bb.npc.GetInCombat() && !bb.npc.IsPauseMovementTimerActive() { if runbackDistance > RunbackThreshold || (bb.npc.ShouldCallRunback() && !bb.npc.IsFollowing()) { bb.npc.SetEncounterState(EncounterStateBroken) bb.npc.Runback(runbackDistance) bb.npc.SetCallRunback(false) } else if bb.npc.GetRunbackLocation() != nil { bb.handleRunbackStages() } } // Clear encounter if any entities remain if bb.encounterList.Size() > 0 { bb.encounterList.Clear() } } return nil } // 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) } // 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) } // 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) } // 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) } // GetBody returns the NPC this brain controls func (bb *BaseBrain) GetBody() NPC { bb.mutex.RLock() defer bb.mutex.RUnlock() return bb.npc } // SetBody sets the NPC this brain controls func (bb *BaseBrain) SetBody(npc NPC) { bb.mutex.Lock() defer bb.mutex.Unlock() bb.npc = npc } // AddHate adds hate for an entity func (bb *BaseBrain) AddHate(entityID int32, hate int32) { // Don't add hate while running back if bb.npc != nil && bb.npc.IsRunningBack() { return } // Don't add hate if owner is attacking pet if bb.npc != nil && bb.npc.IsPet() && bb.npc.GetOwner() != nil { if bb.npc.GetOwner().GetID() == entityID { return } } // Check for taunt immunity // TODO: Implement immunity checking bb.hateList.AddHate(entityID, hate) // Update statistics bb.mutex.Lock() bb.statistics.HateEvents++ bb.mutex.Unlock() // TODO: Add to entity's HatedBy list // Add pet owner to hate list if not already present entity := bb.getEntityByID(entityID) if entity != nil && entity.IsPet() && entity.GetOwner() != nil { ownerID := entity.GetOwner().GetID() if bb.hateList.GetHate(ownerID) == 0 { bb.hateList.AddHate(ownerID, 0) } } } // GetHate returns the hate value for an entity func (bb *BaseBrain) GetHate(entityID int32) int32 { return bb.hateList.GetHate(entityID) } // ClearHate removes all hate entries func (bb *BaseBrain) ClearHate() { bb.hateList.Clear() // TODO: Update entities' HatedBy lists } // ClearHateForEntity removes hate for a specific entity func (bb *BaseBrain) ClearHateForEntity(entityID int32) { bb.hateList.RemoveHate(entityID) // TODO: Update entity's HatedBy list } // GetMostHated returns the ID of the most hated entity func (bb *BaseBrain) GetMostHated() int32 { return bb.hateList.GetMostHated() } // GetHatePercentage returns the hate percentage for an entity func (bb *BaseBrain) GetHatePercentage(entityID int32) int8 { return bb.hateList.GetHatePercentage(entityID) } // GetHateList returns a copy of the hate list func (bb *BaseBrain) GetHateList() map[int32]*HateEntry { return bb.hateList.GetAllEntries() } // AddToEncounter adds an entity to the encounter list func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool { success := bb.encounterList.AddEntity(entityID, characterID, isPlayer, isBot) if success { bb.mutex.Lock() bb.statistics.EncounterEvents++ bb.mutex.Unlock() } return success } // IsEntityInEncounter checks if an entity is in the encounter func (bb *BaseBrain) IsEntityInEncounter(entityID int32) bool { return bb.encounterList.IsEntityInEncounter(entityID) } // IsPlayerInEncounter checks if a player is in the encounter func (bb *BaseBrain) IsPlayerInEncounter(characterID int32) bool { return bb.encounterList.IsPlayerInEncounter(characterID) } // HasPlayerInEncounter returns whether any player is in the encounter func (bb *BaseBrain) HasPlayerInEncounter() bool { return bb.encounterList.HasPlayerInEncounter() } // GetEncounterSize returns the size of the encounter list func (bb *BaseBrain) GetEncounterSize() int { return bb.encounterList.Size() } // ClearEncounter removes all entities from the encounter func (bb *BaseBrain) ClearEncounter() { bb.encounterList.Clear() // TODO: Remove spells from NPC } // CheckLootAllowed checks if an entity can loot this NPC func (bb *BaseBrain) CheckLootAllowed(entityID int32) bool { // TODO: Implement loot method checking, chest timers, etc. // Basic check - is entity in encounter? return bb.encounterList.IsEntityInEncounter(entityID) } // ProcessSpell attempts to cast a spell func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool { if bb.npc == nil { return false } // Check cast percentage and conditions castChance := bb.npc.GetCastPercentage() if castChance <= 0 { return false } // TODO: Implement random chance checking // TODO: Check for stifled, feared conditions // Get next spell to cast spell := bb.npc.GetNextSpell(target, distance) if spell == nil { return false } // Determine spell target var spellTarget Spawn = target if spell.IsFriendlySpell() { // TODO: Find best friendly target (lowest HP group member) spellTarget = bb.npc } // Cast the spell success := bb.castSpell(spell, spellTarget, false) if success { bb.mutex.Lock() bb.statistics.SpellsCast++ bb.mutex.Unlock() } return success } // ProcessMelee handles melee combat func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) { if bb.npc == nil || target == nil { return } maxCombatRange := bb.getMaxCombatRange() if distance > maxCombatRange { bb.MoveCloser(target) } else { if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed { bb.logger.LogDebug("NPC %s is within melee range of %s", bb.npc.GetName(), target.GetName()) } // Check if attack is allowed if !bb.npc.AttackAllowed(target) { return } currentTime := time.Now().UnixMilli() // Primary weapon attack if bb.npc.PrimaryWeaponReady() && !bb.npc.IsDazed() && !bb.npc.IsFeared() { if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelVerbose { bb.logger.LogDebug("NPC %s swings primary weapon at %s", bb.npc.GetName(), target.GetName()) } bb.npc.SetPrimaryLastAttackTime(currentTime) bb.npc.MeleeAttack(target, distance, true) bb.mutex.Lock() bb.statistics.MeleeAttacks++ bb.mutex.Unlock() // TODO: Call spawn script for auto attack tick } // Secondary weapon attack if bb.npc.SecondaryWeaponReady() && !bb.npc.IsDazed() { bb.npc.SetSecondaryLastAttackTime(currentTime) bb.npc.MeleeAttack(target, distance, false) bb.mutex.Lock() bb.statistics.MeleeAttacks++ bb.mutex.Unlock() } } } // CheckBuffs checks and casts buff spells func (bb *BaseBrain) CheckBuffs() bool { if bb.npc == nil { return false } // Don't buff in combat, while casting, stunned, etc. if bb.npc.GetInCombat() || bb.npc.IsCasting() || bb.npc.IsMezzedOrStunned() || !bb.npc.IsAlive() || bb.npc.IsStifled() || !bb.HasRecovered() { return false } // Get next buff spell buffSpell := bb.npc.GetNextBuffSpell(bb.npc) if buffSpell == nil { return false } // Try to cast on self first if bb.castSpell(buffSpell, bb.npc, false) { return true } // TODO: Try to buff group members return false } // HasRecovered checks if the brain has recovered from spell casting func (bb *BaseBrain) HasRecovered() bool { return bb.state.HasRecovered() } // MoveCloser moves the NPC closer to a target func (bb *BaseBrain) MoveCloser(target Spawn) { if bb.npc == nil || target == nil { return } maxCombatRange := bb.getMaxCombatRange() if bb.npc.GetFollowTarget() != target { bb.npc.SetFollowTarget(target, maxCombatRange) } if bb.npc.GetFollowTarget() != nil && !bb.npc.IsFollowing() { bb.npc.CalculateRunningLocation(true) bb.npc.SetFollowing(true) } } // GetStatistics returns brain performance statistics func (bb *BaseBrain) GetStatistics() *BrainStatistics { bb.mutex.RLock() defer bb.mutex.RUnlock() // Return a copy return &BrainStatistics{ ThinkCycles: bb.statistics.ThinkCycles, SpellsCast: bb.statistics.SpellsCast, MeleeAttacks: bb.statistics.MeleeAttacks, HateEvents: bb.statistics.HateEvents, EncounterEvents: bb.statistics.EncounterEvents, AverageThinkTime: bb.statistics.AverageThinkTime, LastThinkTime: bb.statistics.LastThinkTime, TotalActiveTime: bb.statistics.TotalActiveTime, } } // ResetStatistics resets all performance statistics func (bb *BaseBrain) ResetStatistics() { bb.mutex.Lock() defer bb.mutex.Unlock() bb.statistics = NewBrainStatistics() } // Helper methods // shouldBreakPursuit checks if the NPC should break pursuit of a target func (bb *BaseBrain) shouldBreakPursuit(target Entity, runbackDistance float32) bool { if target == nil { return false } // Check max chase distance maxChase := bb.getMaxChaseDistance() if runbackDistance > maxChase { return true } // Check water creature restrictions if bb.npc != nil && bb.npc.IsWaterCreature() && !bb.npc.IsFlyingCreature() && !target.InWater() { return true } return false } // castSpell casts a spell on a target func (bb *BaseBrain) castSpell(spell Spell, target Spawn, calculateRunLoc bool) bool { if spell == nil || bb.npc == nil { return false } if calculateRunLoc { bb.npc.CalculateRunningLocation(true) } // TODO: Process spell through zone // bb.npc.GetZone().ProcessSpell(spell, bb.npc, target) // Set spell recovery time castTime := spell.GetCastTime() * RecoveryTimeMultiple recoveryTime := spell.GetRecoveryTime() * RecoveryTimeMultiple totalRecovery := time.Now().UnixMilli() + int64(castTime) + int64(recoveryTime) + int64(SpellRecoveryBuffer) bb.state.SetSpellRecovery(totalRecovery) return true } // handleRunbackStages handles the various stages of runback func (bb *BaseBrain) handleRunbackStages() { if bb.npc == nil { return } runbackLoc := bb.npc.GetRunbackLocation() if runbackLoc == nil { return } // TODO: Implement runback stage handling // This would involve movement management and position updates } // getEntityByID retrieves an entity by ID (placeholder) func (bb *BaseBrain) getEntityByID(entityID int32) Entity { // TODO: Implement entity lookup through zone return nil } // getMaxChaseDistance returns the maximum chase distance func (bb *BaseBrain) getMaxChaseDistance() float32 { // TODO: Check NPC info struct and zone rules return MaxChaseDistance } // getMaxCombatRange returns the maximum combat range func (bb *BaseBrain) getMaxCombatRange() float32 { // TODO: Check zone rules return MaxCombatRange }