eq2go/internal/npc/ai/interfaces.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("-------------------")
}