package npc import ( "fmt" "math/rand" "strings" "sync" ) // Manager provides high-level management of the NPC system type Manager struct { npcs map[int32]*NPC // NPCs indexed by ID npcsByZone map[int32][]*NPC // NPCs indexed by zone ID npcsByAppearance map[int32][]*NPC // NPCs indexed by appearance ID database Database // Database interface logger Logger // Logger interface spellManager SpellManager // Spell system interface skillManager SkillManager // Skill system interface appearanceManager AppearanceManager // Appearance system interface mutex sync.RWMutex // Thread safety // Statistics totalNPCs int64 npcsInCombat int64 spellCastCount int64 skillUsageCount int64 runbackCount int64 aiStrategyCounts map[int8]int64 // Configuration maxNPCs int32 defaultAggroRadius float32 enableAI bool } // NewManager creates a new NPC manager func NewManager(database Database, logger Logger) *Manager { return &Manager{ npcs: make(map[int32]*NPC), npcsByZone: make(map[int32][]*NPC), npcsByAppearance: make(map[int32][]*NPC), database: database, logger: logger, aiStrategyCounts: make(map[int8]int64), maxNPCs: 10000, // Default limit defaultAggroRadius: DefaultAggroRadius, enableAI: true, } } // Initialize loads NPCs from database and sets up the system func (m *Manager) Initialize() error { if m.logger != nil { m.logger.LogInfo("Initializing NPC manager...") } if m.database == nil { if m.logger != nil { m.logger.LogWarning("No database provided, starting with empty NPC list") } return nil } // Load NPCs from database npcs, err := m.database.LoadAllNPCs() if err != nil { return fmt.Errorf("failed to load NPCs from database: %w", err) } for _, npc := range npcs { if err := m.addNPCInternal(npc); err != nil { if m.logger != nil { m.logger.LogError("Failed to add NPC %d: %v", npc.GetNPCID(), err) } } } if m.logger != nil { m.logger.LogInfo("Loaded %d NPCs from database", len(npcs)) } return nil } // AddNPC adds a new NPC to the system func (m *Manager) AddNPC(npc *NPC) error { if npc == nil { return fmt.Errorf("NPC cannot be nil") } if !npc.IsValid() { return fmt.Errorf("NPC is not valid: %s", npc.String()) } m.mutex.Lock() defer m.mutex.Unlock() if len(m.npcs) >= int(m.maxNPCs) { return fmt.Errorf("maximum NPC limit reached (%d)", m.maxNPCs) } return m.addNPCInternal(npc) } // addNPCInternal adds an NPC without locking (internal use) func (m *Manager) addNPCInternal(npc *NPC) error { npcID := npc.GetNPCID() // Check for duplicate ID if _, exists := m.npcs[npcID]; exists { return fmt.Errorf("NPC with ID %d already exists", npcID) } // Add to main index m.npcs[npcID] = npc // Add to zone index // TODO: Add zone support when Entity.GetZoneID() is available // if npc.Entity != nil { // zoneID := npc.Entity.GetZoneID() // m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) // } // Add to appearance index appearanceID := npc.GetAppearanceID() m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc) // Update statistics m.totalNPCs++ strategy := npc.GetAIStrategy() m.aiStrategyCounts[strategy]++ // Save to database if available if m.database != nil { if err := m.database.SaveNPC(npc); err != nil { // Remove from indexes if database save failed delete(m.npcs, npcID) m.removeFromZoneIndex(npc) m.removeFromAppearanceIndex(npc) m.totalNPCs-- m.aiStrategyCounts[strategy]-- return fmt.Errorf("failed to save NPC to database: %w", err) } } if m.logger != nil { m.logger.LogInfo("Added NPC %d: %s", npcID, npc.String()) } return nil } // GetNPC retrieves an NPC by ID func (m *Manager) GetNPC(id int32) *NPC { m.mutex.RLock() defer m.mutex.RUnlock() return m.npcs[id] } // GetNPCsByZone retrieves all NPCs in a specific zone func (m *Manager) GetNPCsByZone(zoneID int32) []*NPC { m.mutex.RLock() defer m.mutex.RUnlock() npcs := m.npcsByZone[zoneID] result := make([]*NPC, len(npcs)) copy(result, npcs) return result } // GetNPCsByAppearance retrieves all NPCs with a specific appearance func (m *Manager) GetNPCsByAppearance(appearanceID int32) []*NPC { m.mutex.RLock() defer m.mutex.RUnlock() npcs := m.npcsByAppearance[appearanceID] result := make([]*NPC, len(npcs)) copy(result, npcs) return result } // RemoveNPC removes an NPC from the system func (m *Manager) RemoveNPC(id int32) error { m.mutex.Lock() defer m.mutex.Unlock() npc, exists := m.npcs[id] if !exists { return fmt.Errorf("NPC with ID %d does not exist", id) } // Remove from database first if available if m.database != nil { if err := m.database.DeleteNPC(id); err != nil { return fmt.Errorf("failed to delete NPC from database: %w", err) } } // Remove from all indexes delete(m.npcs, id) m.removeFromZoneIndex(npc) m.removeFromAppearanceIndex(npc) // Update statistics m.totalNPCs-- strategy := npc.GetAIStrategy() if count := m.aiStrategyCounts[strategy]; count > 0 { m.aiStrategyCounts[strategy]-- } if m.logger != nil { m.logger.LogInfo("Removed NPC %d", id) } return nil } // UpdateNPC updates an existing NPC func (m *Manager) UpdateNPC(npc *NPC) error { if npc == nil { return fmt.Errorf("NPC cannot be nil") } if !npc.IsValid() { return fmt.Errorf("NPC is not valid: %s", npc.String()) } m.mutex.Lock() defer m.mutex.Unlock() npcID := npc.GetNPCID() oldNPC, exists := m.npcs[npcID] if !exists { return fmt.Errorf("NPC with ID %d does not exist", npcID) } // Update indexes if zone or appearance changed // TODO: Add zone support when Entity.GetZoneID() is available // if npc.Entity != nil && oldNPC.Entity != nil { // if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() { // m.removeFromZoneIndex(oldNPC) // zoneID := npc.Entity.GetZoneID() // m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc) // } // } if npc.GetAppearanceID() != oldNPC.GetAppearanceID() { m.removeFromAppearanceIndex(oldNPC) appearanceID := npc.GetAppearanceID() m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc) } // Update AI strategy statistics oldStrategy := oldNPC.GetAIStrategy() newStrategy := npc.GetAIStrategy() if oldStrategy != newStrategy { if count := m.aiStrategyCounts[oldStrategy]; count > 0 { m.aiStrategyCounts[oldStrategy]-- } m.aiStrategyCounts[newStrategy]++ } // Update main index m.npcs[npcID] = npc // Save to database if available if m.database != nil { if err := m.database.SaveNPC(npc); err != nil { return fmt.Errorf("failed to save NPC to database: %w", err) } } if m.logger != nil { m.logger.LogInfo("Updated NPC %d: %s", npcID, npc.String()) } return nil } // CreateNPCFromTemplate creates a new NPC from an existing template func (m *Manager) CreateNPCFromTemplate(templateID, newID int32) (*NPC, error) { template := m.GetNPC(templateID) if template == nil { return nil, fmt.Errorf("template NPC with ID %d not found", templateID) } // Create new NPC from template newNPC := NewNPCFromExisting(template) newNPC.SetNPCID(newID) // Add to system if err := m.AddNPC(newNPC); err != nil { return nil, fmt.Errorf("failed to add new NPC: %w", err) } return newNPC, nil } // GetRandomNPCByAppearance returns a random NPC with the specified appearance func (m *Manager) GetRandomNPCByAppearance(appearanceID int32) *NPC { npcs := m.GetNPCsByAppearance(appearanceID) if len(npcs) == 0 { return nil } return npcs[rand.Intn(len(npcs))] } // ProcessCombat handles combat processing for all NPCs func (m *Manager) ProcessCombat() { m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { // TODO: Add combat status check when GetInCombat() is available // if npc.Entity != nil && npc.Entity.GetInCombat() { if npc.Entity != nil { npcs = append(npcs, npc) } } m.mutex.RUnlock() // Process combat for each NPC in combat for _, npc := range npcs { npc.ProcessCombat() } // Update combat statistics m.mutex.Lock() m.npcsInCombat = int64(len(npcs)) m.mutex.Unlock() } // ProcessAI handles AI processing for all NPCs func (m *Manager) ProcessAI() { if !m.enableAI { return } m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { npcs = append(npcs, npc) } m.mutex.RUnlock() // Process AI for each NPC for _, npc := range npcs { if brain := npc.GetBrain(); brain != nil && brain.IsActive() { if err := brain.Think(); err != nil && m.logger != nil { m.logger.LogError("AI brain error for NPC %d: %v", npc.GetNPCID(), err) } } } } // ProcessMovement handles movement processing for all NPCs func (m *Manager) ProcessMovement() { m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { npcs = append(npcs, npc) } m.mutex.RUnlock() // Process movement for each NPC for _, npc := range npcs { // Check pause timer if npc.IsPauseMovementTimerActive() { continue } // Handle runback if needed if npc.callRunback && npc.GetRunbackLocation() != nil { npc.callRunback = false npc.Runback(0, true) m.mutex.Lock() m.runbackCount++ m.mutex.Unlock() } } } // GetStatistics returns NPC system statistics func (m *Manager) GetStatistics() *NPCStatistics { m.mutex.RLock() defer m.mutex.RUnlock() // Create AI strategy counts by name aiCounts := make(map[string]int) for strategy, count := range m.aiStrategyCounts { switch strategy { case AIStrategyBalanced: aiCounts["balanced"] = int(count) case AIStrategyOffensive: aiCounts["offensive"] = int(count) case AIStrategyDefensive: aiCounts["defensive"] = int(count) default: aiCounts[fmt.Sprintf("unknown_%d", strategy)] = int(count) } } // Calculate average aggro radius var totalAggro float32 npcCount := 0 for _, npc := range m.npcs { totalAggro += npc.GetAggroRadius() npcCount++ } var avgAggro float32 if npcCount > 0 { avgAggro = totalAggro / float32(npcCount) } // Count NPCs with spells and skills npcsWithSpells := 0 npcsWithSkills := 0 for _, npc := range m.npcs { if npc.HasSpells() { npcsWithSpells++ } if len(npc.skills) > 0 { npcsWithSkills++ } } return &NPCStatistics{ TotalNPCs: int(m.totalNPCs), NPCsInCombat: int(m.npcsInCombat), NPCsWithSpells: npcsWithSpells, NPCsWithSkills: npcsWithSkills, AIStrategyCounts: aiCounts, SpellCastCount: m.spellCastCount, SkillUsageCount: m.skillUsageCount, RunbackCount: m.runbackCount, AverageAggroRadius: avgAggro, } } // ValidateAllNPCs validates all NPCs in the system func (m *Manager) ValidateAllNPCs() []string { m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { npcs = append(npcs, npc) } m.mutex.RUnlock() var issues []string for _, npc := range npcs { if !npc.IsValid() { issues = append(issues, fmt.Sprintf("NPC %d is invalid: %s", npc.GetNPCID(), npc.String())) } } return issues } // ProcessCommand handles NPC-related commands func (m *Manager) ProcessCommand(command string, args []string) (string, error) { switch command { case "stats": return m.handleStatsCommand(args) case "validate": return m.handleValidateCommand(args) case "list": return m.handleListCommand(args) case "info": return m.handleInfoCommand(args) case "create": return m.handleCreateCommand(args) case "remove": return m.handleRemoveCommand(args) case "search": return m.handleSearchCommand(args) case "combat": return m.handleCombatCommand(args) default: return "", fmt.Errorf("unknown NPC command: %s", command) } } // Command handlers func (m *Manager) handleStatsCommand(args []string) (string, error) { stats := m.GetStatistics() result := "NPC System Statistics:\n" result += fmt.Sprintf("Total NPCs: %d\n", stats.TotalNPCs) result += fmt.Sprintf("NPCs in Combat: %d\n", stats.NPCsInCombat) result += fmt.Sprintf("NPCs with Spells: %d\n", stats.NPCsWithSpells) result += fmt.Sprintf("NPCs with Skills: %d\n", stats.NPCsWithSkills) result += fmt.Sprintf("Average Aggro Radius: %.2f\n", stats.AverageAggroRadius) result += fmt.Sprintf("Spell Casts: %d\n", stats.SpellCastCount) result += fmt.Sprintf("Skill Uses: %d\n", stats.SkillUsageCount) result += fmt.Sprintf("Runbacks: %d\n", stats.RunbackCount) if len(stats.AIStrategyCounts) > 0 { result += "\nAI Strategy Distribution:\n" for strategy, count := range stats.AIStrategyCounts { result += fmt.Sprintf(" %s: %d\n", strategy, count) } } return result, nil } func (m *Manager) handleValidateCommand(args []string) (string, error) { issues := m.ValidateAllNPCs() if len(issues) == 0 { return "All NPCs are valid.", nil } result := fmt.Sprintf("Found %d issues with NPCs:\n", len(issues)) for i, issue := range issues { if i >= 10 { // Limit output result += "... (and more)\n" break } result += fmt.Sprintf("%d. %s\n", i+1, issue) } return result, nil } func (m *Manager) handleListCommand(args []string) (string, error) { m.mutex.RLock() npcs := make([]*NPC, 0, len(m.npcs)) for _, npc := range m.npcs { npcs = append(npcs, npc) } m.mutex.RUnlock() if len(npcs) == 0 { return "No NPCs loaded.", nil } result := fmt.Sprintf("NPCs (%d):\n", len(npcs)) count := 0 for _, npc := range npcs { if count >= 20 { // Limit output result += "... (and more)\n" break } result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String()) count++ } return result, nil } func (m *Manager) handleInfoCommand(args []string) (string, error) { if len(args) == 0 { return "", fmt.Errorf("NPC ID required") } var npcID int32 if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil { return "", fmt.Errorf("invalid NPC ID: %s", args[0]) } npc := m.GetNPC(npcID) if npc == nil { return fmt.Sprintf("NPC %d not found.", npcID), nil } result := fmt.Sprintf("NPC Information:\n") result += fmt.Sprintf("ID: %d\n", npc.GetNPCID()) result += fmt.Sprintf("Appearance ID: %d\n", npc.GetAppearanceID()) result += fmt.Sprintf("AI Strategy: %d\n", npc.GetAIStrategy()) result += fmt.Sprintf("Cast Percentage: %d%%\n", npc.GetCastPercentage()) result += fmt.Sprintf("Aggro Radius: %.2f\n", npc.GetAggroRadius()) result += fmt.Sprintf("Has Spells: %v\n", npc.HasSpells()) result += fmt.Sprintf("Running Back: %v\n", npc.IsRunningBack()) if npc.Entity != nil { result += fmt.Sprintf("Name: %s\n", npc.Entity.GetName()) result += fmt.Sprintf("Level: %d\n", npc.Entity.GetLevel()) // TODO: Add zone and combat status when methods are available // result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID()) // result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat()) } return result, nil } func (m *Manager) handleCreateCommand(args []string) (string, error) { if len(args) < 2 { return "", fmt.Errorf("usage: create ") } var templateID, newID int32 if _, err := fmt.Sscanf(args[0], "%d", &templateID); err != nil { return "", fmt.Errorf("invalid template ID: %s", args[0]) } if _, err := fmt.Sscanf(args[1], "%d", &newID); err != nil { return "", fmt.Errorf("invalid new ID: %s", args[1]) } _, err := m.CreateNPCFromTemplate(templateID, newID) if err != nil { return "", fmt.Errorf("failed to create NPC: %w", err) } return fmt.Sprintf("Successfully created NPC %d from template %d", newID, templateID), nil } func (m *Manager) handleRemoveCommand(args []string) (string, error) { if len(args) == 0 { return "", fmt.Errorf("NPC ID required") } var npcID int32 if _, err := fmt.Sscanf(args[0], "%d", &npcID); err != nil { return "", fmt.Errorf("invalid NPC ID: %s", args[0]) } if err := m.RemoveNPC(npcID); err != nil { return "", fmt.Errorf("failed to remove NPC: %w", err) } return fmt.Sprintf("Successfully removed NPC %d", npcID), nil } func (m *Manager) handleSearchCommand(args []string) (string, error) { if len(args) == 0 { return "", fmt.Errorf("search term required") } searchTerm := strings.ToLower(args[0]) m.mutex.RLock() var results []*NPC for _, npc := range m.npcs { if npc.Entity != nil { name := strings.ToLower(npc.Entity.GetName()) if strings.Contains(name, searchTerm) { results = append(results, npc) } } } m.mutex.RUnlock() if len(results) == 0 { return fmt.Sprintf("No NPCs found matching '%s'.", args[0]), nil } result := fmt.Sprintf("Found %d NPCs matching '%s':\n", len(results), args[0]) for i, npc := range results { if i >= 20 { // Limit output result += "... (and more)\n" break } result += fmt.Sprintf(" %d: %s\n", npc.GetNPCID(), npc.String()) } return result, nil } func (m *Manager) handleCombatCommand(args []string) (string, error) { result := "Combat Processing Status:\n" result += fmt.Sprintf("NPCs in Combat: %d\n", m.npcsInCombat) result += fmt.Sprintf("Total Spell Casts: %d\n", m.spellCastCount) result += fmt.Sprintf("Total Runbacks: %d\n", m.runbackCount) return result, nil } // Helper methods func (m *Manager) removeFromZoneIndex(npc *NPC) { if npc.Entity == nil { return } // TODO: Implement zone index removal when Entity.GetZoneID() is available // zoneID := npc.Entity.GetZoneID() // npcs := m.npcsByZone[zoneID] // for i, n := range npcs { // if n == npc { // // Remove from slice // m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...) // break // } // } // // Clean up empty slices // if len(m.npcsByZone[zoneID]) == 0 { // delete(m.npcsByZone, zoneID) // } } func (m *Manager) removeFromAppearanceIndex(npc *NPC) { appearanceID := npc.GetAppearanceID() npcs := m.npcsByAppearance[appearanceID] for i, n := range npcs { if n == npc { // Remove from slice m.npcsByAppearance[appearanceID] = append(npcs[:i], npcs[i+1:]...) break } } // Clean up empty slices if len(m.npcsByAppearance[appearanceID]) == 0 { delete(m.npcsByAppearance, appearanceID) } } // SetManagers sets the external system managers func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) { m.mutex.Lock() defer m.mutex.Unlock() m.spellManager = spellMgr m.skillManager = skillMgr m.appearanceManager = appearanceMgr } // Configuration methods func (m *Manager) SetMaxNPCs(max int32) { m.mutex.Lock() defer m.mutex.Unlock() m.maxNPCs = max } func (m *Manager) SetDefaultAggroRadius(radius float32) { m.mutex.Lock() defer m.mutex.Unlock() m.defaultAggroRadius = radius } func (m *Manager) SetAIEnabled(enabled bool) { m.mutex.Lock() defer m.mutex.Unlock() m.enableAI = enabled } // GetNPCCount returns the total number of NPCs func (m *Manager) GetNPCCount() int32 { m.mutex.RLock() defer m.mutex.RUnlock() return int32(len(m.npcs)) } // Shutdown gracefully shuts down the manager func (m *Manager) Shutdown() { if m.logger != nil { m.logger.LogInfo("Shutting down NPC manager...") } // Stop all AI brains m.mutex.Lock() for _, npc := range m.npcs { if brain := npc.GetBrain(); brain != nil { brain.SetActive(false) } } // Clear all data m.npcs = make(map[int32]*NPC) m.npcsByZone = make(map[int32][]*NPC) m.npcsByAppearance = make(map[int32][]*NPC) m.mutex.Unlock() }