eq2go/internal/npc/npc.go
2025-08-29 17:48:39 -05:00

2170 lines
51 KiB
Go

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 <template_id> <new_id>")
}
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)
}