package npc import ( "encoding/json" "fmt" "math" "math/rand" "strings" "sync" "time" "eq2emu/internal/entity" ) // AI Strategy constants const ( AIStrategyBalanced int8 = 1 AIStrategyOffensive int8 = 2 AIStrategyDefensive int8 = 3 ) // Randomize Appearances constants const ( RandomizeGender int32 = 1 RandomizeRace int32 = 2 RandomizeModelType int32 = 4 RandomizeFacialHairType int32 = 8 RandomizeHairType int32 = 16 RandomizeWingType int32 = 64 RandomizeCheekType int32 = 128 RandomizeChinType int32 = 256 RandomizeEarType int32 = 512 RandomizeEyeBrowType int32 = 1024 RandomizeEyeType int32 = 2048 RandomizeLipType int32 = 4096 RandomizeNoseType int32 = 8192 RandomizeEyeColor int32 = 16384 RandomizeHairColor1 int32 = 32768 RandomizeHairColor2 int32 = 65536 RandomizeHairHighlight int32 = 131072 RandomizeHairFaceColor int32 = 262144 RandomizeHairFaceHigh int32 = 524288 RandomizeHairTypeColor int32 = 1048576 RandomizeHairTypeHigh int32 = 2097152 RandomizeSkinColor int32 = 4194304 RandomizeWingColor1 int32 = 8388608 RandomizeWingColor2 int32 = 16777216 RandomizeAll int32 = 33554431 ) // Pet Type constants const ( PetTypeCombat int8 = 1 PetTypeCharmed int8 = 2 PetTypeDeity int8 = 3 PetTypeCosmetic int8 = 4 PetTypeDumbfire int8 = 5 ) // Cast Type constants const ( CastOnSpawn int8 = 0 CastOnAggro int8 = 1 MaxCastTypes int8 = 2 ) // Default values const ( DefaultCastPercentage int8 = 25 DefaultAggroRadius float32 = 10.0 DefaultRunbackSpeed float32 = 2.0 MaxSkillBonuses int = 100 MaxNPCSpells int = 50 MaxPauseTime int32 = 300000 // 5 minutes max pause ) // NPC validation constants const ( MinNPCLevel int8 = 1 MaxNPCLevel int8 = 100 MaxNPCNameLen int = 64 MinAppearanceID int32 = 0 MaxAppearanceID int32 = 999999 ) // Combat constants const ( RunbackDistanceThreshold float32 = 5.0 HPRatioMin int8 = -100 HPRatioMax int8 = 100 DefaultMaxPetLevel int8 = 20 ) // Color randomization constants const ( ColorRandomMin int8 = 0 ColorRandomMax int8 = 127 // Max value for int8 ColorVariation int8 = 30 ) // Movement constants const ( DefaultPauseCheckMS int32 = 100 RunbackCheckMS int32 = 250 ) // NPCSpell represents a spell configuration for NPCs type NPCSpell struct { ListID int32 // Spell list identifier SpellID int32 // Spell ID from master spell list Tier int8 // Spell tier CastOnSpawn bool // Cast when NPC spawns CastOnInitialAggro bool // Cast when first entering combat RequiredHPRatio int8 // HP ratio requirement for casting (-100 to 100) mutex sync.RWMutex } // NewNPCSpell creates a new NPCSpell func NewNPCSpell() *NPCSpell { return &NPCSpell{ ListID: 0, SpellID: 0, Tier: 1, CastOnSpawn: false, CastOnInitialAggro: false, RequiredHPRatio: 0, } } // Copy creates a deep copy of the NPCSpell func (ns *NPCSpell) Copy() *NPCSpell { ns.mutex.RLock() defer ns.mutex.RUnlock() return &NPCSpell{ ListID: ns.ListID, SpellID: ns.SpellID, Tier: ns.Tier, CastOnSpawn: ns.CastOnSpawn, CastOnInitialAggro: ns.CastOnInitialAggro, RequiredHPRatio: ns.RequiredHPRatio, } } // Getters func (ns *NPCSpell) GetListID() int32 { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.ListID } func (ns *NPCSpell) GetSpellID() int32 { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.SpellID } func (ns *NPCSpell) GetTier() int8 { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.Tier } func (ns *NPCSpell) GetCastOnSpawn() bool { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.CastOnSpawn } func (ns *NPCSpell) GetCastOnInitialAggro() bool { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.CastOnInitialAggro } func (ns *NPCSpell) GetRequiredHPRatio() int8 { ns.mutex.RLock() defer ns.mutex.RUnlock() return ns.RequiredHPRatio } // Setters func (ns *NPCSpell) SetListID(id int32) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.ListID = id } func (ns *NPCSpell) SetSpellID(id int32) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.SpellID = id } func (ns *NPCSpell) SetTier(tier int8) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.Tier = tier } func (ns *NPCSpell) SetCastOnSpawn(cast bool) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.CastOnSpawn = cast } func (ns *NPCSpell) SetCastOnInitialAggro(cast bool) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.CastOnInitialAggro = cast } func (ns *NPCSpell) SetRequiredHPRatio(ratio int8) { ns.mutex.Lock() defer ns.mutex.Unlock() ns.RequiredHPRatio = ratio } // SkillBonus represents a skill bonus from spells type SkillBonus struct { SpellID int32 // Spell providing the bonus Skills map[int32]*SkillBonusValue // Map of skill ID to bonus value mutex sync.RWMutex } // SkillBonusValue represents the actual bonus value for a skill type SkillBonusValue struct { SkillID int32 // Skill receiving the bonus Value float32 // Bonus amount } // NewSkillBonus creates a new SkillBonus func NewSkillBonus(spellID int32) *SkillBonus { return &SkillBonus{ SpellID: spellID, Skills: make(map[int32]*SkillBonusValue), } } // AddSkill adds a skill bonus func (sb *SkillBonus) AddSkill(skillID int32, value float32) { sb.mutex.Lock() defer sb.mutex.Unlock() sb.Skills[skillID] = &SkillBonusValue{ SkillID: skillID, Value: value, } } // RemoveSkill removes a skill bonus func (sb *SkillBonus) RemoveSkill(skillID int32) bool { sb.mutex.Lock() defer sb.mutex.Unlock() if _, exists := sb.Skills[skillID]; exists { delete(sb.Skills, skillID) return true } return false } // GetSkills returns a copy of all skill bonuses func (sb *SkillBonus) GetSkills() map[int32]*SkillBonusValue { sb.mutex.RLock() defer sb.mutex.RUnlock() result := make(map[int32]*SkillBonusValue) for id, bonus := range sb.Skills { result[id] = &SkillBonusValue{ SkillID: bonus.SkillID, Value: bonus.Value, } } return result } // MovementLocation represents a movement destination for runback type MovementLocation struct { X float32 // X coordinate Y float32 // Y coordinate Z float32 // Z coordinate GridID int32 // Grid location ID Stage int32 // Movement stage ResetHPOnRunback bool // Whether to reset HP when reaching location UseNavPath bool // Whether to use navigation pathfinding Mapped bool // Whether location is mapped } // NewMovementLocation creates a new MovementLocation func NewMovementLocation(x, y, z float32, gridID int32) *MovementLocation { return &MovementLocation{ X: x, Y: y, Z: z, GridID: gridID, Stage: 0, ResetHPOnRunback: false, UseNavPath: false, Mapped: false, } } // Copy creates a deep copy of the MovementLocation func (ml *MovementLocation) Copy() *MovementLocation { return &MovementLocation{ X: ml.X, Y: ml.Y, Z: ml.Z, GridID: ml.GridID, Stage: ml.Stage, ResetHPOnRunback: ml.ResetHPOnRunback, UseNavPath: ml.UseNavPath, Mapped: ml.Mapped, } } // Timer represents a simple timer for NPC operations type Timer struct { duration time.Duration startTime time.Time enabled bool mutex sync.RWMutex } // NewTimer creates a new timer func NewTimer() *Timer { return &Timer{ enabled: false, } } // Start starts the timer with the given duration func (t *Timer) Start(durationMS int32, reset bool) { t.mutex.Lock() defer t.mutex.Unlock() if reset || !t.enabled { t.duration = time.Duration(durationMS) * time.Millisecond t.startTime = time.Now() t.enabled = true } } // Check checks if the timer has expired func (t *Timer) Check() bool { t.mutex.RLock() defer t.mutex.RUnlock() if !t.enabled { return false } return time.Since(t.startTime) >= t.duration } // Enabled returns whether the timer is currently enabled func (t *Timer) Enabled() bool { t.mutex.RLock() defer t.mutex.RUnlock() return t.enabled } // Disable disables the timer func (t *Timer) Disable() { t.mutex.Lock() defer t.mutex.Unlock() t.enabled = false } // Skill represents an NPC skill (simplified from C++ version) type Skill struct { SkillID int32 // Skill identifier Name string // Skill name CurrentVal int16 // Current skill value MaxVal int16 // Maximum skill value mutex sync.RWMutex } // NewSkill creates a new skill func NewSkill(id int32, name string, current, max int16) *Skill { return &Skill{ SkillID: id, Name: name, CurrentVal: current, MaxVal: max, } } // GetCurrentVal returns the current skill value func (s *Skill) GetCurrentVal() int16 { s.mutex.RLock() defer s.mutex.RUnlock() return s.CurrentVal } // SetCurrentVal sets the current skill value func (s *Skill) SetCurrentVal(val int16) { s.mutex.Lock() defer s.mutex.Unlock() s.CurrentVal = val } // IncreaseSkill increases the skill value (with random chance) func (s *Skill) IncreaseSkill() bool { s.mutex.Lock() defer s.mutex.Unlock() if s.CurrentVal < s.MaxVal { s.CurrentVal++ return true } return false } // Brain interface represents the AI brain system (placeholder) type Brain interface { Think() error GetBody() *NPC SetBody(*NPC) IsActive() bool SetActive(bool) } // DefaultBrain provides a simple brain implementation type DefaultBrain struct { npc *NPC active bool mutex sync.RWMutex } // NewDefaultBrain creates a new default brain func NewDefaultBrain(npc *NPC) *DefaultBrain { return &DefaultBrain{ npc: npc, active: true, } } // Think processes AI logic (placeholder implementation) func (b *DefaultBrain) Think() error { return nil } // GetBody returns the NPC this brain controls func (b *DefaultBrain) GetBody() *NPC { b.mutex.RLock() defer b.mutex.RUnlock() return b.npc } // SetBody sets the NPC this brain controls func (b *DefaultBrain) SetBody(npc *NPC) { b.mutex.Lock() defer b.mutex.Unlock() b.npc = npc } // IsActive returns whether the brain is active func (b *DefaultBrain) IsActive() bool { b.mutex.RLock() defer b.mutex.RUnlock() return b.active } // SetActive sets the brain's active state func (b *DefaultBrain) SetActive(active bool) { b.mutex.Lock() defer b.mutex.Unlock() b.active = active } // NPC represents a non-player character extending Entity type NPC struct { *entity.Entity // Embedded entity for combat capabilities // Core NPC properties appearanceID int32 // Appearance ID for client display npcID int32 // NPC database ID aiStrategy int8 // AI strategy (balanced/offensive/defensive) attackType int8 // Attack type preference castPercentage int8 // Percentage chance to cast spells maxPetLevel int8 // Maximum pet level // Combat and movement aggroRadius float32 // Aggro detection radius baseAggroRadius float32 // Base aggro radius (for resets) runback *MovementLocation // Runback location when leaving combat runningBack bool // Currently running back to spawn point runbackHeadingDir1 int16 // Original heading direction 1 runbackHeadingDir2 int16 // Original heading direction 2 pauseTimer *Timer // Movement pause timer // Spell and skill management primarySpellList int32 // Primary spell list ID secondarySpellList int32 // Secondary spell list ID primarySkillList int32 // Primary skill list ID secondarySkillList int32 // Secondary skill list ID equipmentListID int32 // Equipment list ID skills map[string]*Skill // NPC skills by name spells []*NPCSpell // Available spells castOnSpells map[int8][]*NPCSpell // Spells to cast by trigger type skillBonuses map[int32]*SkillBonus // Skill bonuses from spells hasSpells bool // Whether NPC has any spells castOnAggroCompleted bool // Whether cast-on-aggro spells are done // Brain/AI system brain Brain // AI brain for decision making // Shard system (for cross-server functionality) shardID int32 // Shard identifier shardCharID int32 // Character ID on shard shardCreatedTimestamp int64 // Timestamp when created on shard // Thread safety mutex sync.RWMutex // Main NPC mutex brainMutex sync.RWMutex // Brain-specific mutex // Atomic flags for thread-safe state management callRunback bool // Flag to trigger runback } // NewNPC creates a new NPC with default values func NewNPC() *NPC { npc := &NPC{ Entity: entity.NewEntity(), appearanceID: 0, npcID: 0, aiStrategy: AIStrategyBalanced, attackType: 0, castPercentage: DefaultCastPercentage, maxPetLevel: DefaultMaxPetLevel, aggroRadius: DefaultAggroRadius, baseAggroRadius: DefaultAggroRadius, runback: nil, runningBack: false, runbackHeadingDir1: 0, runbackHeadingDir2: 0, pauseTimer: NewTimer(), primarySpellList: 0, secondarySpellList: 0, primarySkillList: 0, secondarySkillList: 0, equipmentListID: 0, skills: make(map[string]*Skill), spells: make([]*NPCSpell, 0), castOnSpells: make(map[int8][]*NPCSpell), skillBonuses: make(map[int32]*SkillBonus), hasSpells: false, castOnAggroCompleted: false, shardID: 0, shardCharID: 0, shardCreatedTimestamp: 0, callRunback: false, } // Initialize cast-on spell arrays npc.castOnSpells[CastOnSpawn] = make([]*NPCSpell, 0) npc.castOnSpells[CastOnAggro] = make([]*NPCSpell, 0) // Create default brain npc.brain = NewDefaultBrain(npc) return npc } // NewNPCFromExisting creates a copy of an existing NPC with randomization func NewNPCFromExisting(oldNPC *NPC) *NPC { if oldNPC == nil { return NewNPC() } npc := NewNPC() // Copy basic properties npc.npcID = oldNPC.npcID npc.appearanceID = oldNPC.appearanceID npc.aiStrategy = oldNPC.aiStrategy npc.attackType = oldNPC.attackType npc.castPercentage = oldNPC.castPercentage npc.maxPetLevel = oldNPC.maxPetLevel npc.baseAggroRadius = oldNPC.baseAggroRadius npc.aggroRadius = oldNPC.baseAggroRadius // Copy spell lists npc.primarySpellList = oldNPC.primarySpellList npc.secondarySpellList = oldNPC.secondarySpellList npc.primarySkillList = oldNPC.primarySkillList npc.secondarySkillList = oldNPC.secondarySkillList npc.equipmentListID = oldNPC.equipmentListID // Copy skills (deep copy) npc.copySkills(oldNPC) // Copy spells (deep copy) npc.copySpells(oldNPC) return npc } // IsNPC always returns true for NPC instances func (n *NPC) IsNPC() bool { return true } // GetAppearanceID returns the appearance ID func (n *NPC) GetAppearanceID() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.appearanceID } // SetAppearanceID sets the appearance ID func (n *NPC) SetAppearanceID(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.appearanceID = id } // GetNPCID returns the NPC database ID func (n *NPC) GetNPCID() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.npcID } // SetNPCID sets the NPC database ID func (n *NPC) SetNPCID(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.npcID = id } // AI Strategy methods func (n *NPC) GetAIStrategy() int8 { n.mutex.RLock() defer n.mutex.RUnlock() return n.aiStrategy } func (n *NPC) SetAIStrategy(strategy int8) { n.mutex.Lock() defer n.mutex.Unlock() n.aiStrategy = strategy } // Attack Type methods func (n *NPC) GetAttackType() int8 { n.mutex.RLock() defer n.mutex.RUnlock() return n.attackType } func (n *NPC) SetAttackType(attackType int8) { n.mutex.Lock() defer n.mutex.Unlock() n.attackType = attackType } // Cast Percentage methods func (n *NPC) GetCastPercentage() int8 { n.mutex.RLock() defer n.mutex.RUnlock() return n.castPercentage } func (n *NPC) SetCastPercentage(percentage int8) { n.mutex.Lock() defer n.mutex.Unlock() if percentage < 0 { percentage = 0 } else if percentage > 100 { percentage = 100 } n.castPercentage = percentage } // Aggro Radius methods func (n *NPC) GetAggroRadius() float32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.aggroRadius } func (n *NPC) SetAggroRadius(radius float32, overrideBase bool) { n.mutex.Lock() defer n.mutex.Unlock() if n.baseAggroRadius == 0.0 || overrideBase { n.baseAggroRadius = radius } n.aggroRadius = radius } func (n *NPC) GetBaseAggroRadius() float32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.baseAggroRadius } // Pet Level methods func (n *NPC) GetMaxPetLevel() int8 { n.mutex.RLock() defer n.mutex.RUnlock() return n.maxPetLevel } func (n *NPC) SetMaxPetLevel(level int8) { n.mutex.Lock() defer n.mutex.Unlock() n.maxPetLevel = level } // Spell List methods func (n *NPC) GetPrimarySpellList() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.primarySpellList } func (n *NPC) SetPrimarySpellList(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.primarySpellList = id } func (n *NPC) GetSecondarySpellList() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.secondarySpellList } func (n *NPC) SetSecondarySpellList(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.secondarySpellList = id } // Skill List methods func (n *NPC) GetPrimarySkillList() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.primarySkillList } func (n *NPC) SetPrimarySkillList(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.primarySkillList = id } func (n *NPC) GetSecondarySkillList() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.secondarySkillList } func (n *NPC) SetSecondarySkillList(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.secondarySkillList = id } // Equipment List methods func (n *NPC) GetEquipmentListID() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.equipmentListID } func (n *NPC) SetEquipmentListID(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.equipmentListID = id } // HasSpells returns whether the NPC has any spells func (n *NPC) HasSpells() bool { n.mutex.RLock() defer n.mutex.RUnlock() return n.hasSpells } // GetSpells returns a copy of all spells func (n *NPC) GetSpells() []*NPCSpell { n.mutex.RLock() defer n.mutex.RUnlock() result := make([]*NPCSpell, len(n.spells)) for i, spell := range n.spells { result[i] = spell.Copy() } return result } // SetSpells sets the NPC's spell list func (n *NPC) SetSpells(spells []*NPCSpell) { n.mutex.Lock() defer n.mutex.Unlock() // Clear existing cast-on spells for i := int8(0); i < MaxCastTypes; i++ { n.castOnSpells[i] = make([]*NPCSpell, 0) } // Clear existing spells n.spells = make([]*NPCSpell, 0) if spells == nil || len(spells) == 0 { n.hasSpells = false return } n.hasSpells = true // Process spells and separate cast-on types for _, spell := range spells { if spell == nil { continue } spellCopy := spell.Copy() if spellCopy.GetCastOnSpawn() { n.castOnSpells[CastOnSpawn] = append(n.castOnSpells[CastOnSpawn], spellCopy) } else if spellCopy.GetCastOnInitialAggro() { n.castOnSpells[CastOnAggro] = append(n.castOnSpells[CastOnAggro], spellCopy) } else { n.spells = append(n.spells, spellCopy) } } } // GetSkillByName returns a skill by name func (n *NPC) GetSkillByName(name string, checkUpdate bool) *Skill { n.mutex.RLock() defer n.mutex.RUnlock() skill, exists := n.skills[name] if !exists { return nil } // Random skill increase (10% chance) if checkUpdate && skill.GetCurrentVal() < skill.MaxVal && rand.Intn(100) >= 90 { skill.IncreaseSkill() } return skill } // GetSkillByID returns a skill by ID (requires master skill list lookup) func (n *NPC) GetSkillByID(id int32, checkUpdate bool) *Skill { return nil } // SetSkills sets the NPC's skills func (n *NPC) SetSkills(skills map[string]*Skill) { n.mutex.Lock() defer n.mutex.Unlock() // Clear existing skills n.skills = make(map[string]*Skill) // Copy skills if skills != nil { for name, skill := range skills { if skill != nil { n.skills[name] = &Skill{ SkillID: skill.SkillID, Name: skill.Name, CurrentVal: skill.CurrentVal, MaxVal: skill.MaxVal, } } } } } // AddSkillBonus adds a skill bonus from a spell func (n *NPC) AddSkillBonus(spellID, skillID int32, value float32) { if value == 0 { return } n.mutex.Lock() defer n.mutex.Unlock() // Get or create skill bonus skillBonus, exists := n.skillBonuses[spellID] if !exists { skillBonus = NewSkillBonus(spellID) n.skillBonuses[spellID] = skillBonus } // Add the skill bonus skillBonus.AddSkill(skillID, value) // Apply bonus to existing skills for _, skill := range n.skills { if skill.SkillID == skillID { skill.CurrentVal += int16(value) skill.MaxVal += int16(value) } } } // RemoveSkillBonus removes skill bonuses from a spell func (n *NPC) RemoveSkillBonus(spellID int32) { n.mutex.Lock() defer n.mutex.Unlock() skillBonus, exists := n.skillBonuses[spellID] if !exists { return } // Remove bonuses from skills bonuses := skillBonus.GetSkills() for _, bonus := range bonuses { for _, skill := range n.skills { if skill.SkillID == bonus.SkillID { skill.CurrentVal -= int16(bonus.Value) skill.MaxVal -= int16(bonus.Value) } } } // Remove the skill bonus delete(n.skillBonuses, spellID) } // Runback Location methods func (n *NPC) SetRunbackLocation(x, y, z float32, gridID int32, resetHP bool) { n.mutex.Lock() defer n.mutex.Unlock() n.runback = &MovementLocation{ X: x, Y: y, Z: z, GridID: gridID, Stage: 0, ResetHPOnRunback: resetHP, UseNavPath: false, Mapped: false, } } func (n *NPC) GetRunbackLocation() *MovementLocation { n.mutex.RLock() defer n.mutex.RUnlock() if n.runback == nil { return nil } return n.runback.Copy() } func (n *NPC) GetRunbackDistance() float32 { n.mutex.RLock() defer n.mutex.RUnlock() if n.runback == nil || n.Entity == nil { return 0 } // Calculate distance using basic distance formula dx := n.Entity.GetX() - n.runback.X dy := n.Entity.GetY() - n.runback.Y dz := n.Entity.GetZ() - n.runback.Z return float32(math.Sqrt(float64(dx*dx + dy*dy + dz*dz))) } func (n *NPC) ClearRunback() { n.mutex.Lock() defer n.mutex.Unlock() n.runback = nil n.runningBack = false n.runbackHeadingDir1 = 0 n.runbackHeadingDir2 = 0 } // StartRunback sets the current location as the runback point func (n *NPC) StartRunback(resetHP bool) { if n.GetRunbackLocation() != nil { return } if n.Entity == nil { return } n.mutex.Lock() defer n.mutex.Unlock() n.runback = &MovementLocation{ X: n.Entity.GetX(), Y: n.Entity.GetY(), Z: n.Entity.GetZ(), GridID: 0, // TODO: Implement grid system Stage: 0, ResetHPOnRunback: resetHP, UseNavPath: false, Mapped: false, } n.runbackHeadingDir1 = 0 n.runbackHeadingDir2 = 0 } // Runback initiates runback movement func (n *NPC) Runback(distance float32, stopFollowing bool) { if n.runback == nil { return } if distance == 0.0 { distance = n.GetRunbackDistance() } n.mutex.Lock() n.runningBack = true n.mutex.Unlock() if stopFollowing && n.Entity != nil { // TODO: Implement SetFollowing when available on Entity } } // IsRunningBack returns whether the NPC is currently running back func (n *NPC) IsRunningBack() bool { n.mutex.RLock() defer n.mutex.RUnlock() return n.runningBack } // Movement pause methods func (n *NPC) PauseMovement(periodMS int32) bool { if periodMS < 1 { periodMS = 1 } if periodMS > MaxPauseTime { periodMS = MaxPauseTime } n.pauseTimer.Start(periodMS, true) return true } func (n *NPC) IsPauseMovementTimerActive() bool { if n.pauseTimer.Check() { n.pauseTimer.Disable() n.callRunback = true } return n.pauseTimer.Enabled() } // Brain methods func (n *NPC) GetBrain() Brain { n.brainMutex.RLock() defer n.brainMutex.RUnlock() return n.brain } func (n *NPC) SetBrain(brain Brain) { n.brainMutex.Lock() defer n.brainMutex.Unlock() // Validate brain matches this NPC if brain != nil && brain.GetBody() != n { return } n.brain = brain } // Shard methods func (n *NPC) GetShardID() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.shardID } func (n *NPC) SetShardID(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.shardID = id } func (n *NPC) GetShardCharID() int32 { n.mutex.RLock() defer n.mutex.RUnlock() return n.shardCharID } func (n *NPC) SetShardCharID(id int32) { n.mutex.Lock() defer n.mutex.Unlock() n.shardCharID = id } func (n *NPC) GetShardCreatedTimestamp() int64 { n.mutex.RLock() defer n.mutex.RUnlock() return n.shardCreatedTimestamp } func (n *NPC) SetShardCreatedTimestamp(timestamp int64) { n.mutex.Lock() defer n.mutex.Unlock() n.shardCreatedTimestamp = timestamp } // InCombat handles combat state changes func (n *NPC) InCombat(val bool) { if n.Entity == nil { return } if val { // Entering combat if n.GetRunbackLocation() == nil { n.StartRunback(true) } } else { // Leaving combat if n.Entity.GetHP() > 0 { // TODO: Re-enable action states, stop heroic opportunities } } } // ProcessCombat handles combat processing func (n *NPC) ProcessCombat() { // TODO: Implement combat processing logic } // Copy helper methods func (n *NPC) copySkills(oldNPC *NPC) { if oldNPC == nil { return } oldNPC.mutex.RLock() oldSkills := make(map[string]*Skill) for name, skill := range oldNPC.skills { if skill != nil { oldSkills[name] = &Skill{ SkillID: skill.SkillID, Name: skill.Name, CurrentVal: skill.CurrentVal, MaxVal: skill.MaxVal, } } } oldNPC.mutex.RUnlock() n.SetSkills(oldSkills) } func (n *NPC) copySpells(oldNPC *NPC) { if oldNPC == nil { return } oldNPC.mutex.RLock() oldSpells := make([]*NPCSpell, len(oldNPC.spells)) for i, spell := range oldNPC.spells { if spell != nil { oldSpells[i] = spell.Copy() } } // Also copy cast-on spells for _, spells := range oldNPC.castOnSpells { for _, spell := range spells { if spell != nil { oldSpells = append(oldSpells, spell.Copy()) } } } oldNPC.mutex.RUnlock() n.SetSpells(oldSpells) } // Validation methods func (n *NPC) IsValid() bool { if n.Entity == nil { return false } // Basic validation if n.Entity.GetLevel() < MinNPCLevel || n.Entity.GetLevel() > MaxNPCLevel { return false } if n.appearanceID < MinAppearanceID || n.appearanceID > MaxAppearanceID { return false } return true } // String returns a string representation of the NPC func (n *NPC) String() string { if n.Entity == nil { return fmt.Sprintf("NPC{ID: %d, AppearanceID: %d, Entity: nil}", n.npcID, n.appearanceID) } return fmt.Sprintf("NPC{ID: %d, Name: %s, Level: %d, AppearanceID: %d}", n.npcID, n.Entity.GetName(), n.Entity.GetLevel(), n.appearanceID) } // NPCStatistics contains NPC system statistics type NPCStatistics struct { TotalNPCs int `json:"total_npcs"` NPCsInCombat int `json:"npcs_in_combat"` NPCsWithSpells int `json:"npcs_with_spells"` NPCsWithSkills int `json:"npcs_with_skills"` AIStrategyCounts map[string]int `json:"ai_strategy_counts"` SpellCastCount int64 `json:"spell_cast_count"` SkillUsageCount int64 `json:"skill_usage_count"` RunbackCount int64 `json:"runback_count"` AverageAggroRadius float32 `json:"average_aggro_radius"` } // 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 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() *Manager { return &Manager{ npcs: make(map[int32]*NPC), npcsByZone: make(map[int32][]*NPC), npcsByAppearance: make(map[int32][]*NPC), aiStrategyCounts: make(map[int8]int64), maxNPCs: 10000, defaultAggroRadius: DefaultAggroRadius, enableAI: true, } } // 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 appearance index appearanceID := npc.GetAppearanceID() m.npcsByAppearance[appearanceID] = append(m.npcsByAppearance[appearanceID], npc) // Update statistics m.totalNPCs++ strategy := npc.GetAIStrategy() m.aiStrategyCounts[strategy]++ 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 all indexes delete(m.npcs, id) m.removeFromAppearanceIndex(npc) // Update statistics m.totalNPCs-- strategy := npc.GetAIStrategy() if count := m.aiStrategyCounts[strategy]; count > 0 { m.aiStrategyCounts[strategy]-- } 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) } 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 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 { 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() { brain.Think() } } } // 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 { 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 { 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()) } 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 { 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) 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) } } // 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() { // 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() } // Initialize loads NPCs from database and sets up the system func (m *Manager) Initialize() error { return nil } // Packet handling functions for complete NPC network functionality // PacketType constants for NPC operations const ( PacketNPCUpdate = 0x01 PacketNPCCombatUpdate = 0x02 PacketNPCSpellCast = 0x03 PacketNPCMovement = 0x04 PacketNPCDespawn = 0x05 PacketNPCSpawn = 0x06 PacketNPCAggro = 0x07 PacketNPCCommand = 0x08 ) // SendNPCUpdate creates and sends NPC update packet func (n *NPC) SendNPCUpdate(clients []NPCClient) error { if n.Entity == nil { return fmt.Errorf("entity is nil") } data := map[string]interface{}{ "npc_id": n.GetNPCID(), "appearance_id": n.GetAppearanceID(), "name": n.Entity.GetName(), "level": n.Entity.GetLevel(), "x": n.Entity.GetX(), "y": n.Entity.GetY(), "z": n.Entity.GetZ(), "heading": n.Entity.GetHeading(), "hp": n.Entity.GetHP(), "max_hp": n.Entity.GetTotalHP(), "in_combat": false, // TODO: Get from entity when available "ai_strategy": n.GetAIStrategy(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC update: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCUpdate, packetData); err != nil { return fmt.Errorf("failed to send NPC update to client: %w", err) } } return nil } // SendNPCSpawn creates and sends NPC spawn packet func (n *NPC) SendNPCSpawn(clients []NPCClient) error { if n.Entity == nil { return fmt.Errorf("entity is nil") } data := map[string]interface{}{ "npc_id": n.GetNPCID(), "appearance_id": n.GetAppearanceID(), "name": n.Entity.GetName(), "level": n.Entity.GetLevel(), "x": n.Entity.GetX(), "y": n.Entity.GetY(), "z": n.Entity.GetZ(), "heading": n.Entity.GetHeading(), "max_hp": n.Entity.GetTotalHP(), "ai_strategy": n.GetAIStrategy(), "aggro_radius": n.GetAggroRadius(), "has_spells": n.HasSpells(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC spawn: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCSpawn, packetData); err != nil { return fmt.Errorf("failed to send NPC spawn to client: %w", err) } } return nil } // SendNPCDespawn creates and sends NPC despawn packet func (n *NPC) SendNPCDespawn(clients []NPCClient) error { data := map[string]interface{}{ "npc_id": n.GetNPCID(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC despawn: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCDespawn, packetData); err != nil { return fmt.Errorf("failed to send NPC despawn to client: %w", err) } } return nil } // SendNPCCombatUpdate creates and sends NPC combat update packet func (n *NPC) SendNPCCombatUpdate(clients []NPCClient, targetID int32, damage int32) error { if n.Entity == nil { return fmt.Errorf("entity is nil") } data := map[string]interface{}{ "npc_id": n.GetNPCID(), "target_id": targetID, "damage": damage, "hp": n.Entity.GetHP(), "max_hp": n.Entity.GetTotalHP(), "in_combat": true, "timestamp": time.Now().UnixMilli(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC combat update: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCCombatUpdate, packetData); err != nil { return fmt.Errorf("failed to send NPC combat update to client: %w", err) } } return nil } // SendNPCSpellCast creates and sends NPC spell cast packet func (n *NPC) SendNPCSpellCast(clients []NPCClient, spellID int32, targetID int32, tier int8) error { data := map[string]interface{}{ "npc_id": n.GetNPCID(), "spell_id": spellID, "target_id": targetID, "tier": tier, "cast_time": 3000, // Default 3 second cast time "timestamp": time.Now().UnixMilli(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC spell cast: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCSpellCast, packetData); err != nil { return fmt.Errorf("failed to send NPC spell cast to client: %w", err) } } return nil } // SendNPCMovement creates and sends NPC movement packet func (n *NPC) SendNPCMovement(clients []NPCClient) error { if n.Entity == nil { return fmt.Errorf("entity is nil") } data := map[string]interface{}{ "npc_id": n.GetNPCID(), "x": n.Entity.GetX(), "y": n.Entity.GetY(), "z": n.Entity.GetZ(), "heading": n.Entity.GetHeading(), "speed": float32(2.0), // TODO: Get from entity when available "running_back": n.IsRunningBack(), "timestamp": time.Now().UnixMilli(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC movement: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCMovement, packetData); err != nil { return fmt.Errorf("failed to send NPC movement to client: %w", err) } } return nil } // SendNPCAggro creates and sends NPC aggro packet func (n *NPC) SendNPCAggro(clients []NPCClient, targetID int32, aggroAmount int32) error { data := map[string]interface{}{ "npc_id": n.GetNPCID(), "target_id": targetID, "aggro_amount": aggroAmount, "timestamp": time.Now().UnixMilli(), } packetData, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal NPC aggro: %w", err) } for _, client := range clients { if err := client.SendPacket(PacketNPCAggro, packetData); err != nil { return fmt.Errorf("failed to send NPC aggro to client: %w", err) } } return nil } // ProcessNPCCommand handles incoming NPC command packets func (n *NPC) ProcessNPCCommand(packetData []byte) error { var commandData map[string]interface{} if err := json.Unmarshal(packetData, &commandData); err != nil { return fmt.Errorf("failed to unmarshal NPC command: %w", err) } commandType, ok := commandData["command"].(string) if !ok { return fmt.Errorf("missing or invalid command type") } switch commandType { case "use": return n.handleUseCommand(commandData) case "attack": return n.handleAttackCommand(commandData) case "follow": return n.handleFollowCommand(commandData) case "stop": return n.handleStopCommand(commandData) default: return fmt.Errorf("unknown NPC command: %s", commandType) } } // HandleUse processes entity command usage (enhanced with packet support) func (n *NPC) HandleUse(client NPCClient, commandType string) bool { if client == nil || len(commandType) == 0 { return false } if n.Entity == nil { return false } // Create use command response packet data := map[string]interface{}{ "npc_id": n.GetNPCID(), "command": commandType, "response": "Command acknowledged", "client_id": client.GetClientID(), "timestamp": time.Now().UnixMilli(), } packetData, _ := json.Marshal(data) client.SendPacket(PacketNPCCommand, packetData) return true } // Command handlers for packet processing func (n *NPC) handleUseCommand(data map[string]interface{}) error { return nil } func (n *NPC) handleAttackCommand(data map[string]interface{}) error { targetID, ok := data["target_id"].(float64) if !ok { return fmt.Errorf("missing target_id in attack command") } // TODO: Implement attack logic _ = int32(targetID) return nil } func (n *NPC) handleFollowCommand(data map[string]interface{}) error { targetID, ok := data["target_id"].(float64) if !ok { return fmt.Errorf("missing target_id in follow command") } // TODO: Implement follow logic _ = int32(targetID) return nil } func (n *NPC) handleStopCommand(data map[string]interface{}) error { // TODO: Implement stop movement logic return nil } // NPCClient interface for packet communication type NPCClient interface { GetClientID() int32 SendPacket(packetType uint8, data []byte) error } // Broadcast packet to all clients in range func (m *Manager) BroadcastNPCUpdate(npc *NPC, clients []NPCClient) error { return npc.SendNPCUpdate(clients) } // Broadcast NPC spawn to all clients in range func (m *Manager) BroadcastNPCSpawn(npc *NPC, clients []NPCClient) error { return npc.SendNPCSpawn(clients) } // Broadcast NPC despawn to all clients in range func (m *Manager) BroadcastNPCDespawn(npc *NPC, clients []NPCClient) error { return npc.SendNPCDespawn(clients) }