451 lines
11 KiB
Go
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)
|
|
} |