483 lines
11 KiB
Go
483 lines
11 KiB
Go
package ai
|
|
|
|
import "fmt"
|
|
|
|
// Logger interface for AI logging
|
|
type Logger interface {
|
|
LogInfo(message string, args ...interface{})
|
|
LogError(message string, args ...interface{})
|
|
LogDebug(message string, args ...interface{})
|
|
LogWarning(message string, args ...interface{})
|
|
}
|
|
|
|
// NPC interface defines the required NPC functionality for AI
|
|
type NPC interface {
|
|
// Basic NPC information
|
|
GetID() int32
|
|
GetName() string
|
|
GetHP() int32
|
|
GetTotalHP() int32
|
|
SetHP(int32)
|
|
IsAlive() bool
|
|
|
|
// Combat state
|
|
GetInCombat() bool
|
|
InCombat(bool)
|
|
GetTarget() Entity
|
|
SetTarget(Entity)
|
|
|
|
// Pet functionality
|
|
IsPet() bool
|
|
GetOwner() Entity
|
|
|
|
// Movement and positioning
|
|
GetX() float32
|
|
GetY() float32
|
|
GetZ() float32
|
|
GetDistance(Entity) float32
|
|
FaceTarget(Entity, bool)
|
|
IsFollowing() bool
|
|
SetFollowing(bool)
|
|
GetFollowTarget() Spawn
|
|
SetFollowTarget(Spawn, float32)
|
|
CalculateRunningLocation(bool)
|
|
ClearRunningLocations()
|
|
|
|
// Runback functionality
|
|
IsRunningBack() bool
|
|
GetRunbackLocation() *MovementLocation
|
|
GetRunbackDistance() float32
|
|
Runback(float32)
|
|
ShouldCallRunback() bool
|
|
SetCallRunback(bool)
|
|
|
|
// Status effects
|
|
IsMezzedOrStunned() bool
|
|
IsCasting() bool
|
|
IsDazed() bool
|
|
IsFeared() bool
|
|
IsStifled() bool
|
|
InWater() bool
|
|
IsWaterCreature() bool
|
|
IsFlyingCreature() bool
|
|
|
|
// Combat mechanics
|
|
AttackAllowed(Entity) bool
|
|
PrimaryWeaponReady() bool
|
|
SecondaryWeaponReady() bool
|
|
SetPrimaryLastAttackTime(int64)
|
|
SetSecondaryLastAttackTime(int64)
|
|
MeleeAttack(Entity, float32, bool)
|
|
|
|
// Spell casting
|
|
GetCastPercentage() int8
|
|
GetNextSpell(Entity, float32) Spell
|
|
GetNextBuffSpell(Spawn) Spell
|
|
SetCastOnAggroCompleted(bool)
|
|
CheckLoS(Entity) bool
|
|
|
|
// Movement pausing
|
|
IsPauseMovementTimerActive() bool
|
|
|
|
// Encounter state
|
|
SetEncounterState(int8)
|
|
|
|
// Scripts
|
|
GetSpawnScript() string
|
|
|
|
// Utility
|
|
KillSpawn(NPC)
|
|
}
|
|
|
|
// Entity interface for combat entities
|
|
type Entity interface {
|
|
Spawn
|
|
GetID() int32
|
|
GetName() string
|
|
GetHP() int32
|
|
GetTotalHP() int32
|
|
IsPlayer() bool
|
|
IsBot() bool
|
|
IsPet() bool
|
|
GetOwner() Entity
|
|
InWater() bool
|
|
}
|
|
|
|
// Spawn interface for basic spawn functionality
|
|
type Spawn interface {
|
|
GetID() int32
|
|
GetName() string
|
|
GetX() float32
|
|
GetY() float32
|
|
GetZ() float32
|
|
}
|
|
|
|
// Spell interface for spell data
|
|
type Spell interface {
|
|
GetSpellID() int32
|
|
GetName() string
|
|
IsFriendlySpell() bool
|
|
GetCastTime() int32
|
|
GetRecoveryTime() int32
|
|
GetRange() float32
|
|
GetMinRange() float32
|
|
}
|
|
|
|
// MovementLocation represents a location for movement/runback
|
|
type MovementLocation struct {
|
|
X float32
|
|
Y float32
|
|
Z float32
|
|
GridID int32
|
|
Stage int32
|
|
}
|
|
|
|
// LuaInterface defines Lua script integration
|
|
type LuaInterface interface {
|
|
RunSpawnScript(script, function string, npc NPC, target Entity) error
|
|
}
|
|
|
|
// Zone interface for zone-related AI operations
|
|
type Zone interface {
|
|
GetSpawnByID(int32) Spawn
|
|
ProcessSpell(spell Spell, caster NPC, target Spawn) error
|
|
CallSpawnScript(npc NPC, scriptType string, args ...interface{}) error
|
|
}
|
|
|
|
// AIManager provides high-level management of the AI system
|
|
type AIManager struct {
|
|
brains map[int32]Brain // Map of NPC ID to brain
|
|
activeCount int64 // Number of active brains
|
|
totalThinks int64 // Total think cycles processed
|
|
logger Logger // Logger for AI events
|
|
luaInterface LuaInterface // Lua script interface
|
|
}
|
|
|
|
// NewAIManager creates a new AI manager
|
|
func NewAIManager(logger Logger, luaInterface LuaInterface) *AIManager {
|
|
return &AIManager{
|
|
brains: make(map[int32]Brain),
|
|
activeCount: 0,
|
|
totalThinks: 0,
|
|
logger: logger,
|
|
luaInterface: luaInterface,
|
|
}
|
|
}
|
|
|
|
// AddBrain adds a brain for an NPC
|
|
func (am *AIManager) AddBrain(npcID int32, brain Brain) error {
|
|
if brain == nil {
|
|
return fmt.Errorf("brain cannot be nil")
|
|
}
|
|
|
|
if _, exists := am.brains[npcID]; exists {
|
|
return fmt.Errorf("brain already exists for NPC %d", npcID)
|
|
}
|
|
|
|
am.brains[npcID] = brain
|
|
if brain.IsActive() {
|
|
am.activeCount++
|
|
}
|
|
|
|
if am.logger != nil {
|
|
am.logger.LogDebug("Added brain for NPC %d (type: %d)", npcID, brain.GetBrainType())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveBrain removes a brain for an NPC
|
|
func (am *AIManager) RemoveBrain(npcID int32) {
|
|
if brain, exists := am.brains[npcID]; exists {
|
|
if brain.IsActive() {
|
|
am.activeCount--
|
|
}
|
|
delete(am.brains, npcID)
|
|
|
|
if am.logger != nil {
|
|
am.logger.LogDebug("Removed brain for NPC %d", npcID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetBrain retrieves a brain for an NPC
|
|
func (am *AIManager) GetBrain(npcID int32) Brain {
|
|
return am.brains[npcID]
|
|
}
|
|
|
|
// CreateBrainForNPC creates and adds the appropriate brain for an NPC
|
|
func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...interface{}) error {
|
|
if npc == nil {
|
|
return fmt.Errorf("NPC cannot be nil")
|
|
}
|
|
|
|
npcID := npc.GetID()
|
|
|
|
// Create brain based on type
|
|
var brain Brain
|
|
switch brainType {
|
|
case BrainTypeCombatPet:
|
|
brain = NewCombatPetBrain(npc, am.logger)
|
|
|
|
case BrainTypeNonCombatPet:
|
|
brain = NewNonCombatPetBrain(npc, am.logger)
|
|
|
|
case BrainTypeBlank:
|
|
brain = NewBlankBrain(npc, am.logger)
|
|
|
|
case BrainTypeLua:
|
|
brain = NewLuaBrain(npc, am.logger, am.luaInterface)
|
|
|
|
case BrainTypeDumbFire:
|
|
if len(options) >= 2 {
|
|
if target, ok := options[0].(Entity); ok {
|
|
if expireTime, ok := options[1].(int32); ok {
|
|
brain = NewDumbFirePetBrain(npc, target, expireTime, am.logger)
|
|
}
|
|
}
|
|
}
|
|
if brain == nil {
|
|
return fmt.Errorf("invalid options for dumbfire brain")
|
|
}
|
|
|
|
default:
|
|
brain = NewBaseBrain(npc, am.logger)
|
|
}
|
|
|
|
return am.AddBrain(npcID, brain)
|
|
}
|
|
|
|
// ProcessAllBrains runs think cycles for all active brains
|
|
func (am *AIManager) ProcessAllBrains() {
|
|
currentTime := currentTimeMillis()
|
|
|
|
for npcID, brain := range am.brains {
|
|
if !brain.IsActive() {
|
|
continue
|
|
}
|
|
|
|
// Check if it's time to think
|
|
lastThink := brain.GetLastThink()
|
|
thinkTick := brain.GetThinkTick()
|
|
|
|
if currentTime-lastThink >= int64(thinkTick) {
|
|
if err := brain.Think(); err != nil {
|
|
if am.logger != nil {
|
|
am.logger.LogError("Brain think error for NPC %d: %v", npcID, err)
|
|
}
|
|
}
|
|
am.totalThinks++
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetBrainActive sets the active state of a brain
|
|
func (am *AIManager) SetBrainActive(npcID int32, active bool) {
|
|
if brain := am.brains[npcID]; brain != nil {
|
|
wasActive := brain.IsActive()
|
|
brain.SetActive(active)
|
|
|
|
// Update active count
|
|
if wasActive && !active {
|
|
am.activeCount--
|
|
} else if !wasActive && active {
|
|
am.activeCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetActiveCount returns the number of active brains
|
|
func (am *AIManager) GetActiveCount() int64 {
|
|
return am.activeCount
|
|
}
|
|
|
|
// GetTotalThinks returns the total number of think cycles processed
|
|
func (am *AIManager) GetTotalThinks() int64 {
|
|
return am.totalThinks
|
|
}
|
|
|
|
// GetBrainCount returns the total number of brains
|
|
func (am *AIManager) GetBrainCount() int {
|
|
return len(am.brains)
|
|
}
|
|
|
|
// GetBrainsByType returns all brains of a specific type
|
|
func (am *AIManager) GetBrainsByType(brainType int8) []Brain {
|
|
var result []Brain
|
|
for _, brain := range am.brains {
|
|
if brain.GetBrainType() == brainType {
|
|
result = append(result, brain)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ClearAllBrains removes all brains
|
|
func (am *AIManager) ClearAllBrains() {
|
|
am.brains = make(map[int32]Brain)
|
|
am.activeCount = 0
|
|
|
|
if am.logger != nil {
|
|
am.logger.LogInfo("Cleared all AI brains")
|
|
}
|
|
}
|
|
|
|
// GetStatistics returns overall AI system statistics
|
|
func (am *AIManager) GetStatistics() *AIStatistics {
|
|
return &AIStatistics{
|
|
TotalBrains: len(am.brains),
|
|
ActiveBrains: int(am.activeCount),
|
|
TotalThinks: am.totalThinks,
|
|
BrainsByType: am.getBrainCountsByType(),
|
|
}
|
|
}
|
|
|
|
// getBrainCountsByType returns counts of brains by type
|
|
func (am *AIManager) getBrainCountsByType() map[string]int {
|
|
counts := make(map[string]int)
|
|
|
|
for _, brain := range am.brains {
|
|
typeName := getBrainTypeName(brain.GetBrainType())
|
|
counts[typeName]++
|
|
}
|
|
|
|
return counts
|
|
}
|
|
|
|
// AIStatistics contains AI system statistics
|
|
type AIStatistics struct {
|
|
TotalBrains int `json:"total_brains"`
|
|
ActiveBrains int `json:"active_brains"`
|
|
TotalThinks int64 `json:"total_thinks"`
|
|
BrainsByType map[string]int `json:"brains_by_type"`
|
|
}
|
|
|
|
// AIBrainAdapter provides NPC functionality for brains
|
|
type AIBrainAdapter struct {
|
|
npc NPC
|
|
logger Logger
|
|
}
|
|
|
|
// NewAIBrainAdapter creates a new brain adapter
|
|
func NewAIBrainAdapter(npc NPC, logger Logger) *AIBrainAdapter {
|
|
return &AIBrainAdapter{
|
|
npc: npc,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetNPC returns the adapted NPC
|
|
func (aba *AIBrainAdapter) GetNPC() NPC {
|
|
return aba.npc
|
|
}
|
|
|
|
// ProcessAI processes AI for the NPC using its brain
|
|
func (aba *AIBrainAdapter) ProcessAI(brain Brain) error {
|
|
if brain == nil {
|
|
return fmt.Errorf("brain is nil")
|
|
}
|
|
|
|
if !brain.IsActive() {
|
|
return nil
|
|
}
|
|
|
|
return brain.Think()
|
|
}
|
|
|
|
// SetupDefaultBrain sets up a default brain for the NPC
|
|
func (aba *AIBrainAdapter) SetupDefaultBrain() Brain {
|
|
return NewBaseBrain(aba.npc, aba.logger)
|
|
}
|
|
|
|
// SetupPetBrain sets up a pet brain based on pet type
|
|
func (aba *AIBrainAdapter) SetupPetBrain(combatPet bool) Brain {
|
|
if combatPet {
|
|
return NewCombatPetBrain(aba.npc, aba.logger)
|
|
}
|
|
return NewNonCombatPetBrain(aba.npc, aba.logger)
|
|
}
|
|
|
|
// Utility functions
|
|
|
|
// getBrainTypeName returns the string name for a brain type
|
|
func getBrainTypeName(brainType int8) string {
|
|
switch brainType {
|
|
case BrainTypeDefault:
|
|
return "default"
|
|
case BrainTypeCombatPet:
|
|
return "combat_pet"
|
|
case BrainTypeNonCombatPet:
|
|
return "non_combat_pet"
|
|
case BrainTypeBlank:
|
|
return "blank"
|
|
case BrainTypeLua:
|
|
return "lua"
|
|
case BrainTypeDumbFire:
|
|
return "dumbfire"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// currentTimeMillis returns current time in milliseconds
|
|
func currentTimeMillis() int64 {
|
|
return time.Now().UnixMilli()
|
|
}
|
|
|
|
// HateListDebugger provides debugging functionality for hate lists
|
|
type HateListDebugger struct {
|
|
logger Logger
|
|
}
|
|
|
|
// NewHateListDebugger creates a new hate list debugger
|
|
func NewHateListDebugger(logger Logger) *HateListDebugger {
|
|
return &HateListDebugger{
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// PrintHateList prints a formatted hate list
|
|
func (hld *HateListDebugger) PrintHateList(npcName string, hateList map[int32]*HateEntry) {
|
|
if hld.logger == nil {
|
|
return
|
|
}
|
|
|
|
hld.logger.LogInfo("%s's Hate List", npcName)
|
|
hld.logger.LogInfo("-------------------")
|
|
|
|
if len(hateList) == 0 {
|
|
hld.logger.LogInfo("(empty)")
|
|
} else {
|
|
for entityID, entry := range hateList {
|
|
hld.logger.LogInfo("Entity %d: %d hate", entityID, entry.HateValue)
|
|
}
|
|
}
|
|
|
|
hld.logger.LogInfo("-------------------")
|
|
}
|
|
|
|
// PrintEncounterList prints a formatted encounter list
|
|
func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList map[int32]*EncounterEntry) {
|
|
if hld.logger == nil {
|
|
return
|
|
}
|
|
|
|
hld.logger.LogInfo("%s's Encounter List", npcName)
|
|
hld.logger.LogInfo("-------------------")
|
|
|
|
if len(encounterList) == 0 {
|
|
hld.logger.LogInfo("(empty)")
|
|
} else {
|
|
for entityID, entry := range encounterList {
|
|
entryType := "NPC"
|
|
if entry.IsPlayer {
|
|
entryType = "Player"
|
|
} else if entry.IsBot {
|
|
entryType = "Bot"
|
|
}
|
|
hld.logger.LogInfo("Entity %d (%s)", entityID, entryType)
|
|
}
|
|
}
|
|
|
|
hld.logger.LogInfo("-------------------")
|
|
} |