440 lines
12 KiB
Go
440 lines
12 KiB
Go
package npc
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/common"
|
|
"eq2emu/internal/entity"
|
|
"eq2emu/internal/spawn"
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// 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 (placeholder for now)
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
// TODO: Implement AI thinking logic
|
|
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
|
|
}
|
|
|
|
// 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"`
|
|
} |