package world import ( "fmt" "sync" "eq2emu/internal/database" "eq2emu/internal/npc" "eq2emu/internal/npc/ai" ) // NPCManager manages NPCs for the world server type NPCManager struct { npcManager *npc.Manager aiManager *ai.AIManager database *database.Database world *World // Reference to world server // World-specific NPC tracking npcsByZone map[int32][]*npc.NPC // Zone ID -> NPCs activeCombat map[int32]bool // NPC ID -> in combat mutex sync.RWMutex } // NewNPCManager creates a new NPC manager for the world server func NewNPCManager(db *database.Database) *NPCManager { // Create adapters for the NPC system dbAdapter := &WorldNPCDatabaseAdapter{db: db} logAdapter := &WorldNPCLoggerAdapter{} // Create core NPC manager npcMgr := npc.NewManager(dbAdapter, logAdapter) // Create AI manager with logger aiMgr := ai.NewAIManager(logAdapter, nil) // No Lua interface for now return &NPCManager{ npcManager: npcMgr, aiManager: aiMgr, database: db, npcsByZone: make(map[int32][]*npc.NPC), activeCombat: make(map[int32]bool), } } // SetWorld sets the world server reference func (nm *NPCManager) SetWorld(world *World) { nm.world = world } // LoadNPCs loads all NPCs from database func (nm *NPCManager) LoadNPCs() error { fmt.Println("Loading NPC data...") // Initialize the NPC manager err := nm.npcManager.Initialize() if err != nil { return fmt.Errorf("failed to initialize NPC manager: %w", err) } // Load NPCs from database dbAdapter := &WorldNPCDatabaseAdapter{db: nm.database} dbNPCs, err := dbAdapter.LoadAllNPCs() if err != nil { fmt.Printf("Warning: Failed to load NPCs from database: %v\n", err) // Continue with test NPCs } else { // Add loaded NPCs to zone tracking and AI management for _, npc := range dbNPCs { if npc != nil { // Add to zone tracking (using zone ID from database or default) zoneID := int32(1) // TODO: Get actual zone ID from NPC nm.AddNPCToZone(zoneID, npc) // Create AI brain for this NPC (skip for now due to interface incompatibility) // TODO: Create proper NPC-to-AI adapter or implement missing methods // err := nm.aiManager.CreateBrainForNPC(npc, ai.BrainTypeDefault) // if err != nil { // fmt.Printf("Warning: Failed to create AI brain for NPC %s: %v\n", npc.GetName(), err) // } } } fmt.Printf("Loaded %d NPCs from database\n", len(dbNPCs)) } // Setup default/test NPCs for development err = nm.createTestNPCs() if err != nil { fmt.Printf("Warning: Failed to create test NPCs: %v\n", err) } stats := nm.npcManager.GetStatistics() fmt.Printf("Total NPCs loaded: %d\n", stats.TotalNPCs) return nil } // createTestNPCs creates some test NPCs for development func (nm *NPCManager) createTestNPCs() error { fmt.Println("Creating test NPCs for development...") // Create a few test NPCs using npc.NewNPC() testNPCs := []struct { id int32 name string level int8 zoneID int32 }{ {1001, "Test Goblin Scout", 5, 1}, {1002, "Test Orc Warrior", 10, 1}, {1003, "Test Forest Bear", 15, 2}, {1004, "Test Fire Elemental", 20, 3}, } for _, testData := range testNPCs { // Create new NPC instance newNPC := npc.NewNPC() if newNPC == nil { return fmt.Errorf("failed to create NPC %s", testData.name) } // Set basic NPC properties newNPC.SetID(testData.id) newNPC.SetName(testData.name) newNPC.SetLevel(int16(testData.level)) // Set some default stats based on level baseHP := int32(testData.level) * 50 newNPC.SetTotalHP(baseHP) newNPC.SetHP(baseHP) // Set position (placeholder coordinates) x := float32(100 + testData.id) y := float32(100 + testData.id) z := float32(50) newNPC.SetX(x) newNPC.SetY(y, false) // SetY takes bool parameter newNPC.SetZ(z) newNPC.SetHeadingFromFloat(0.0) // Add to zone tracking nm.AddNPCToZone(testData.zoneID, newNPC) // Create an AI brain for this NPC (skip for now due to interface incompatibility) // TODO: Create proper NPC-to-AI adapter or implement missing methods // err := nm.aiManager.CreateBrainForNPC(newNPC, ai.BrainTypeDefault) // if err != nil { // fmt.Printf("Warning: Failed to create AI brain for NPC %s: %v\n", testData.name, err) // } fmt.Printf("Created test NPC: %s (ID: %d, Level: %d, Zone: %d)\n", testData.name, testData.id, testData.level, testData.zoneID) } fmt.Printf("Successfully created %d test NPCs\n", len(testNPCs)) return nil } // ProcessNPCs processes all NPCs for one tick func (nm *NPCManager) ProcessNPCs() { nm.mutex.RLock() defer nm.mutex.RUnlock() // Process AI for all NPCs nm.aiManager.ProcessAllBrains() // Process NPC-specific logic stats := nm.npcManager.GetStatistics() if stats.NPCsInCombat > 0 { // Process combat NPCs nm.processCombatNPCs() } } // processCombatNPCs handles NPCs currently in combat func (nm *NPCManager) processCombatNPCs() { nm.mutex.RLock() defer nm.mutex.RUnlock() // Process each NPC that's in combat for npcID, inCombat := range nm.activeCombat { if !inCombat { continue } // Find the NPC in our zone tracking var combatNPC *npc.NPC for _, npcs := range nm.npcsByZone { for _, npc := range npcs { if npc != nil && npc.GetID() == npcID { combatNPC = npc break } } if combatNPC != nil { break } } if combatNPC == nil { // NPC not found, remove from combat tracking delete(nm.activeCombat, npcID) continue } // Skip if NPC is dead or incapacitated if !combatNPC.IsAlive() { delete(nm.activeCombat, npcID) continue } // Process combat AI nm.processCombatNPC(combatNPC) } } // processCombatNPC processes a single NPC in combat func (nm *NPCManager) processCombatNPC(npc *npc.NPC) { if npc == nil { return } npcID := npc.GetID() // TODO: Implement proper target system - GetTarget() returns int32 not Entity // For now, simulate combat without target validation // Check if NPC should still be in combat (placeholder logic) if !npc.IsAlive() { npc.InCombat(false) delete(nm.activeCombat, npcID) return } // Simplified combat processing (placeholder until target system is implemented) fmt.Printf("NPC %s (%d) processing combat AI (simplified)\n", npc.GetName(), npcID) // For now, just simulate combat for a few seconds then exit combat // TODO: Implement proper combat mechanics with target system // This is a placeholder to test the basic NPC system } // TODO: Combat helper methods commented out due to interface incompatibilities // These will be re-implemented once proper target system and interfaces are resolved // processCombatSpell handles NPC spell casting in combat // func (nm *NPCManager) processCombatSpell(npc *npc.NPC, target entity.Entity, spell Spell, distance float32) { // // Implementation deferred until interface issues are resolved // } // processCombatMelee handles NPC melee combat // func (nm *NPCManager) processCombatMelee(npc *npc.NPC, target entity.Entity, distance float32) { // // Implementation deferred until interface issues are resolved // } // processCombatMovement handles NPC movement in combat // func (nm *NPCManager) processCombatMovement(npc *npc.NPC, target entity.Entity, distance float32) { // // Implementation deferred until interface issues are resolved // } // OnNPCKilled handles when an NPC is killed func (nm *NPCManager) OnNPCKilled(npcID int32, killerCharacterID int32) { nm.mutex.Lock() delete(nm.activeCombat, npcID) nm.mutex.Unlock() // Trigger achievement events if nm.world != nil && nm.world.achievementMgr != nil { // Get NPC info npcInfo := nm.GetNPCInfo(npcID) if npcInfo != nil { // Trigger NPC kill event for achievements nm.world.OnNPCKill(killerCharacterID, npcID, int32(npcInfo.Level)) } } // Generate and distribute loot if nm.world != nil && nm.world.itemMgr != nil { npcInfo := nm.GetNPCInfo(npcID) if npcInfo != nil { // Generate loot for this NPC kill loot, err := nm.world.itemMgr.GenerateLootForNPC(npcID, int16(npcInfo.Level)) if err != nil { fmt.Printf("Failed to generate loot for NPC %d: %v\n", npcID, err) } else if len(loot) > 0 { // Create loot drops in the world (for now, just create basic drops) // TODO: Get NPC position for proper loot positioning x, y, z := float32(100), float32(100), float32(50) zoneID := npcInfo.ZoneID if zoneID == 0 { zoneID = 1 // Default zone } for _, item := range loot { err := nm.world.itemMgr.CreateWorldDrop(item.Details.ItemID, item.Details.Count, x, y, z, zoneID) if err != nil { fmt.Printf("Failed to create loot drop for item %d: %v\n", item.Details.ItemID, err) } } fmt.Printf("Generated %d loot items for NPC %d kill\n", len(loot), npcID) } } } fmt.Printf("NPC %d killed by character %d\n", npcID, killerCharacterID) } // OnNPCEnteredCombat handles when an NPC enters combat func (nm *NPCManager) OnNPCEnteredCombat(npcID int32, targetID int32) { nm.mutex.Lock() nm.activeCombat[npcID] = true nm.mutex.Unlock() fmt.Printf("NPC %d entered combat with target %d\n", npcID, targetID) } // OnNPCLeftCombat handles when an NPC leaves combat func (nm *NPCManager) OnNPCLeftCombat(npcID int32) { nm.mutex.Lock() delete(nm.activeCombat, npcID) nm.mutex.Unlock() fmt.Printf("NPC %d left combat\n", npcID) } // GetNPCInfo gets basic info about an NPC func (nm *NPCManager) GetNPCInfo(npcID int32) *NPCInfo { nm.mutex.RLock() defer nm.mutex.RUnlock() // First, check if we have the NPC in memory for _, npcs := range nm.npcsByZone { for _, npc := range npcs { if npc != nil && npc.GetID() == npcID { return &NPCInfo{ ID: npc.GetID(), Name: npc.GetName(), Level: npc.GetLevel(), ZoneID: 0, // TODO: Get zone ID from NPC or tracking } } } } // If not in memory, try to load from database row := nm.database.QueryRow(` SELECT id, name, level, zone_id FROM npcs WHERE id = ? `, npcID) var id, zoneID int32 var level int8 var name string err := row.Scan(&id, &name, &level, &zoneID) if err != nil { // NPC not found in database either, return default info return &NPCInfo{ ID: npcID, Level: 1, Name: fmt.Sprintf("Unknown_NPC_%d", npcID), ZoneID: 0, } } return &NPCInfo{ ID: id, Name: name, Level: level, ZoneID: zoneID, } } // GetNPCsByZone gets all NPCs in a zone func (nm *NPCManager) GetNPCsByZone(zoneID int32) []*npc.NPC { nm.mutex.RLock() defer nm.mutex.RUnlock() if npcs, exists := nm.npcsByZone[zoneID]; exists { // Return a copy to avoid concurrent modification result := make([]*npc.NPC, len(npcs)) copy(result, npcs) return result } return nil } // AddNPCToZone adds an NPC to a zone's tracking func (nm *NPCManager) AddNPCToZone(zoneID int32, npc *npc.NPC) { nm.mutex.Lock() defer nm.mutex.Unlock() nm.npcsByZone[zoneID] = append(nm.npcsByZone[zoneID], npc) } // RemoveNPCFromZone removes an NPC from zone tracking func (nm *NPCManager) RemoveNPCFromZone(zoneID int32, npcID int32) { nm.mutex.Lock() defer nm.mutex.Unlock() if npcs, exists := nm.npcsByZone[zoneID]; exists { for i, npc := range npcs { if npc != nil && npc.GetID() == npcID { // Remove NPC from slice nm.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...) break } } } } // GetStatistics returns NPC system statistics func (nm *NPCManager) GetStatistics() map[string]any { nm.mutex.RLock() defer nm.mutex.RUnlock() // Get base statistics from NPC manager stats := nm.npcManager.GetStatistics() // Add world-specific statistics totalZones := len(nm.npcsByZone) totalInCombat := len(nm.activeCombat) result := map[string]any{ "total_npcs": stats.TotalNPCs, "npcs_in_combat": stats.NPCsInCombat, "npcs_with_spells": stats.NPCsWithSpells, "npcs_with_skills": stats.NPCsWithSkills, "spell_cast_count": stats.SpellCastCount, "skill_usage_count": stats.SkillUsageCount, "runback_count": stats.RunbackCount, "average_aggro_radius": stats.AverageAggroRadius, "ai_strategy_counts": stats.AIStrategyCounts, "zones_with_npcs": totalZones, "world_npcs_in_combat": totalInCombat, } return result } // Shutdown gracefully shuts down the NPC manager func (nm *NPCManager) Shutdown() { fmt.Println("Shutting down NPC manager...") nm.mutex.Lock() defer nm.mutex.Unlock() // Clear all tracking nm.npcsByZone = make(map[int32][]*npc.NPC) nm.activeCombat = make(map[int32]bool) // TODO: Shutdown AI manager when shutdown method is available // nm.aiManager.Shutdown() fmt.Println("NPC manager shutdown complete") } // NPCInfo represents basic information about an NPC type NPCInfo struct { ID int32 Name string Level int8 ZoneID int32 } // WorldNPCDatabaseAdapter adapts the world database for NPC use type WorldNPCDatabaseAdapter struct { db *database.Database } // LoadAllNPCs implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) LoadAllNPCs() ([]*npc.NPC, error) { fmt.Println("Loading NPCs from database...") rows, err := wdb.db.Query(` SELECT id, name, level, max_level, race, model_type, size, hp, power, x, y, z, heading, respawn_time, zone_id, aggro_radius, ai_strategy, loot_table_id, merchant_type, randomize_appearance, show_name, show_level, targetable, show_command_icon, display_hand_icon, faction_id FROM npcs ORDER BY zone_id, id `) if err != nil { return nil, fmt.Errorf("failed to query NPCs: %w", err) } defer rows.Close() var npcs []*npc.NPC for rows.Next() { newNPC := npc.NewNPC() if newNPC == nil { continue // Skip if we can't create NPC } var id, maxLevel, race, modelType, size, hp, power int32 var x, y, z, heading, aggroRadius float32 var respawnTime, zoneID, aiStrategy, lootTableID, merchantType int32 var randomizeAppearance, showName, showLevel, targetable, showCommandIcon, displayHandIcon, factionID int32 var name string var level int8 err := rows.Scan(&id, &name, &level, &maxLevel, &race, &modelType, &size, &hp, &power, &x, &y, &z, &heading, &respawnTime, &zoneID, &aggroRadius, &aiStrategy, &lootTableID, &merchantType, &randomizeAppearance, &showName, &showLevel, &targetable, &showCommandIcon, &displayHandIcon, &factionID) if err != nil { fmt.Printf("Error scanning NPC row: %v\n", err) continue } // Set NPC properties newNPC.SetID(id) newNPC.SetName(name) newNPC.SetLevel(int16(level)) newNPC.SetTotalHP(hp) newNPC.SetHP(hp) newNPC.SetX(x) newNPC.SetY(y, false) newNPC.SetZ(z) newNPC.SetHeadingFromFloat(heading) // Set additional properties if the NPC supports them // TODO: Set additional properties like race, model type, etc. // This would require checking what methods are available on the NPC type npcs = append(npcs, newNPC) fmt.Printf("Loaded NPC: %s (ID: %d, Level: %d, Zone: %d)\n", name, id, level, zoneID) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC rows: %w", err) } fmt.Printf("Successfully loaded %d NPCs from database\n", len(npcs)) return npcs, nil } // SaveNPC implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) SaveNPC(npcEntity *npc.NPC) error { if npcEntity == nil { return fmt.Errorf("cannot save nil NPC") } // Extract NPC properties for saving id := npcEntity.GetID() name := npcEntity.GetName() level := npcEntity.GetLevel() hp := npcEntity.GetTotalHP() x := npcEntity.GetX() y := npcEntity.GetY() z := npcEntity.GetZ() heading := npcEntity.GetHeading() // Insert or update NPC in database _, err := wdb.db.Exec(` INSERT OR REPLACE INTO npcs (id, name, level, max_level, race, model_type, size, hp, power, x, y, z, heading, respawn_time, zone_id, aggro_radius, ai_strategy, loot_table_id, merchant_type, randomize_appearance, show_name, show_level, targetable, show_command_icon, display_hand_icon, faction_id, created_date, last_modified) VALUES (?, ?, ?, ?, 0, 0, 32, ?, ?, ?, ?, ?, ?, 300, 0, 10.0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, strftime('%s', 'now'), strftime('%s', 'now')) `, id, name, level, level, hp, hp, x, y, z, heading) if err != nil { return fmt.Errorf("failed to save NPC %d: %w", id, err) } fmt.Printf("Saved NPC: %s (ID: %d) to database\n", name, id) return nil } // DeleteNPC implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) DeleteNPC(npcID int32) error { // Start transaction to delete NPC and related data tx, err := wdb.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete related data first (due to foreign key constraints) _, err = tx.Exec("DELETE FROM npc_spells WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC spells: %w", err) } _, err = tx.Exec("DELETE FROM npc_skills WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC skills: %w", err) } _, err = tx.Exec("DELETE FROM npc_loot WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC loot: %w", err) } // Delete the NPC itself result, err := tx.Exec("DELETE FROM npcs WHERE id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete NPC: %w", err) } // Check if NPC was actually deleted rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to check rows affected: %w", err) } if rowsAffected == 0 { return fmt.Errorf("NPC %d not found", npcID) } // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } fmt.Printf("Deleted NPC %d from database\n", npcID) return nil } // LoadNPCSpells implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) LoadNPCSpells(npcID int32) ([]*npc.NPCSpell, error) { rows, err := wdb.db.Query(` SELECT npc_id, spell_id, tier, hp_percentage, priority, cast_type, recast_delay FROM npc_spells WHERE npc_id = ? ORDER BY priority DESC, hp_percentage DESC `, npcID) if err != nil { return nil, fmt.Errorf("failed to query NPC spells: %w", err) } defer rows.Close() var spells []*npc.NPCSpell for rows.Next() { spell := npc.NewNPCSpell() var npcIDDB, spellID, tier, hpPercentage, priority, castType, recastDelay int32 err := rows.Scan(&npcIDDB, &spellID, &tier, &hpPercentage, &priority, &castType, &recastDelay) if err != nil { fmt.Printf("Error scanning NPC spell row: %v\n", err) continue } // Set spell properties (using available methods from NPCSpell) // TODO: Set spell properties based on what methods are available // spell.SetSpellID(spellID) // spell.SetTier(tier) // spell.SetHPPercentage(hpPercentage) // spell.SetPriority(priority) // spell.SetCastType(castType) // spell.SetRecastDelay(recastDelay) spells = append(spells, spell) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC spell rows: %w", err) } return spells, nil } // SaveNPCSpells implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) SaveNPCSpells(npcID int32, spells []*npc.NPCSpell) error { // Start transaction tx, err := wdb.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing spells for this NPC _, err = tx.Exec("DELETE FROM npc_spells WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete existing NPC spells: %w", err) } // Insert new spells for _, spell := range spells { if spell == nil { continue } // TODO: Get spell properties from NPCSpell object // For now, use placeholder values _, err = tx.Exec(` INSERT INTO npc_spells (npc_id, spell_id, tier, hp_percentage, priority, cast_type, recast_delay) VALUES (?, ?, ?, ?, ?, ?, ?) `, npcID, 1, 1, 100, 1, 0, 5) // Placeholder values if err != nil { return fmt.Errorf("failed to save NPC spell: %w", err) } } // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } fmt.Printf("Saved %d spells for NPC %d\n", len(spells), npcID) return nil } // LoadNPCSkills implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) LoadNPCSkills(npcID int32) (map[string]*npc.Skill, error) { rows, err := wdb.db.Query(` SELECT npc_id, skill_name, skill_value, max_value FROM npc_skills WHERE npc_id = ? ORDER BY skill_name `, npcID) if err != nil { return nil, fmt.Errorf("failed to query NPC skills: %w", err) } defer rows.Close() skills := make(map[string]*npc.Skill) for rows.Next() { var npcIDDB, skillValue, maxValue int32 var skillName string err := rows.Scan(&npcIDDB, &skillName, &skillValue, &maxValue) if err != nil { fmt.Printf("Error scanning NPC skill row: %v\n", err) continue } // Create skill object skill := npc.NewSkill(0, skillName, int16(skillValue), int16(maxValue)) skills[skillName] = skill } if err := rows.Err(); err != nil { return nil, fmt.Errorf("error iterating NPC skill rows: %w", err) } return skills, nil } // SaveNPCSkills implements npc.Database interface func (wdb *WorldNPCDatabaseAdapter) SaveNPCSkills(npcID int32, skills map[string]*npc.Skill) error { // Start transaction tx, err := wdb.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Delete existing skills for this NPC _, err = tx.Exec("DELETE FROM npc_skills WHERE npc_id = ?", npcID) if err != nil { return fmt.Errorf("failed to delete existing NPC skills: %w", err) } // Insert new skills for skillName, skill := range skills { if skill == nil { continue } // Get skill values (need to access directly since GetMaxVal doesn't exist) currentVal := skill.GetCurrentVal() // TODO: Add GetMaxVal method to Skill struct or access MaxVal field maxVal := int16(100) // Placeholder - should be skill.MaxVal when accessible _, err = tx.Exec(` INSERT INTO npc_skills (npc_id, skill_name, skill_value, max_value) VALUES (?, ?, ?, ?) `, npcID, skillName, currentVal, maxVal) if err != nil { return fmt.Errorf("failed to save NPC skill %s: %w", skillName, err) } } // Commit transaction if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } fmt.Printf("Saved %d skills for NPC %d\n", len(skills), npcID) return nil } // WorldNPCLoggerAdapter adapts world logging for NPC use type WorldNPCLoggerAdapter struct{} // LogInfo implements npc.Logger interface func (wl *WorldNPCLoggerAdapter) LogInfo(message string, args ...any) { fmt.Printf("[NPC] INFO: "+message+"\n", args...) } // LogError implements npc.Logger interface func (wl *WorldNPCLoggerAdapter) LogError(message string, args ...any) { fmt.Printf("[NPC] ERROR: "+message+"\n", args...) } // LogDebug implements npc.Logger interface func (wl *WorldNPCLoggerAdapter) LogDebug(message string, args ...any) { fmt.Printf("[NPC] DEBUG: "+message+"\n", args...) } // LogWarning implements npc.Logger interface func (wl *WorldNPCLoggerAdapter) LogWarning(message string, args ...any) { fmt.Printf("[NPC] WARNING: "+message+"\n", args...) }