794 lines
22 KiB
Go
794 lines
22 KiB
Go
package world
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"eq2emu/internal/npc"
|
|
"eq2emu/internal/npc/ai"
|
|
"eq2emu/internal/database"
|
|
)
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
|
|
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]interface{} {
|
|
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]interface{}{
|
|
"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...)
|
|
} |