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) }