2170 lines
51 KiB
Go
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)
|
|
} |