eq2go/internal/world/npc_manager.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...)
}