eq2go/internal/npc/ai/brain.go

636 lines
16 KiB
Go

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
wasInCombat := bb.npc.GetInCombat()
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
}