812 lines
18 KiB
Go

package npc
import (
"fmt"
"math"
"math/rand"
"eq2emu/internal/entity"
)
// 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.)
// TODO: Implement entity copying when Entity.Copy() is available
// if oldNPC.Entity != nil {
// npc.Entity = oldNPC.Entity.Copy().(*entity.Entity)
// }
// Handle level randomization
// TODO: Implement level randomization when GetMinLevel/GetMaxLevel are available
// 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
// TODO: Implement appearance randomization when GetRandomize is available
// 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: 0, // TODO: Implement grid system
Stage: 0,
ResetHPOnRunback: resetHP,
UseNavPath: false,
Mapped: false,
}
// Store original heading
// TODO: Implement heading storage when Entity.GetHeading() returns compatible type
// n.runbackHeadingDir1 = int16(n.Entity.GetHeading())
// n.runbackHeadingDir2 = int16(n.Entity.GetHeading()) // In C++ these are separate values
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()
// TODO: Implement actual movement logic
// This would integrate with the movement system
if stopFollowing && n.Entity != nil {
// TODO: Implement SetFollowing when available on Entity
// 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
}
// TODO: Implement GetInCombat and SetInCombat when available on Entity
// 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
// TODO: Implement GetMaxSpeed and SetSpeed when available on Entity
// 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 _, 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 {
// TODO: Implement SetGender when available on Entity
// gender := int8(rand.Intn(2) + 1) // 1 or 2
// n.Entity.SetGender(gender)
}
// Random race (simplified)
if flags&RandomizeRace != 0 {
// TODO: Implement SetRace when available on Entity
// 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)
}