806 lines
17 KiB
Go
806 lines
17 KiB
Go
package npc
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/common"
|
|
"eq2emu/internal/entity"
|
|
"eq2emu/internal/spawn"
|
|
)
|
|
|
|
// 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 entity data (stats, appearance, etc.)
|
|
if oldNPC.Entity != nil {
|
|
npc.Entity = oldNPC.Entity.Copy().(*entity.Entity)
|
|
}
|
|
|
|
// Handle level randomization
|
|
if oldNPC.Entity != nil {
|
|
minLevel := oldNPC.Entity.GetMinLevel()
|
|
maxLevel := oldNPC.Entity.GetMaxLevel()
|
|
if minLevel < maxLevel {
|
|
randomLevel := minLevel + int8(rand.Intn(int(maxLevel-minLevel)+1))
|
|
npc.Entity.SetLevel(randomLevel)
|
|
}
|
|
}
|
|
|
|
// Copy skills (deep copy)
|
|
npc.copySkills(oldNPC)
|
|
|
|
// Copy spells (deep copy)
|
|
npc.copySpells(oldNPC)
|
|
|
|
// Handle appearance randomization
|
|
if oldNPC.Entity != nil && oldNPC.Entity.GetRandomize() > 0 {
|
|
npc.randomizeAppearance(oldNPC.Entity.GetRandomize())
|
|
}
|
|
|
|
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 {
|
|
// TODO: Implement skill lookup by ID using master skill list
|
|
// For now, return nil as we need the master skill list integration
|
|
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: n.Entity.GetLocation(),
|
|
Stage: 0,
|
|
ResetHPOnRunback: resetHP,
|
|
UseNavPath: false,
|
|
Mapped: false,
|
|
}
|
|
|
|
// Store original heading
|
|
n.runbackHeadingDir1 = n.Entity.GetHeading()
|
|
n.runbackHeadingDir2 = n.Entity.GetHeading() // In C++ these are separate values
|
|
}
|
|
|
|
// 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()
|
|
|
|
// TODO: Implement actual movement logic
|
|
// This would integrate with the movement system
|
|
|
|
if stopFollowing && n.Entity != nil {
|
|
n.Entity.SetFollowing(false)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// TODO: Integrate with movement system to stop movement
|
|
// For now, just start the pause timer
|
|
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 {
|
|
// TODO: Log error
|
|
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
|
|
}
|
|
|
|
// HandleUse processes entity command usage
|
|
func (n *NPC) HandleUse(client Client, commandType string) bool {
|
|
if client == nil || len(commandType) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check if NPC shows command icons
|
|
if n.Entity == nil {
|
|
return false
|
|
}
|
|
|
|
// TODO: Implement entity command processing
|
|
// This would integrate with the command system
|
|
|
|
return false
|
|
}
|
|
|
|
// InCombat handles combat state changes
|
|
func (n *NPC) InCombat(val bool) {
|
|
if n.Entity == nil {
|
|
return
|
|
}
|
|
|
|
currentCombat := n.Entity.GetInCombat()
|
|
if currentCombat == val {
|
|
return
|
|
}
|
|
|
|
n.Entity.SetInCombat(val)
|
|
|
|
if val {
|
|
// Entering combat
|
|
if n.GetRunbackLocation() == nil {
|
|
n.StartRunback(true)
|
|
}
|
|
|
|
// Set max speed for combat
|
|
if n.Entity.GetMaxSpeed() > 0 {
|
|
n.Entity.SetSpeed(n.Entity.GetMaxSpeed())
|
|
}
|
|
|
|
// TODO: Add combat icon, call spawn scripts, etc.
|
|
|
|
} else {
|
|
// Leaving combat
|
|
// TODO: Remove combat icon, call combat reset scripts, etc.
|
|
|
|
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
|
|
// This would handle spell casting, AI decisions, etc.
|
|
}
|
|
|
|
// 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 castType, spells := range oldNPC.castOnSpells {
|
|
for _, spell := range spells {
|
|
if spell != nil {
|
|
oldSpells = append(oldSpells, spell.Copy())
|
|
}
|
|
}
|
|
}
|
|
oldNPC.mutex.RUnlock()
|
|
|
|
n.SetSpells(oldSpells)
|
|
}
|
|
|
|
// randomizeAppearance applies appearance randomization
|
|
func (n *NPC) randomizeAppearance(flags int32) {
|
|
// TODO: Implement full appearance randomization
|
|
// This is a complex system that would integrate with the appearance system
|
|
|
|
// For now, just implement basic randomization
|
|
if n.Entity == nil {
|
|
return
|
|
}
|
|
|
|
// Random gender
|
|
if flags&RandomizeGender != 0 {
|
|
gender := int8(rand.Intn(2) + 1) // 1 or 2
|
|
n.Entity.SetGender(gender)
|
|
}
|
|
|
|
// Random race (simplified)
|
|
if flags&RandomizeRace != 0 {
|
|
// TODO: Implement race randomization based on alignment
|
|
race := int16(rand.Intn(21)) // 0-20 for basic races
|
|
n.Entity.SetRace(race)
|
|
}
|
|
|
|
// Color randomization
|
|
if flags&RandomizeSkinColor != 0 {
|
|
// TODO: Implement skin color randomization
|
|
}
|
|
|
|
// More randomization options would be implemented here
|
|
}
|
|
|
|
// 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)
|
|
} |