eq2go/internal/npc/ai/manager.go
2025-08-29 17:48:39 -05:00

451 lines
11 KiB
Go

package ai
import (
"encoding/json"
"fmt"
)
// 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
// No external dependencies needed
}
// NewAIManager creates a new AI manager
func NewAIManager(logger Logger) *AIManager {
return &AIManager{
brains: make(map[int32]Brain),
activeCount: 0,
totalThinks: 0,
logger: logger,
}
}
// 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 ...any) 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 BrainTypeCustom:
// Custom brains need to be created with specific CustomAI implementations
// This is a fallback - normally created via NewCustomBrain directly
brain = NewBaseBrain(npc, am.logger)
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
}
// AI Communication and Packets
// SendAIUpdate sends an AI update packet for an NPC
func (am *AIManager) SendAIUpdate(client AIClient, npcID int32) error {
brain := am.GetBrain(npcID)
if brain == nil {
return fmt.Errorf("no brain found for NPC %d", npcID)
}
data := map[string]interface{}{
"npc_id": npcID,
"brain_type": brain.GetBrainType(),
"state": brain.GetState(),
"active": brain.IsActive(),
"think_tick": brain.GetThinkTick(),
"most_hated": brain.GetMostHated(),
"encounter_size": brain.GetEncounterSize(),
}
packet := CreateAIPacket(AIPacketTypeUpdate, npcID, data)
return client.SendPacket(packet)
}
// SendHateUpdate sends a hate update packet
func (am *AIManager) SendHateUpdate(client AIClient, npcID int32, targetID int32, hateValue int32) error {
data := map[string]interface{}{
"target_id": targetID,
"hate_value": hateValue,
}
packet := CreateAIPacket(AIPacketTypeHateUpdate, npcID, data)
return client.SendPacket(packet)
}
// SendEncounterUpdate sends an encounter update packet
func (am *AIManager) SendEncounterUpdate(client AIClient, npcID int32, encounterSize int) error {
data := map[string]interface{}{
"encounter_size": encounterSize,
}
packet := CreateAIPacket(AIPacketTypeEncounter, npcID, data)
return client.SendPacket(packet)
}
// SendAIStatistics sends AI system statistics
func (am *AIManager) SendAIStatistics(client AIClient) error {
stats := am.GetStatistics()
data := map[string]interface{}{
"total_brains": stats.TotalBrains,
"active_brains": stats.ActiveBrains,
"total_thinks": stats.TotalThinks,
"brains_by_type": stats.BrainsByType,
}
packet := CreateAIPacket(AIPacketTypeStatistics, 0, data)
return client.SendPacket(packet)
}
// BroadcastAIUpdate broadcasts an AI update to all clients
func (am *AIManager) BroadcastAIUpdate(client AIClient, npcID int32) error {
brain := am.GetBrain(npcID)
if brain == nil {
return fmt.Errorf("no brain found for NPC %d", npcID)
}
data := map[string]interface{}{
"npc_id": npcID,
"brain_type": brain.GetBrainType(),
"state": brain.GetState(),
"active": brain.IsActive(),
}
packet := CreateAIPacket(AIPacketTypeUpdate, npcID, data)
return client.BroadcastPacket(packet)
}
// ProcessAICommand processes an AI command packet
func (am *AIManager) ProcessAICommand(packet *AIPacket) error {
if packet.Type != AIPacketTypeCommand {
return fmt.Errorf("invalid packet type for command processing")
}
brain := am.GetBrain(packet.EntityID)
if brain == nil {
return fmt.Errorf("no brain found for NPC %d", packet.EntityID)
}
command, ok := packet.Data["command"].(string)
if !ok {
return fmt.Errorf("missing command in packet data")
}
switch command {
case "set_active":
if active, ok := packet.Data["active"].(bool); ok {
brain.SetActive(active)
}
case "add_hate":
if targetID, ok := packet.Data["target_id"].(float64); ok {
if hateValue, ok := packet.Data["hate_value"].(float64); ok {
brain.AddHate(int32(targetID), int32(hateValue))
}
}
case "clear_hate":
if targetID, ok := packet.Data["target_id"].(float64); ok {
brain.ClearHateForEntity(int32(targetID))
} else {
brain.ClearHate()
}
case "set_debug_level":
if level, ok := packet.Data["debug_level"].(float64); ok {
brain.SetDebugLevel(int8(level))
}
case "reset_stats":
// Reset brain statistics
stats := brain.GetStatistics()
*stats = *NewBrainStatistics()
default:
return fmt.Errorf("unknown command: %s", command)
}
return nil
}
// Debugging and Utilities
// 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)
}
// 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("-------------------")
}
// ToJSON converts AI statistics to JSON
func (stats *AIStatistics) ToJSON() ([]byte, error) {
return json.Marshal(stats)
}
// ToJSON converts brain statistics to JSON
func (stats *BrainStatistics) ToJSON() ([]byte, error) {
return json.Marshal(stats)
}