634 lines
16 KiB
Go
634 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
|
|
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
|
|
}
|