simplify npc

This commit is contained in:
Sky Johnson 2025-08-29 17:48:39 -05:00
parent afded7da3b
commit d74f309ade
23 changed files with 3731 additions and 4473 deletions

View File

@ -19,6 +19,9 @@ This document outlines how we successfully simplified the EverQuest II housing p
- Items - Items
- Items/Loot - Items/Loot
- Languages - Languages
- NPC
- NPC/AI
- NPC/Race Types
## Before: Complex Architecture (8 Files, ~2000+ Lines) ## Before: Complex Architecture (8 Files, ~2000+ Lines)

View File

@ -1406,4 +1406,194 @@ func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player P
} }
} }
} }
}
// LanguagePacketBuilder handles language-related packet building
type LanguagePacketBuilder struct {
logger Logger
}
// NewLanguagePacketBuilder creates a new language packet builder
func NewLanguagePacketBuilder(logger Logger) *LanguagePacketBuilder {
return &LanguagePacketBuilder{
logger: logger,
}
}
// BuildLanguagesPacket builds the WS_Languages packet for a player
// This packet sends the player's known languages and current language to the client
func (lpb *LanguagePacketBuilder) BuildLanguagesPacket(languages []*Language, currentLanguageID int32) map[string]any {
data := make(map[string]any)
// Number of languages the player knows
data["num_languages"] = int8(len(languages))
// Array of language IDs
languageIDs := make([]int8, len(languages))
for i, lang := range languages {
languageIDs[i] = int8(lang.GetID())
}
data["language_array"] = languageIDs
// Unknown field (from packet structure)
data["unknown"] = int8(0)
// Current active language
data["current_language"] = int8(currentLanguageID)
if lpb.logger != nil {
lpb.logger.LogDebug("Built Languages packet: %d languages, current: %d", len(languages), currentLanguageID)
}
return data
}
// BuildHearChatPacket builds a HearChat packet with language support
// This is used when players speak in different languages
func (lpb *LanguagePacketBuilder) BuildHearChatPacket(speakerName string, message string, languageID int32, channel int16) map[string]any {
data := make(map[string]any)
// Basic chat data
data["from"] = speakerName
data["message"] = message
data["language"] = int8(languageID)
data["channel"] = channel
if lpb.logger != nil {
lpb.logger.LogDebug("Built HearChat packet: %s speaking %s in language %d", speakerName, message, languageID)
}
return data
}
// BuildPlayFlavorPacket builds a PlayFlavor packet with language support
// This is used for emotes and flavor text that can be in different languages
func (lpb *LanguagePacketBuilder) BuildPlayFlavorPacket(targetName string, message string, languageID int32, emoteID int32) map[string]any {
data := make(map[string]any)
data["target"] = targetName
data["message"] = message
data["language"] = int8(languageID)
data["emote"] = emoteID
if lpb.logger != nil {
lpb.logger.LogDebug("Built PlayFlavor packet: %s with message %s in language %d", targetName, message, languageID)
}
return data
}
// BuildUpdateSkillBookPacket builds skill book update packet with language information
// This is used when showing skill information that may include language requirements
func (lpb *LanguagePacketBuilder) BuildUpdateSkillBookPacket(skills []SkillInfo) map[string]any {
data := make(map[string]any)
// This would be expanded based on the skill book packet structure
// For now, we'll focus on language-related fields
for _, skill := range skills {
// Mark language skills appropriately
if skill.IsLanguageSkill {
data["language_unknown"] = int8(0) // 0 = known, 1 = unknown
}
}
if lpb.logger != nil {
lpb.logger.LogDebug("Built UpdateSkillBook packet with %d skills", len(skills))
}
return data
}
// SkillInfo represents skill information for packet building
type SkillInfo struct {
ID int32
Name string
IsLanguageSkill bool
Value int16
MaxValue int16
}
// LanguagePacketHandler handles incoming language-related packets from clients
type LanguagePacketHandler struct {
manager *LanguageManager
builder *LanguagePacketBuilder
logger Logger
}
// NewLanguagePacketHandler creates a new language packet handler
func NewLanguagePacketHandler(manager *LanguageManager, builder *LanguagePacketBuilder, logger Logger) *LanguagePacketHandler {
return &LanguagePacketHandler{
manager: manager,
builder: builder,
logger: logger,
}
}
// HandleLanguageChangeRequest handles requests to change the player's active language
func (lph *LanguagePacketHandler) HandleLanguageChangeRequest(playerID int32, languageID int32) (map[string]any, error) {
// Validate language exists
language := lph.manager.GetLanguage(languageID)
if language == nil {
return nil, fmt.Errorf("language %d does not exist", languageID)
}
// Record language usage
lph.manager.RecordLanguageUsage(languageID)
if lph.logger != nil {
lph.logger.LogInfo("Player %d changed to language %d (%s)", playerID, languageID, language.GetName())
}
// Build response packet - this would typically be sent back to confirm the change
// For now, we'll return the Languages packet with updated current language
return lph.buildLanguagesResponsePacket(playerID, languageID)
}
// buildLanguagesResponsePacket builds a response packet for language changes
func (lph *LanguagePacketHandler) buildLanguagesResponsePacket(playerID int32, currentLanguageID int32) (map[string]any, error) {
// In a real implementation, you would get the player's known languages from their PlayerLanguageAdapter
// For now, we'll return a basic response with common language
languages := []*Language{}
// Add common language (always known)
if commonLang := lph.manager.GetLanguage(LanguageIDCommon); commonLang != nil {
languages = append(languages, commonLang)
}
// Add the requested language if it exists and is different from common
if currentLanguageID != LanguageIDCommon {
if requestedLang := lph.manager.GetLanguage(currentLanguageID); requestedLang != nil {
languages = append(languages, requestedLang)
}
}
return lph.builder.BuildLanguagesPacket(languages, currentLanguageID), nil
}
// HandleChatMessage handles chat messages with language support
func (lph *LanguagePacketHandler) HandleChatMessage(speakerName string, message string, languageID int32, channel int16, listeners []Player) []map[string]any {
var packets []map[string]any
// Process message for each listener
for range listeners {
// Check if listener can understand the language
processedMessage := message
// In a real implementation, you would check if the listener knows the language
// and potentially scramble the message if they don't understand it
// For now, we'll just pass through the message
// Build packet for this listener
packet := lph.builder.BuildHearChatPacket(speakerName, processedMessage, languageID, channel)
packets = append(packets, packet)
}
// Record language usage
lph.manager.RecordLanguageUsage(languageID)
if lph.logger != nil {
lph.logger.LogDebug("Processed chat message for %d listeners in language %d", len(listeners), languageID)
}
return packets
} }

422
internal/npc/ai/ai.go Normal file
View File

@ -0,0 +1,422 @@
package ai
import (
"encoding/json"
"fmt"
"time"
)
// AI tick constants
const (
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
)
// Combat constants
const (
MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback
)
// Hate system constants
const (
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list
)
// Encounter system constants
const (
MaxEncounterSize int = 50 // Maximum entities in encounter list
)
// Spell recovery constants
const (
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
)
// Brain type constants for identification
const (
BrainTypeDefault int8 = 0
BrainTypeCombatPet int8 = 1
BrainTypeNonCombatPet int8 = 2
BrainTypeBlank int8 = 3
BrainTypeCustom int8 = 4
BrainTypeDumbFire int8 = 5
)
// Pet movement constants
const (
PetMovementFollow int8 = 0
PetMovementStay int8 = 1
PetMovementGuard int8 = 2
)
// Encounter state constants
const (
EncounterStateAvailable int8 = 0
EncounterStateLocked int8 = 1
EncounterStateBroken int8 = 2
)
// Combat decision constants
const (
MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs
)
// AI state flags
const (
AIStateIdle int32 = 0
AIStateCombat int32 = 1
AIStateFollowing int32 = 2
AIStateRunback int32 = 3
AIStateCasting int32 = 4
AIStateMoving int32 = 5
)
// Debug levels
const (
DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3
)
// Timer constants
const (
MillisecondsPerSecond int32 = 1000
RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10
)
// MovementLocation represents a location for movement/runback
type MovementLocation struct {
X float32
Y float32
Z float32
GridID int32
Stage int32
}
// Brain interface defines all AI brain capabilities
type Brain interface {
// Core brain functions
Think() error
GetBrainType() int8
IsActive() bool
SetActive(bool)
GetLastThink() int64
SetLastThink(int64)
GetThinkTick() int32
SetThinkTick(int32)
// Hate management
AddHate(entityID, hate int32)
GetHate(entityID int32) int32
GetMostHated() int32
ClearHate()
ClearHateForEntity(entityID int32)
// Encounter management
AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool
RemoveFromEncounter(entityID int32)
IsInEncounter(entityID int32) bool
ClearEncounter()
GetEncounterSize() int
// State management
GetState() int32
SetState(int32)
HasRecovered() bool
SetSpellRecovery(int64)
// Statistics
GetStatistics() *BrainStatistics
// Debugging
SetDebugLevel(int8)
GetDebugLevel() int8
}
// Logger interface for AI logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// NPC interface defines the required NPC functionality for AI
type NPC interface {
// Basic NPC information
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
SetHP(int32)
IsAlive() bool
// Combat state
GetInCombat() bool
InCombat(bool)
GetTarget() Entity
SetTarget(Entity)
// Pet functionality
IsPet() bool
GetOwner() Entity
// Movement and positioning
GetX() float32
GetY() float32
GetZ() float32
GetDistance(Entity) float32
FaceTarget(Entity, bool)
IsFollowing() bool
SetFollowing(bool)
GetFollowTarget() Spawn
SetFollowTarget(Spawn, float32)
CalculateRunningLocation(bool)
ClearRunningLocations()
// Runback functionality
IsRunningBack() bool
GetRunbackLocation() *MovementLocation
GetRunbackDistance() float32
Runback(float32)
ShouldCallRunback() bool
SetCallRunback(bool)
// Status effects
IsMezzedOrStunned() bool
IsCasting() bool
IsDazed() bool
IsFeared() bool
IsStifled() bool
InWater() bool
IsWaterCreature() bool
IsFlyingCreature() bool
// Combat mechanics
AttackAllowed(Entity) bool
PrimaryWeaponReady() bool
SecondaryWeaponReady() bool
SetPrimaryLastAttackTime(int64)
SetSecondaryLastAttackTime(int64)
MeleeAttack(Entity, float32, bool)
// Spell casting
GetCastPercentage() int8
GetNextSpell(Entity, float32) Spell
GetNextBuffSpell(Spawn) Spell
SetCastOnAggroCompleted(bool)
CheckLoS(Entity) bool
// Movement pausing
IsPauseMovementTimerActive() bool
// Encounter state
SetEncounterState(int8)
// Scripts
GetSpawnScript() string
// Utility
KillSpawn(NPC)
}
// Entity interface for combat entities
type Entity interface {
Spawn
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
IsPlayer() bool
IsBot() bool
IsPet() bool
GetOwner() Entity
InWater() bool
}
// Spawn interface for basic spawn functionality
type Spawn interface {
GetID() int32
GetName() string
GetX() float32
GetY() float32
GetZ() float32
}
// Spell interface for spell data
type Spell interface {
GetSpellID() int32
GetName() string
IsFriendlySpell() bool
GetCastTime() int32
GetRecoveryTime() int32
GetRange() float32
GetMinRange() float32
}
// Zone interface for zone-related AI operations
type Zone interface {
GetSpawnByID(int32) Spawn
ProcessSpell(spell Spell, caster NPC, target Spawn) error
CallSpawnScript(npc NPC, scriptType string, args ...any) error
}
// Packet types for AI communication
type AIPacketType uint8
const (
AIPacketTypeUpdate AIPacketType = 1
AIPacketTypeStateUpdate AIPacketType = 2
AIPacketTypeHateUpdate AIPacketType = 3
AIPacketTypeEncounter AIPacketType = 4
AIPacketTypeCommand AIPacketType = 5
AIPacketTypeBrainType AIPacketType = 6
AIPacketTypeStatistics AIPacketType = 7
AIPacketTypeDebugUpdate AIPacketType = 8
)
// AIPacket represents an AI communication packet
type AIPacket struct {
Type AIPacketType `json:"type"`
EntityID int32 `json:"entity_id"`
Timestamp int64 `json:"timestamp"`
Data map[string]interface{} `json:"data"`
}
// AIClient interface for AI communication
type AIClient interface {
SendPacket(packet *AIPacket) error
BroadcastPacket(packet *AIPacket) error
}
// AIStatistics contains AI system statistics
type AIStatistics struct {
TotalBrains int `json:"total_brains"`
ActiveBrains int `json:"active_brains"`
TotalThinks int64 `json:"total_thinks"`
BrainsByType map[string]int `json:"brains_by_type"`
}
// BrainStatistics contains brain performance statistics
type BrainStatistics struct {
ThinkCycles int64 `json:"think_cycles"`
SpellsCast int64 `json:"spells_cast"`
MeleeAttacks int64 `json:"melee_attacks"`
HateEvents int64 `json:"hate_events"`
EncounterEvents int64 `json:"encounter_events"`
AverageThinkTime float64 `json:"average_think_time_ms"`
LastThinkTime int64 `json:"last_think_time"`
TotalActiveTime int64 `json:"total_active_time_ms"`
}
// NewBrainStatistics creates new brain statistics
func NewBrainStatistics() *BrainStatistics {
return &BrainStatistics{
ThinkCycles: 0,
SpellsCast: 0,
MeleeAttacks: 0,
HateEvents: 0,
EncounterEvents: 0,
AverageThinkTime: 0.0,
LastThinkTime: time.Now().UnixMilli(),
TotalActiveTime: 0,
}
}
// CreateBrain creates the appropriate brain type for an NPC
func CreateBrain(npc NPC, brainType int8, logger Logger, options ...any) Brain {
switch brainType {
case BrainTypeCombatPet:
return NewCombatPetBrain(npc, logger)
case BrainTypeNonCombatPet:
return NewNonCombatPetBrain(npc, logger)
case BrainTypeBlank:
return NewBlankBrain(npc, logger)
case BrainTypeCustom:
if len(options) > 0 {
if customAI, ok := options[0].(CustomAI); ok {
return NewCustomBrain(npc, logger, customAI)
}
}
return NewBaseBrain(npc, logger) // Fallback to default
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
if expireTime, ok := options[1].(int32); ok {
return NewDumbFirePetBrain(npc, target, expireTime, logger)
}
}
}
return NewBaseBrain(npc, logger) // Fallback to default
default:
return NewBaseBrain(npc, logger)
}
}
// Helper functions
// getBrainTypeName returns the string name for a brain type
func getBrainTypeName(brainType int8) string {
switch brainType {
case BrainTypeDefault:
return "default"
case BrainTypeCombatPet:
return "combat_pet"
case BrainTypeNonCombatPet:
return "non_combat_pet"
case BrainTypeBlank:
return "blank"
case BrainTypeCustom:
return "custom"
case BrainTypeDumbFire:
return "dumbfire"
default:
return "unknown"
}
}
// currentTimeMillis returns current time in milliseconds
func currentTimeMillis() int64 {
return time.Now().UnixMilli()
}
// CreateAIPacket creates a new AI packet
func CreateAIPacket(packetType AIPacketType, entityID int32, data map[string]interface{}) *AIPacket {
return &AIPacket{
Type: packetType,
EntityID: entityID,
Timestamp: currentTimeMillis(),
Data: data,
}
}
// ToJSON converts AI packet to JSON
func (p *AIPacket) ToJSON() ([]byte, error) {
return json.Marshal(p)
}
// FromJSON creates AI packet from JSON
func FromJSON(data []byte) (*AIPacket, error) {
var packet AIPacket
err := json.Unmarshal(data, &packet)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal AI packet: %w", err)
}
return &packet, nil
}

View File

@ -1,633 +0,0 @@
package ai
import (
"fmt"
"sync"
"time"
)
// Brain interface defines the core AI functionality
type Brain interface {
// Core AI methods
Think() error
GetBrainType() int8
// State management
IsActive() bool
SetActive(bool)
GetState() int32
SetState(int32)
// Timing
GetThinkTick() int32
SetThinkTick(int32)
GetLastThink() int64
SetLastThink(int64)
// Body management
GetBody() NPC
SetBody(NPC)
// Hate management
AddHate(entityID int32, hate int32)
GetHate(entityID int32) int32
ClearHate()
ClearHateForEntity(entityID int32)
GetMostHated() int32
GetHatePercentage(entityID int32) int8
GetHateList() map[int32]*HateEntry
// Encounter management
AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool
IsEntityInEncounter(entityID int32) bool
IsPlayerInEncounter(characterID int32) bool
HasPlayerInEncounter() bool
GetEncounterSize() int
ClearEncounter()
CheckLootAllowed(entityID int32) bool
// Combat methods
ProcessSpell(target Entity, distance float32) bool
ProcessMelee(target Entity, distance float32)
CheckBuffs() bool
HasRecovered() bool
MoveCloser(target Spawn)
// Statistics
GetStatistics() *BrainStatistics
ResetStatistics()
}
// BaseBrain provides the default AI implementation
type BaseBrain struct {
npc NPC // The NPC this brain controls
brainType int8 // Type of brain
state *BrainState // Brain state management
hateList *HateList // Hate management
encounterList *EncounterList // Encounter management
statistics *BrainStatistics // Performance statistics
logger Logger // Logger interface
mutex sync.RWMutex // Thread safety
}
// NewBaseBrain creates a new base brain
func NewBaseBrain(npc NPC, logger Logger) *BaseBrain {
return &BaseBrain{
npc: npc,
brainType: BrainTypeDefault,
state: NewBrainState(),
hateList: NewHateList(),
encounterList: NewEncounterList(),
statistics: NewBrainStatistics(),
logger: logger,
}
}
// Think implements the main AI logic
func (bb *BaseBrain) Think() error {
if !bb.IsActive() {
return nil
}
startTime := time.Now()
defer func() {
// Update statistics
bb.mutex.Lock()
bb.statistics.ThinkCycles++
thinkTime := float64(time.Since(startTime).Nanoseconds()) / 1000000.0 // Convert to milliseconds
bb.statistics.AverageThinkTime = (bb.statistics.AverageThinkTime + thinkTime) / 2.0
bb.statistics.LastThinkTime = time.Now().UnixMilli()
bb.mutex.Unlock()
bb.state.SetLastThink(time.Now().UnixMilli())
}()
if bb.npc == nil {
return fmt.Errorf("brain has no body")
}
// Handle pet ID registration for players
if bb.npc.IsPet() && bb.npc.GetOwner() != nil && bb.npc.GetOwner().IsPlayer() {
// TODO: Register pet ID with player's info struct
}
// Get the most hated target
mostHatedID := bb.hateList.GetMostHated()
var target Entity
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
// Remove dead targets from hate list
if target != nil && target.GetHP() <= 0 {
bb.hateList.RemoveHate(mostHatedID)
target = nil
// Try again to get most hated
mostHatedID = bb.hateList.GetMostHated()
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
}
}
}
// Skip if mezzed, stunned, or feared
if bb.npc.IsMezzedOrStunned() {
return nil
}
// Get runback distance
runbackDistance := bb.npc.GetRunbackDistance()
if target != nil {
// We have a target to fight
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s has target %s", bb.npc.GetName(), target.GetName())
}
// Set target if not already set
if bb.npc.GetTarget() != target {
bb.npc.SetTarget(target)
}
// Face the target
bb.npc.FaceTarget(target, false)
// Enter combat if not already in combat
if !bb.npc.GetInCombat() {
bb.npc.ClearRunningLocations()
bb.npc.InCombat(true)
bb.npc.SetCastOnAggroCompleted(false)
// TODO: Call spawn script for aggro
}
// Check chase distance and water restrictions
if bb.shouldBreakPursuit(target, runbackDistance) {
// Break pursuit - clear hate and encounter
if bb.logger != nil {
bb.logger.LogDebug("NPC %s breaking pursuit (distance: %.2f)", bb.npc.GetName(), runbackDistance)
}
// TODO: Send encounter break messages to players
bb.hateList.Clear()
bb.encounterList.Clear()
} else {
// Continue combat
distance := bb.npc.GetDistance(target)
// Try to cast spells first, then melee
if !bb.npc.IsCasting() && (!bb.HasRecovered() || !bb.ProcessSpell(target, distance)) {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s attempting melee on %s", bb.npc.GetName(), target.GetName())
}
bb.npc.FaceTarget(target, false)
bb.ProcessMelee(target, distance)
}
}
} else {
// No target - handle out of combat behavior
if bb.npc.GetInCombat() {
bb.npc.InCombat(false)
// Restore HP for non-player pets
if !bb.npc.IsPet() || (bb.npc.IsPet() && bb.npc.GetOwner() != nil && !bb.npc.GetOwner().IsPlayer()) {
bb.npc.SetHP(bb.npc.GetTotalHP())
}
}
// Check for buffs when not in combat
bb.CheckBuffs()
// Handle runback if needed
if !bb.npc.GetInCombat() && !bb.npc.IsPauseMovementTimerActive() {
if runbackDistance > RunbackThreshold || (bb.npc.ShouldCallRunback() && !bb.npc.IsFollowing()) {
bb.npc.SetEncounterState(EncounterStateBroken)
bb.npc.Runback(runbackDistance)
bb.npc.SetCallRunback(false)
} else if bb.npc.GetRunbackLocation() != nil {
bb.handleRunbackStages()
}
}
// Clear encounter if any entities remain
if bb.encounterList.Size() > 0 {
bb.encounterList.Clear()
}
}
return nil
}
// GetBrainType returns the brain type
func (bb *BaseBrain) GetBrainType() int8 {
return bb.brainType
}
// IsActive returns whether the brain is active
func (bb *BaseBrain) IsActive() bool {
return bb.state.IsActive()
}
// SetActive sets the brain's active state
func (bb *BaseBrain) SetActive(active bool) {
bb.state.SetActive(active)
}
// GetState returns the current AI state
func (bb *BaseBrain) GetState() int32 {
return bb.state.GetState()
}
// SetState sets the current AI state
func (bb *BaseBrain) SetState(state int32) {
bb.state.SetState(state)
}
// GetThinkTick returns the think tick interval
func (bb *BaseBrain) GetThinkTick() int32 {
return bb.state.GetThinkTick()
}
// SetThinkTick sets the think tick interval
func (bb *BaseBrain) SetThinkTick(tick int32) {
bb.state.SetThinkTick(tick)
}
// GetLastThink returns the timestamp of the last think cycle
func (bb *BaseBrain) GetLastThink() int64 {
return bb.state.GetLastThink()
}
// SetLastThink sets the timestamp of the last think cycle
func (bb *BaseBrain) SetLastThink(timestamp int64) {
bb.state.SetLastThink(timestamp)
}
// GetBody returns the NPC this brain controls
func (bb *BaseBrain) GetBody() NPC {
bb.mutex.RLock()
defer bb.mutex.RUnlock()
return bb.npc
}
// SetBody sets the NPC this brain controls
func (bb *BaseBrain) SetBody(npc NPC) {
bb.mutex.Lock()
defer bb.mutex.Unlock()
bb.npc = npc
}
// AddHate adds hate for an entity
func (bb *BaseBrain) AddHate(entityID int32, hate int32) {
// Don't add hate while running back
if bb.npc != nil && bb.npc.IsRunningBack() {
return
}
// Don't add hate if owner is attacking pet
if bb.npc != nil && bb.npc.IsPet() && bb.npc.GetOwner() != nil {
if bb.npc.GetOwner().GetID() == entityID {
return
}
}
// Check for taunt immunity
// TODO: Implement immunity checking
bb.hateList.AddHate(entityID, hate)
// Update statistics
bb.mutex.Lock()
bb.statistics.HateEvents++
bb.mutex.Unlock()
// TODO: Add to entity's HatedBy list
// Add pet owner to hate list if not already present
entity := bb.getEntityByID(entityID)
if entity != nil && entity.IsPet() && entity.GetOwner() != nil {
ownerID := entity.GetOwner().GetID()
if bb.hateList.GetHate(ownerID) == 0 {
bb.hateList.AddHate(ownerID, 0)
}
}
}
// GetHate returns the hate value for an entity
func (bb *BaseBrain) GetHate(entityID int32) int32 {
return bb.hateList.GetHate(entityID)
}
// ClearHate removes all hate entries
func (bb *BaseBrain) ClearHate() {
bb.hateList.Clear()
// TODO: Update entities' HatedBy lists
}
// ClearHateForEntity removes hate for a specific entity
func (bb *BaseBrain) ClearHateForEntity(entityID int32) {
bb.hateList.RemoveHate(entityID)
// TODO: Update entity's HatedBy list
}
// GetMostHated returns the ID of the most hated entity
func (bb *BaseBrain) GetMostHated() int32 {
return bb.hateList.GetMostHated()
}
// GetHatePercentage returns the hate percentage for an entity
func (bb *BaseBrain) GetHatePercentage(entityID int32) int8 {
return bb.hateList.GetHatePercentage(entityID)
}
// GetHateList returns a copy of the hate list
func (bb *BaseBrain) GetHateList() map[int32]*HateEntry {
return bb.hateList.GetAllEntries()
}
// AddToEncounter adds an entity to the encounter list
func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool {
success := bb.encounterList.AddEntity(entityID, characterID, isPlayer, isBot)
if success {
bb.mutex.Lock()
bb.statistics.EncounterEvents++
bb.mutex.Unlock()
}
return success
}
// IsEntityInEncounter checks if an entity is in the encounter
func (bb *BaseBrain) IsEntityInEncounter(entityID int32) bool {
return bb.encounterList.IsEntityInEncounter(entityID)
}
// IsPlayerInEncounter checks if a player is in the encounter
func (bb *BaseBrain) IsPlayerInEncounter(characterID int32) bool {
return bb.encounterList.IsPlayerInEncounter(characterID)
}
// HasPlayerInEncounter returns whether any player is in the encounter
func (bb *BaseBrain) HasPlayerInEncounter() bool {
return bb.encounterList.HasPlayerInEncounter()
}
// GetEncounterSize returns the size of the encounter list
func (bb *BaseBrain) GetEncounterSize() int {
return bb.encounterList.Size()
}
// ClearEncounter removes all entities from the encounter
func (bb *BaseBrain) ClearEncounter() {
bb.encounterList.Clear()
// TODO: Remove spells from NPC
}
// CheckLootAllowed checks if an entity can loot this NPC
func (bb *BaseBrain) CheckLootAllowed(entityID int32) bool {
// TODO: Implement loot method checking, chest timers, etc.
// Basic check - is entity in encounter?
return bb.encounterList.IsEntityInEncounter(entityID)
}
// ProcessSpell attempts to cast a spell
func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool {
if bb.npc == nil {
return false
}
// Check cast percentage and conditions
castChance := bb.npc.GetCastPercentage()
if castChance <= 0 {
return false
}
// TODO: Implement random chance checking
// TODO: Check for stifled, feared conditions
// Get next spell to cast
spell := bb.npc.GetNextSpell(target, distance)
if spell == nil {
return false
}
// Determine spell target
var spellTarget Spawn = target
if spell.IsFriendlySpell() {
// TODO: Find best friendly target (lowest HP group member)
spellTarget = bb.npc
}
// Cast the spell
success := bb.castSpell(spell, spellTarget, false)
if success {
bb.mutex.Lock()
bb.statistics.SpellsCast++
bb.mutex.Unlock()
}
return success
}
// ProcessMelee handles melee combat
func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if distance > maxCombatRange {
bb.MoveCloser(target)
} else {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s is within melee range of %s", bb.npc.GetName(), target.GetName())
}
// Check if attack is allowed
if !bb.npc.AttackAllowed(target) {
return
}
currentTime := time.Now().UnixMilli()
// Primary weapon attack
if bb.npc.PrimaryWeaponReady() && !bb.npc.IsDazed() && !bb.npc.IsFeared() {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelVerbose {
bb.logger.LogDebug("NPC %s swings primary weapon at %s", bb.npc.GetName(), target.GetName())
}
bb.npc.SetPrimaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, true)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
// TODO: Call spawn script for auto attack tick
}
// Secondary weapon attack
if bb.npc.SecondaryWeaponReady() && !bb.npc.IsDazed() {
bb.npc.SetSecondaryLastAttackTime(currentTime)
bb.npc.MeleeAttack(target, distance, false)
bb.mutex.Lock()
bb.statistics.MeleeAttacks++
bb.mutex.Unlock()
}
}
}
// CheckBuffs checks and casts buff spells
func (bb *BaseBrain) CheckBuffs() bool {
if bb.npc == nil {
return false
}
// Don't buff in combat, while casting, stunned, etc.
if bb.npc.GetInCombat() || bb.npc.IsCasting() || bb.npc.IsMezzedOrStunned() ||
!bb.npc.IsAlive() || bb.npc.IsStifled() || !bb.HasRecovered() {
return false
}
// Get next buff spell
buffSpell := bb.npc.GetNextBuffSpell(bb.npc)
if buffSpell == nil {
return false
}
// Try to cast on self first
if bb.castSpell(buffSpell, bb.npc, false) {
return true
}
// TODO: Try to buff group members
return false
}
// HasRecovered checks if the brain has recovered from spell casting
func (bb *BaseBrain) HasRecovered() bool {
return bb.state.HasRecovered()
}
// MoveCloser moves the NPC closer to a target
func (bb *BaseBrain) MoveCloser(target Spawn) {
if bb.npc == nil || target == nil {
return
}
maxCombatRange := bb.getMaxCombatRange()
if bb.npc.GetFollowTarget() != target {
bb.npc.SetFollowTarget(target, maxCombatRange)
}
if bb.npc.GetFollowTarget() != nil && !bb.npc.IsFollowing() {
bb.npc.CalculateRunningLocation(true)
bb.npc.SetFollowing(true)
}
}
// GetStatistics returns brain performance statistics
func (bb *BaseBrain) GetStatistics() *BrainStatistics {
bb.mutex.RLock()
defer bb.mutex.RUnlock()
// Return a copy
return &BrainStatistics{
ThinkCycles: bb.statistics.ThinkCycles,
SpellsCast: bb.statistics.SpellsCast,
MeleeAttacks: bb.statistics.MeleeAttacks,
HateEvents: bb.statistics.HateEvents,
EncounterEvents: bb.statistics.EncounterEvents,
AverageThinkTime: bb.statistics.AverageThinkTime,
LastThinkTime: bb.statistics.LastThinkTime,
TotalActiveTime: bb.statistics.TotalActiveTime,
}
}
// ResetStatistics resets all performance statistics
func (bb *BaseBrain) ResetStatistics() {
bb.mutex.Lock()
defer bb.mutex.Unlock()
bb.statistics = NewBrainStatistics()
}
// Helper methods
// shouldBreakPursuit checks if the NPC should break pursuit of a target
func (bb *BaseBrain) shouldBreakPursuit(target Entity, runbackDistance float32) bool {
if target == nil {
return false
}
// Check max chase distance
maxChase := bb.getMaxChaseDistance()
if runbackDistance > maxChase {
return true
}
// Check water creature restrictions
if bb.npc != nil && bb.npc.IsWaterCreature() && !bb.npc.IsFlyingCreature() && !target.InWater() {
return true
}
return false
}
// castSpell casts a spell on a target
func (bb *BaseBrain) castSpell(spell Spell, target Spawn, calculateRunLoc bool) bool {
if spell == nil || bb.npc == nil {
return false
}
if calculateRunLoc {
bb.npc.CalculateRunningLocation(true)
}
// TODO: Process spell through zone
// bb.npc.GetZone().ProcessSpell(spell, bb.npc, target)
// Set spell recovery time
castTime := spell.GetCastTime() * RecoveryTimeMultiple
recoveryTime := spell.GetRecoveryTime() * RecoveryTimeMultiple
totalRecovery := time.Now().UnixMilli() + int64(castTime) + int64(recoveryTime) + int64(SpellRecoveryBuffer)
bb.state.SetSpellRecovery(totalRecovery)
return true
}
// handleRunbackStages handles the various stages of runback
func (bb *BaseBrain) handleRunbackStages() {
if bb.npc == nil {
return
}
runbackLoc := bb.npc.GetRunbackLocation()
if runbackLoc == nil {
return
}
// TODO: Implement runback stage handling
// This would involve movement management and position updates
}
// getEntityByID retrieves an entity by ID (placeholder)
func (bb *BaseBrain) getEntityByID(entityID int32) Entity {
// TODO: Implement entity lookup through zone
return nil
}
// getMaxChaseDistance returns the maximum chase distance
func (bb *BaseBrain) getMaxChaseDistance() float32 {
// TODO: Check NPC info struct and zone rules
return MaxChaseDistance
}
// getMaxCombatRange returns the maximum combat range
func (bb *BaseBrain) getMaxCombatRange() float32 {
// TODO: Check zone rules
return MaxCombatRange
}

664
internal/npc/ai/brains.go Normal file
View File

@ -0,0 +1,664 @@
package ai
import (
"fmt"
"time"
)
// BaseBrain implements the basic AI brain functionality
type BaseBrain struct {
npc NPC
logger Logger
brainType int8
state *BrainState
hateList *HateList
encounter *EncounterList
statistics *BrainStatistics
}
// NewBaseBrain creates a new base brain
func NewBaseBrain(npc NPC, logger Logger) *BaseBrain {
return &BaseBrain{
npc: npc,
logger: logger,
brainType: BrainTypeDefault,
state: NewBrainState(),
hateList: NewHateList(),
encounter: NewEncounterList(),
statistics: NewBrainStatistics(),
}
}
// Think implements the main AI logic
func (bb *BaseBrain) Think() error {
if bb.npc == nil {
return fmt.Errorf("brain has no body")
}
// Update last think time
currentTime := time.Now().UnixMilli()
bb.state.SetLastThink(currentTime)
bb.statistics.ThinkCycles++
// Skip thinking if mezzzed, stunned, feared, or dazed
if bb.npc.IsMezzedOrStunned() || bb.npc.IsFeared() || bb.npc.IsDazed() {
return nil
}
// Check if we need to runback
if bb.npc.IsRunningBack() || bb.npc.ShouldCallRunback() {
return bb.handleRunback()
}
// Get most hated target
mostHatedID := bb.hateList.GetMostHated()
var target Entity
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
if target == nil {
// Target no longer exists, remove from hate list
bb.hateList.RemoveHate(mostHatedID)
mostHatedID = bb.hateList.GetMostHated()
if mostHatedID > 0 {
target = bb.getEntityByID(mostHatedID)
}
}
}
// Handle combat state
if target != nil && bb.npc.GetInCombat() {
return bb.handleCombat(target)
} else if bb.npc.GetInCombat() {
// No target but still in combat - exit combat
bb.npc.InCombat(false)
bb.npc.SetTarget(nil)
bb.state.SetState(AIStateIdle)
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic {
bb.logger.LogDebug("NPC %s exiting combat - no targets", bb.npc.GetName())
}
return nil
}
// Handle idle state
return bb.handleIdle()
}
// handleCombat handles combat AI logic
func (bb *BaseBrain) handleCombat(target Entity) error {
if target == nil {
return nil
}
// Set target if not already set
if bb.npc.GetTarget() != target {
bb.npc.SetTarget(target)
bb.npc.FaceTarget(target, false)
}
distance := bb.npc.GetDistance(target)
// Check if target is too far away
maxRange := bb.getMaxCombatRange()
if distance > maxRange {
// Try to move closer or runback if too far
if distance > MaxChaseDistance {
if bb.npc.ShouldCallRunback() {
bb.npc.Runback(bb.npc.GetRunbackDistance())
return nil
}
} else {
bb.MoveCloser(target)
}
return nil
}
bb.state.SetState(AIStateCombat)
// Try to cast spells first if we have line of sight
if bb.npc.CheckLoS(target) && !bb.npc.IsCasting() {
if bb.HasRecovered() && bb.ProcessSpell(target, distance) {
return nil
}
}
// Try melee combat
bb.ProcessMelee(target, distance)
return nil
}
// handleIdle handles idle AI logic
func (bb *BaseBrain) handleIdle() error {
bb.state.SetState(AIStateIdle)
// Set slower think tick when idle
bb.state.SetThinkTick(SlowThinkTick)
return nil
}
// handleRunback handles runback logic
func (bb *BaseBrain) handleRunback() error {
// Clear hate and encounter when running back
bb.hateList.Clear()
bb.encounter.Clear()
// Set target to nil
bb.npc.SetTarget(nil)
bb.npc.InCombat(false)
bb.state.SetState(AIStateRunback)
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s running back", bb.npc.GetName())
}
return nil
}
// ProcessSpell processes spell casting for the brain
func (bb *BaseBrain) ProcessSpell(target Entity, distance float32) bool {
if bb.npc == nil || target == nil {
return false
}
// Check if we can cast
if bb.npc.IsCasting() || !bb.HasRecovered() {
return false
}
// Get next spell to cast
spell := bb.npc.GetNextSpell(target, distance)
if spell == nil {
return false
}
// Check range
if distance < spell.GetMinRange() || distance > spell.GetRange() {
return false
}
// Cast the spell
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s casting spell %s on %s",
bb.npc.GetName(), spell.GetName(), target.GetName())
}
// Set spell recovery time
recoveryTime := time.Now().UnixMilli() + int64(spell.GetRecoveryTime()*RecoveryTimeMultiple) + int64(SpellRecoveryBuffer)
bb.state.SetSpellRecovery(recoveryTime)
bb.state.SetState(AIStateCasting)
bb.statistics.SpellsCast++
return true
}
// ProcessMelee processes melee combat for the brain
func (bb *BaseBrain) ProcessMelee(target Entity, distance float32) {
if bb.npc == nil || target == nil {
return
}
// Check if we can attack
if !bb.npc.AttackAllowed(target) {
return
}
// Try primary weapon
if bb.npc.PrimaryWeaponReady() {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s melee attacking %s with primary weapon",
bb.npc.GetName(), target.GetName())
}
bb.npc.MeleeAttack(target, distance, true)
bb.npc.SetPrimaryLastAttackTime(time.Now().UnixMilli())
bb.statistics.MeleeAttacks++
return
}
// Try secondary weapon
if bb.npc.SecondaryWeaponReady() {
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s melee attacking %s with secondary weapon",
bb.npc.GetName(), target.GetName())
}
bb.npc.MeleeAttack(target, distance, false)
bb.npc.SetSecondaryLastAttackTime(time.Now().UnixMilli())
bb.statistics.MeleeAttacks++
}
}
// MoveCloser moves the NPC closer to the target
func (bb *BaseBrain) MoveCloser(target Entity) {
if bb.npc == nil || target == nil {
return
}
// Face the target
bb.npc.FaceTarget(target, false)
// Calculate running location towards target
bb.npc.CalculateRunningLocation(true)
bb.state.SetState(AIStateMoving)
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s moving closer to %s", bb.npc.GetName(), target.GetName())
}
}
// Brain interface implementation
// GetBrainType returns the brain type
func (bb *BaseBrain) GetBrainType() int8 {
return bb.brainType
}
// IsActive returns whether the brain is active
func (bb *BaseBrain) IsActive() bool {
return bb.state.IsActive()
}
// SetActive sets the brain's active state
func (bb *BaseBrain) SetActive(active bool) {
bb.state.SetActive(active)
}
// GetLastThink returns the timestamp of the last think cycle
func (bb *BaseBrain) GetLastThink() int64 {
return bb.state.GetLastThink()
}
// SetLastThink sets the timestamp of the last think cycle
func (bb *BaseBrain) SetLastThink(timestamp int64) {
bb.state.SetLastThink(timestamp)
}
// GetThinkTick returns the think tick interval
func (bb *BaseBrain) GetThinkTick() int32 {
return bb.state.GetThinkTick()
}
// SetThinkTick sets the think tick interval
func (bb *BaseBrain) SetThinkTick(tick int32) {
bb.state.SetThinkTick(tick)
}
// Hate management
// AddHate adds hate for an entity
func (bb *BaseBrain) AddHate(entityID, hate int32) {
bb.hateList.AddHate(entityID, hate)
bb.statistics.HateEvents++
// Enter combat if not already
if !bb.npc.GetInCombat() {
bb.npc.InCombat(true)
bb.state.SetThinkTick(FastThinkTick)
}
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s gained %d hate for entity %d", bb.npc.GetName(), hate, entityID)
}
}
// GetHate returns hate for an entity
func (bb *BaseBrain) GetHate(entityID int32) int32 {
return bb.hateList.GetHate(entityID)
}
// GetMostHated returns the most hated entity ID
func (bb *BaseBrain) GetMostHated() int32 {
return bb.hateList.GetMostHated()
}
// ClearHate clears all hate
func (bb *BaseBrain) ClearHate() {
bb.hateList.Clear()
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic {
bb.logger.LogDebug("NPC %s cleared all hate", bb.npc.GetName())
}
}
// ClearHateForEntity clears hate for a specific entity
func (bb *BaseBrain) ClearHateForEntity(entityID int32) {
bb.hateList.RemoveHate(entityID)
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s cleared hate for entity %d", bb.npc.GetName(), entityID)
}
}
// Encounter management
// AddToEncounter adds an entity to the encounter
func (bb *BaseBrain) AddToEncounter(entityID, characterID int32, isPlayer, isBot bool) bool {
success := bb.encounter.AddEntity(entityID, characterID, isPlayer, isBot)
if success {
bb.statistics.EncounterEvents++
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s added entity %d to encounter", bb.npc.GetName(), entityID)
}
}
return success
}
// RemoveFromEncounter removes an entity from the encounter
func (bb *BaseBrain) RemoveFromEncounter(entityID int32) {
bb.encounter.RemoveEntity(entityID)
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelDetailed {
bb.logger.LogDebug("NPC %s removed entity %d from encounter", bb.npc.GetName(), entityID)
}
}
// IsInEncounter checks if an entity is in the encounter
func (bb *BaseBrain) IsInEncounter(entityID int32) bool {
return bb.encounter.IsEntityInEncounter(entityID)
}
// ClearEncounter clears the encounter
func (bb *BaseBrain) ClearEncounter() {
bb.encounter.Clear()
if bb.logger != nil && bb.state.GetDebugLevel() >= DebugLevelBasic {
bb.logger.LogDebug("NPC %s cleared encounter", bb.npc.GetName())
}
}
// GetEncounterSize returns the encounter size
func (bb *BaseBrain) GetEncounterSize() int {
return bb.encounter.Size()
}
// State management
// GetState returns the current AI state
func (bb *BaseBrain) GetState() int32 {
return bb.state.GetState()
}
// SetState sets the current AI state
func (bb *BaseBrain) SetState(state int32) {
bb.state.SetState(state)
}
// HasRecovered checks if the brain has recovered from spell casting
func (bb *BaseBrain) HasRecovered() bool {
return bb.state.HasRecovered()
}
// SetSpellRecovery sets the spell recovery time
func (bb *BaseBrain) SetSpellRecovery(timestamp int64) {
bb.state.SetSpellRecovery(timestamp)
}
// GetStatistics returns brain statistics
func (bb *BaseBrain) GetStatistics() *BrainStatistics {
return bb.statistics
}
// GetDebugLevel returns the debug level
func (bb *BaseBrain) GetDebugLevel() int8 {
return bb.state.GetDebugLevel()
}
// SetDebugLevel sets the debug level
func (bb *BaseBrain) SetDebugLevel(level int8) {
bb.state.SetDebugLevel(level)
}
// Helper methods
// getEntityByID gets an entity by ID (placeholder - would be implemented by zone)
func (bb *BaseBrain) getEntityByID(entityID int32) Entity {
// This would typically call into the zone system to get the entity
// For now, return nil - concrete implementations should override this
return nil
}
// getMaxCombatRange returns the maximum combat range
func (bb *BaseBrain) getMaxCombatRange() float32 {
return MaxCombatRange
}
// Pet Brain Implementations
// CombatPetBrain extends the base brain for combat pets
type CombatPetBrain struct {
*BaseBrain
}
// NewCombatPetBrain creates a new combat pet brain
func NewCombatPetBrain(npc NPC, logger Logger) *CombatPetBrain {
brain := &CombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeCombatPet
return brain
}
// Think implements pet-specific AI logic
func (cpb *CombatPetBrain) Think() error {
// Call parent Think() for default combat behavior
if err := cpb.BaseBrain.Think(); err != nil {
return err
}
// Additional pet-specific logic
if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() {
return nil
}
if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed {
cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName())
}
// Check if owner has stay command set
owner := cpb.npc.GetOwner()
if owner != nil && owner.IsPlayer() {
// TODO: Check player's pet movement setting
// if player.GetInfoStruct().GetPetMovement() == PetMovementStay {
// return nil
// }
}
// Follow owner
if owner != nil {
cpb.npc.SetTarget(owner)
distance := cpb.npc.GetDistance(owner)
maxRange := cpb.getMaxCombatRange()
if distance > maxRange {
cpb.MoveCloser(owner)
}
}
return nil
}
// NonCombatPetBrain handles non-combat pets (cosmetic pets)
type NonCombatPetBrain struct {
*BaseBrain
}
// NewNonCombatPetBrain creates a new non-combat pet brain
func NewNonCombatPetBrain(npc NPC, logger Logger) *NonCombatPetBrain {
brain := &NonCombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeNonCombatPet
return brain
}
// Think implements non-combat pet AI (just following)
func (ncpb *NonCombatPetBrain) Think() error {
// Non-combat pets don't do combat AI
if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() {
return nil
}
if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed {
ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName())
}
// Just follow owner
owner := ncpb.npc.GetOwner()
if owner != nil {
ncpb.npc.SetTarget(owner)
distance := ncpb.npc.GetDistance(owner)
maxRange := ncpb.getMaxCombatRange()
if distance > maxRange {
ncpb.MoveCloser(owner)
}
}
return nil
}
// BlankBrain provides a minimal AI that does nothing
type BlankBrain struct {
*BaseBrain
}
// NewBlankBrain creates a new blank brain
func NewBlankBrain(npc NPC, logger Logger) *BlankBrain {
brain := &BlankBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeBlank
brain.SetThinkTick(BlankBrainTick) // Very slow tick
return brain
}
// Think does nothing for blank brains
func (bb *BlankBrain) Think() error {
// Blank brain does nothing
return nil
}
// DumbFirePetBrain handles dumbfire pets (temporary combat pets)
type DumbFirePetBrain struct {
*BaseBrain
expireTime int64
}
// NewDumbFirePetBrain creates a new dumbfire pet brain
func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logger) *DumbFirePetBrain {
brain := &DumbFirePetBrain{
BaseBrain: NewBaseBrain(npc, logger),
expireTime: time.Now().UnixMilli() + int64(expireTimeMS),
}
brain.brainType = BrainTypeDumbFire
// Add maximum hate for the target
if target != nil {
brain.AddHate(target.GetID(), MaxHateValue)
}
return brain
}
// AddHate only allows hate for the initial target
func (dfpb *DumbFirePetBrain) AddHate(entityID int32, hate int32) {
// Only add hate if we don't already have a target
if dfpb.GetMostHated() == 0 {
dfpb.BaseBrain.AddHate(entityID, hate)
}
}
// Think implements dumbfire pet AI
func (dfpb *DumbFirePetBrain) Think() error {
// Check if expired
if time.Now().UnixMilli() > dfpb.expireTime {
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s expired", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Get target
targetID := dfpb.GetMostHated()
if targetID == 0 {
// No target, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s has no target", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
target := dfpb.getEntityByID(targetID)
if target == nil {
// Target no longer exists, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Skip if mezzed or stunned
if dfpb.npc.IsMezzedOrStunned() {
return nil
}
// Set target if not already set
if dfpb.npc.GetTarget() != target {
dfpb.npc.SetTarget(target)
dfpb.npc.FaceTarget(target, false)
}
// Enter combat if not already
if !dfpb.npc.GetInCombat() {
dfpb.npc.CalculateRunningLocation(true)
dfpb.npc.InCombat(true)
}
distance := dfpb.npc.GetDistance(target)
// Try to cast spells if we have line of sight
if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() &&
(!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) {
if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed {
dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s",
dfpb.npc.GetName(), target.GetName())
}
dfpb.npc.FaceTarget(target, false)
dfpb.ProcessMelee(target, distance)
}
return nil
}
// GetExpireTime returns when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) GetExpireTime() int64 {
return dfpb.expireTime
}
// SetExpireTime sets when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) SetExpireTime(expireTime int64) {
dfpb.expireTime = expireTime
}
// IsExpired checks if the dumbfire pet has expired
func (dfpb *DumbFirePetBrain) IsExpired() bool {
return time.Now().UnixMilli() > dfpb.expireTime
}
// ExtendExpireTime extends the expire time by the given duration
func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) {
dfpb.expireTime += int64(durationMS)
}

View File

@ -1,90 +0,0 @@
package ai
// AI tick constants
const (
DefaultThinkTick int32 = 250 // Default think tick in milliseconds (1/4 second)
FastThinkTick int32 = 100 // Fast think tick for active AI
SlowThinkTick int32 = 1000 // Slow think tick for idle AI
BlankBrainTick int32 = 50000 // Very slow tick for blank brain
MaxThinkTick int32 = 60000 // Maximum think tick (1 minute)
)
// Combat constants
const (
MaxChaseDistance float32 = 150.0 // Default max chase distance
MaxCombatRange float32 = 25.0 // Default max combat range
RunbackThreshold float32 = 1.0 // Distance threshold for runback
)
// Hate system constants
const (
MinHateValue int32 = 1 // Minimum hate value (0 or negative is invalid)
MaxHateValue int32 = 2147483647 // Maximum hate value (INT_MAX)
DefaultHateValue int32 = 100 // Default hate amount
MaxHateListSize int = 100 // Maximum entities in hate list
)
// Encounter system constants
const (
MaxEncounterSize int = 50 // Maximum entities in encounter list
)
// Spell recovery constants
const (
SpellRecoveryBuffer int32 = 2000 // Additional recovery time buffer (2 seconds)
)
// Brain type constants for identification
const (
BrainTypeDefault int8 = 0
BrainTypeCombatPet int8 = 1
BrainTypeNonCombatPet int8 = 2
BrainTypeBlank int8 = 3
BrainTypeLua int8 = 4
BrainTypeDumbFire int8 = 5
)
// Pet movement constants
const (
PetMovementFollow int8 = 0
PetMovementStay int8 = 1
PetMovementGuard int8 = 2
)
// Encounter state constants
const (
EncounterStateAvailable int8 = 0
EncounterStateLocked int8 = 1
EncounterStateBroken int8 = 2
)
// Combat decision constants
const (
MeleeAttackChance int = 70 // Base chance for melee attack
SpellCastChance int = 30 // Base chance for spell casting
BuffCheckChance int = 50 // Chance to check for buffs
)
// AI state flags
const (
AIStateIdle int32 = 0
AIStateCombat int32 = 1
AIStateFollowing int32 = 2
AIStateRunback int32 = 3
AIStateCasting int32 = 4
AIStateMoving int32 = 5
)
// Debug levels
const (
DebugLevelNone int8 = 0
DebugLevelBasic int8 = 1
DebugLevelDetailed int8 = 2
DebugLevelVerbose int8 = 3
)
// Timer constants
const (
MillisecondsPerSecond int32 = 1000
RecoveryTimeMultiple int32 = 10 // Multiply cast/recovery times by 10
)

227
internal/npc/ai/custom.go Normal file
View File

@ -0,0 +1,227 @@
package ai
import (
"fmt"
)
// CustomAI defines a Go-based custom AI behavior interface
type CustomAI interface {
// Think is called every AI tick for custom behavior
Think(npc NPC, target Entity, brain Brain) error
// OnCombatStart is called when entering combat
OnCombatStart(npc NPC, target Entity, brain Brain) error
// OnCombatEnd is called when leaving combat
OnCombatEnd(npc NPC, brain Brain) error
// OnDamageReceived is called when NPC takes damage
OnDamageReceived(npc NPC, attacker Entity, damage int32, brain Brain) error
// OnTargetChanged is called when NPC changes target
OnTargetChanged(npc NPC, oldTarget, newTarget Entity, brain Brain) error
}
// CustomBrain allows AI to be controlled by Go-based custom AI implementations
type CustomBrain struct {
*BaseBrain
customAI CustomAI
}
// NewCustomBrain creates a new custom AI brain
func NewCustomBrain(npc NPC, logger Logger, customAI CustomAI) *CustomBrain {
brain := &CustomBrain{
BaseBrain: NewBaseBrain(npc, logger),
customAI: customAI,
}
brain.brainType = BrainTypeCustom
return brain
}
// Think calls the custom AI's Think function
func (cb *CustomBrain) Think() error {
if cb.customAI == nil {
// Fall back to default behavior if no custom AI
return cb.BaseBrain.Think()
}
if cb.npc == nil {
return fmt.Errorf("brain has no body")
}
target := cb.npc.GetTarget()
err := cb.customAI.Think(cb.npc, target, cb)
if err != nil {
if cb.logger != nil {
cb.logger.LogError("Custom AI Think function failed: %v", err)
}
return fmt.Errorf("custom AI Think function failed: %w", err)
}
return nil
}
// SetCustomAI sets the custom AI implementation
func (cb *CustomBrain) SetCustomAI(customAI CustomAI) {
cb.customAI = customAI
}
// GetCustomAI returns the custom AI implementation
func (cb *CustomBrain) GetCustomAI() CustomAI {
return cb.customAI
}
// NotifyCombatStart notifies custom AI of combat start
func (cb *CustomBrain) NotifyCombatStart(target Entity) {
if cb.customAI != nil {
if err := cb.customAI.OnCombatStart(cb.npc, target, cb); err != nil && cb.logger != nil {
cb.logger.LogError("Custom AI OnCombatStart failed: %v", err)
}
}
}
// NotifyCombatEnd notifies custom AI of combat end
func (cb *CustomBrain) NotifyCombatEnd() {
if cb.customAI != nil {
if err := cb.customAI.OnCombatEnd(cb.npc, cb); err != nil && cb.logger != nil {
cb.logger.LogError("Custom AI OnCombatEnd failed: %v", err)
}
}
}
// NotifyDamageReceived notifies custom AI of damage received
func (cb *CustomBrain) NotifyDamageReceived(attacker Entity, damage int32) {
if cb.customAI != nil {
if err := cb.customAI.OnDamageReceived(cb.npc, attacker, damage, cb); err != nil && cb.logger != nil {
cb.logger.LogError("Custom AI OnDamageReceived failed: %v", err)
}
}
}
// NotifyTargetChanged notifies custom AI of target change
func (cb *CustomBrain) NotifyTargetChanged(oldTarget, newTarget Entity) {
if cb.customAI != nil {
if err := cb.customAI.OnTargetChanged(cb.npc, oldTarget, newTarget, cb); err != nil && cb.logger != nil {
cb.logger.LogError("Custom AI OnTargetChanged failed: %v", err)
}
}
}
// Example Custom AI implementations for demonstration
// AggressiveAI implements CustomAI for aggressive behavior
type AggressiveAI struct{}
// Think implements aggressive AI behavior
func (a *AggressiveAI) Think(npc NPC, target Entity, brain Brain) error {
// Always try to attack if we have a target
if target != nil {
distance := npc.GetDistance(target)
if distance <= 25.0 { // Within range
// Force attack more frequently than normal
if npc.PrimaryWeaponReady() {
npc.MeleeAttack(target, distance, true)
}
}
}
// Fall back to base brain behavior for movement, spells, etc.
return brain.(*CustomBrain).BaseBrain.Think()
}
// OnCombatStart implements aggressive combat start
func (a *AggressiveAI) OnCombatStart(npc NPC, target Entity, brain Brain) error {
// Add extra hate when combat starts
if target != nil {
brain.AddHate(target.GetID(), DefaultHateValue*2)
}
return nil
}
// OnCombatEnd implements aggressive combat end
func (a *AggressiveAI) OnCombatEnd(npc NPC, brain Brain) error {
// Stay alert longer after combat
brain.SetThinkTick(FastThinkTick)
return nil
}
// OnDamageReceived implements aggressive damage response
func (a *AggressiveAI) OnDamageReceived(npc NPC, attacker Entity, damage int32, brain Brain) error {
// Add extra hate when damaged
if attacker != nil {
brain.AddHate(attacker.GetID(), damage*2)
}
return nil
}
// OnTargetChanged implements aggressive target change
func (a *AggressiveAI) OnTargetChanged(npc NPC, oldTarget, newTarget Entity, brain Brain) error {
// No special behavior for target changes in aggressive AI
return nil
}
// DefensiveAI implements CustomAI for defensive behavior
type DefensiveAI struct {
fleeThreshold int32 // HP threshold to flee at
}
// NewDefensiveAI creates a defensive AI with flee threshold
func NewDefensiveAI(fleeThreshold int32) *DefensiveAI {
return &DefensiveAI{
fleeThreshold: fleeThreshold,
}
}
// Think implements defensive AI behavior
func (d *DefensiveAI) Think(npc NPC, target Entity, brain Brain) error {
// Check if we should flee
if npc.GetHP() <= d.fleeThreshold {
// Try to run back to spawn point
if npc.ShouldCallRunback() {
npc.Runback(npc.GetRunbackDistance())
}
return nil
}
// Otherwise use normal behavior but with slower think tick
brain.SetThinkTick(SlowThinkTick)
return brain.(*CustomBrain).BaseBrain.Think()
}
// OnCombatStart implements defensive combat start
func (d *DefensiveAI) OnCombatStart(npc NPC, target Entity, brain Brain) error {
// No special behavior for defensive AI
return nil
}
// OnCombatEnd implements defensive combat end
func (d *DefensiveAI) OnCombatEnd(npc NPC, brain Brain) error {
// Slow down thinking after combat
brain.SetThinkTick(SlowThinkTick)
return nil
}
// OnDamageReceived implements defensive damage response
func (d *DefensiveAI) OnDamageReceived(npc NPC, attacker Entity, damage int32, brain Brain) error {
// Check if we should start fleeing
if npc.GetHP() <= d.fleeThreshold && attacker != nil {
// Remove hate and try to flee
brain.ClearHateForEntity(attacker.GetID())
}
return nil
}
// OnTargetChanged implements defensive target change
func (d *DefensiveAI) OnTargetChanged(npc NPC, oldTarget, newTarget Entity, brain Brain) error {
// No special behavior for target changes in defensive AI
return nil
}
// CreateAggressiveBrain creates a brain with aggressive AI behavior
func CreateAggressiveBrain(npc NPC, logger Logger) Brain {
return NewCustomBrain(npc, logger, &AggressiveAI{})
}
// CreateDefensiveBrain creates a brain with defensive AI behavior
func CreateDefensiveBrain(npc NPC, logger Logger, fleeThreshold int32) Brain {
return NewCustomBrain(npc, logger, NewDefensiveAI(fleeThreshold))
}

View File

@ -1,169 +1,26 @@
package ai package ai
import ( import (
"encoding/json"
"fmt" "fmt"
"time"
) )
// Logger interface for AI logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// NPC interface defines the required NPC functionality for AI
type NPC interface {
// Basic NPC information
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
SetHP(int32)
IsAlive() bool
// Combat state
GetInCombat() bool
InCombat(bool)
GetTarget() Entity
SetTarget(Entity)
// Pet functionality
IsPet() bool
GetOwner() Entity
// Movement and positioning
GetX() float32
GetY() float32
GetZ() float32
GetDistance(Entity) float32
FaceTarget(Entity, bool)
IsFollowing() bool
SetFollowing(bool)
GetFollowTarget() Spawn
SetFollowTarget(Spawn, float32)
CalculateRunningLocation(bool)
ClearRunningLocations()
// Runback functionality
IsRunningBack() bool
GetRunbackLocation() *MovementLocation
GetRunbackDistance() float32
Runback(float32)
ShouldCallRunback() bool
SetCallRunback(bool)
// Status effects
IsMezzedOrStunned() bool
IsCasting() bool
IsDazed() bool
IsFeared() bool
IsStifled() bool
InWater() bool
IsWaterCreature() bool
IsFlyingCreature() bool
// Combat mechanics
AttackAllowed(Entity) bool
PrimaryWeaponReady() bool
SecondaryWeaponReady() bool
SetPrimaryLastAttackTime(int64)
SetSecondaryLastAttackTime(int64)
MeleeAttack(Entity, float32, bool)
// Spell casting
GetCastPercentage() int8
GetNextSpell(Entity, float32) Spell
GetNextBuffSpell(Spawn) Spell
SetCastOnAggroCompleted(bool)
CheckLoS(Entity) bool
// Movement pausing
IsPauseMovementTimerActive() bool
// Encounter state
SetEncounterState(int8)
// Scripts
GetSpawnScript() string
// Utility
KillSpawn(NPC)
}
// Entity interface for combat entities
type Entity interface {
Spawn
GetID() int32
GetName() string
GetHP() int32
GetTotalHP() int32
IsPlayer() bool
IsBot() bool
IsPet() bool
GetOwner() Entity
InWater() bool
}
// Spawn interface for basic spawn functionality
type Spawn interface {
GetID() int32
GetName() string
GetX() float32
GetY() float32
GetZ() float32
}
// Spell interface for spell data
type Spell interface {
GetSpellID() int32
GetName() string
IsFriendlySpell() bool
GetCastTime() int32
GetRecoveryTime() int32
GetRange() float32
GetMinRange() float32
}
// MovementLocation represents a location for movement/runback
type MovementLocation struct {
X float32
Y float32
Z float32
GridID int32
Stage int32
}
// LuaInterface defines Lua script integration
type LuaInterface interface {
RunSpawnScript(script, function string, npc NPC, target Entity) error
}
// Zone interface for zone-related AI operations
type Zone interface {
GetSpawnByID(int32) Spawn
ProcessSpell(spell Spell, caster NPC, target Spawn) error
CallSpawnScript(npc NPC, scriptType string, args ...any) error
}
// AIManager provides high-level management of the AI system // AIManager provides high-level management of the AI system
type AIManager struct { type AIManager struct {
brains map[int32]Brain // Map of NPC ID to brain brains map[int32]Brain // Map of NPC ID to brain
activeCount int64 // Number of active brains activeCount int64 // Number of active brains
totalThinks int64 // Total think cycles processed totalThinks int64 // Total think cycles processed
logger Logger // Logger for AI events logger Logger // Logger for AI events
luaInterface LuaInterface // Lua script interface // No external dependencies needed
} }
// NewAIManager creates a new AI manager // NewAIManager creates a new AI manager
func NewAIManager(logger Logger, luaInterface LuaInterface) *AIManager { func NewAIManager(logger Logger) *AIManager {
return &AIManager{ return &AIManager{
brains: make(map[int32]Brain), brains: make(map[int32]Brain),
activeCount: 0, activeCount: 0,
totalThinks: 0, totalThinks: 0,
logger: logger, logger: logger,
luaInterface: luaInterface,
} }
} }
@ -228,8 +85,10 @@ func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...any)
case BrainTypeBlank: case BrainTypeBlank:
brain = NewBlankBrain(npc, am.logger) brain = NewBlankBrain(npc, am.logger)
case BrainTypeLua: case BrainTypeCustom:
brain = NewLuaBrain(npc, am.logger, am.luaInterface) // Custom brains need to be created with specific CustomAI implementations
// This is a fallback - normally created via NewCustomBrain directly
brain = NewBaseBrain(npc, am.logger)
case BrainTypeDumbFire: case BrainTypeDumbFire:
if len(options) >= 2 { if len(options) >= 2 {
@ -347,14 +206,137 @@ func (am *AIManager) getBrainCountsByType() map[string]int {
return counts return counts
} }
// AIStatistics contains AI system statistics // AI Communication and Packets
type AIStatistics struct {
TotalBrains int `json:"total_brains"` // SendAIUpdate sends an AI update packet for an NPC
ActiveBrains int `json:"active_brains"` func (am *AIManager) SendAIUpdate(client AIClient, npcID int32) error {
TotalThinks int64 `json:"total_thinks"` brain := am.GetBrain(npcID)
BrainsByType map[string]int `json:"brains_by_type"` if brain == nil {
return fmt.Errorf("no brain found for NPC %d", npcID)
}
data := map[string]interface{}{
"npc_id": npcID,
"brain_type": brain.GetBrainType(),
"state": brain.GetState(),
"active": brain.IsActive(),
"think_tick": brain.GetThinkTick(),
"most_hated": brain.GetMostHated(),
"encounter_size": brain.GetEncounterSize(),
}
packet := CreateAIPacket(AIPacketTypeUpdate, npcID, data)
return client.SendPacket(packet)
} }
// SendHateUpdate sends a hate update packet
func (am *AIManager) SendHateUpdate(client AIClient, npcID int32, targetID int32, hateValue int32) error {
data := map[string]interface{}{
"target_id": targetID,
"hate_value": hateValue,
}
packet := CreateAIPacket(AIPacketTypeHateUpdate, npcID, data)
return client.SendPacket(packet)
}
// SendEncounterUpdate sends an encounter update packet
func (am *AIManager) SendEncounterUpdate(client AIClient, npcID int32, encounterSize int) error {
data := map[string]interface{}{
"encounter_size": encounterSize,
}
packet := CreateAIPacket(AIPacketTypeEncounter, npcID, data)
return client.SendPacket(packet)
}
// SendAIStatistics sends AI system statistics
func (am *AIManager) SendAIStatistics(client AIClient) error {
stats := am.GetStatistics()
data := map[string]interface{}{
"total_brains": stats.TotalBrains,
"active_brains": stats.ActiveBrains,
"total_thinks": stats.TotalThinks,
"brains_by_type": stats.BrainsByType,
}
packet := CreateAIPacket(AIPacketTypeStatistics, 0, data)
return client.SendPacket(packet)
}
// BroadcastAIUpdate broadcasts an AI update to all clients
func (am *AIManager) BroadcastAIUpdate(client AIClient, npcID int32) error {
brain := am.GetBrain(npcID)
if brain == nil {
return fmt.Errorf("no brain found for NPC %d", npcID)
}
data := map[string]interface{}{
"npc_id": npcID,
"brain_type": brain.GetBrainType(),
"state": brain.GetState(),
"active": brain.IsActive(),
}
packet := CreateAIPacket(AIPacketTypeUpdate, npcID, data)
return client.BroadcastPacket(packet)
}
// ProcessAICommand processes an AI command packet
func (am *AIManager) ProcessAICommand(packet *AIPacket) error {
if packet.Type != AIPacketTypeCommand {
return fmt.Errorf("invalid packet type for command processing")
}
brain := am.GetBrain(packet.EntityID)
if brain == nil {
return fmt.Errorf("no brain found for NPC %d", packet.EntityID)
}
command, ok := packet.Data["command"].(string)
if !ok {
return fmt.Errorf("missing command in packet data")
}
switch command {
case "set_active":
if active, ok := packet.Data["active"].(bool); ok {
brain.SetActive(active)
}
case "add_hate":
if targetID, ok := packet.Data["target_id"].(float64); ok {
if hateValue, ok := packet.Data["hate_value"].(float64); ok {
brain.AddHate(int32(targetID), int32(hateValue))
}
}
case "clear_hate":
if targetID, ok := packet.Data["target_id"].(float64); ok {
brain.ClearHateForEntity(int32(targetID))
} else {
brain.ClearHate()
}
case "set_debug_level":
if level, ok := packet.Data["debug_level"].(float64); ok {
brain.SetDebugLevel(int8(level))
}
case "reset_stats":
// Reset brain statistics
stats := brain.GetStatistics()
*stats = *NewBrainStatistics()
default:
return fmt.Errorf("unknown command: %s", command)
}
return nil
}
// Debugging and Utilities
// AIBrainAdapter provides NPC functionality for brains // AIBrainAdapter provides NPC functionality for brains
type AIBrainAdapter struct { type AIBrainAdapter struct {
npc NPC npc NPC
@ -400,33 +382,6 @@ func (aba *AIBrainAdapter) SetupPetBrain(combatPet bool) Brain {
return NewNonCombatPetBrain(aba.npc, aba.logger) return NewNonCombatPetBrain(aba.npc, aba.logger)
} }
// Utility functions
// getBrainTypeName returns the string name for a brain type
func getBrainTypeName(brainType int8) string {
switch brainType {
case BrainTypeDefault:
return "default"
case BrainTypeCombatPet:
return "combat_pet"
case BrainTypeNonCombatPet:
return "non_combat_pet"
case BrainTypeBlank:
return "blank"
case BrainTypeLua:
return "lua"
case BrainTypeDumbFire:
return "dumbfire"
default:
return "unknown"
}
}
// currentTimeMillis returns current time in milliseconds
func currentTimeMillis() int64 {
return time.Now().UnixMilli()
}
// HateListDebugger provides debugging functionality for hate lists // HateListDebugger provides debugging functionality for hate lists
type HateListDebugger struct { type HateListDebugger struct {
logger Logger logger Logger
@ -484,3 +439,13 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
hld.logger.LogInfo("-------------------") hld.logger.LogInfo("-------------------")
} }
// ToJSON converts AI statistics to JSON
func (stats *AIStatistics) ToJSON() ([]byte, error) {
return json.Marshal(stats)
}
// ToJSON converts brain statistics to JSON
func (stats *BrainStatistics) ToJSON() ([]byte, error) {
return json.Marshal(stats)
}

View File

@ -475,30 +475,4 @@ func (bs *BrainState) SetDebugLevel(level int8) {
bs.mutex.Lock() bs.mutex.Lock()
defer bs.mutex.Unlock() defer bs.mutex.Unlock()
bs.DebugLevel = level bs.DebugLevel = level
} }
// BrainStatistics contains brain performance statistics
type BrainStatistics struct {
ThinkCycles int64 `json:"think_cycles"`
SpellsCast int64 `json:"spells_cast"`
MeleeAttacks int64 `json:"melee_attacks"`
HateEvents int64 `json:"hate_events"`
EncounterEvents int64 `json:"encounter_events"`
AverageThinkTime float64 `json:"average_think_time_ms"`
LastThinkTime int64 `json:"last_think_time"`
TotalActiveTime int64 `json:"total_active_time_ms"`
}
// NewBrainStatistics creates new brain statistics
func NewBrainStatistics() *BrainStatistics {
return &BrainStatistics{
ThinkCycles: 0,
SpellsCast: 0,
MeleeAttacks: 0,
HateEvents: 0,
EncounterEvents: 0,
AverageThinkTime: 0.0,
LastThinkTime: time.Now().UnixMilli(),
TotalActiveTime: 0,
}
}

View File

@ -1,324 +0,0 @@
package ai
import (
"fmt"
"time"
)
// CombatPetBrain extends the base brain for combat pets
type CombatPetBrain struct {
*BaseBrain
}
// NewCombatPetBrain creates a new combat pet brain
func NewCombatPetBrain(npc NPC, logger Logger) *CombatPetBrain {
brain := &CombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeCombatPet
return brain
}
// Think implements pet-specific AI logic
func (cpb *CombatPetBrain) Think() error {
// Call parent Think() for default combat behavior
if err := cpb.BaseBrain.Think(); err != nil {
return err
}
// Additional pet-specific logic
if cpb.npc.GetInCombat() || !cpb.npc.IsPet() || cpb.npc.IsMezzedOrStunned() {
return nil
}
if cpb.logger != nil && cpb.state.GetDebugLevel() >= DebugLevelDetailed {
cpb.logger.LogDebug("Combat pet AI for %s", cpb.npc.GetName())
}
// Check if owner has stay command set
owner := cpb.npc.GetOwner()
if owner != nil && owner.IsPlayer() {
// TODO: Check player's pet movement setting
// if player.GetInfoStruct().GetPetMovement() == PetMovementStay {
// return nil
// }
}
// Follow owner
if owner != nil {
cpb.npc.SetTarget(owner)
distance := cpb.npc.GetDistance(owner)
maxRange := cpb.getMaxCombatRange()
if distance > maxRange {
cpb.MoveCloser(owner)
}
}
return nil
}
// NonCombatPetBrain handles non-combat pets (cosmetic pets)
type NonCombatPetBrain struct {
*BaseBrain
}
// NewNonCombatPetBrain creates a new non-combat pet brain
func NewNonCombatPetBrain(npc NPC, logger Logger) *NonCombatPetBrain {
brain := &NonCombatPetBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeNonCombatPet
return brain
}
// Think implements non-combat pet AI (just following)
func (ncpb *NonCombatPetBrain) Think() error {
// Non-combat pets don't do combat AI
if !ncpb.npc.IsPet() || ncpb.npc.IsMezzedOrStunned() {
return nil
}
if ncpb.logger != nil && ncpb.state.GetDebugLevel() >= DebugLevelDetailed {
ncpb.logger.LogDebug("Non-combat pet AI for %s", ncpb.npc.GetName())
}
// Just follow owner
owner := ncpb.npc.GetOwner()
if owner != nil {
ncpb.npc.SetTarget(owner)
distance := ncpb.npc.GetDistance(owner)
maxRange := ncpb.getMaxCombatRange()
if distance > maxRange {
ncpb.MoveCloser(owner)
}
}
return nil
}
// BlankBrain provides a minimal AI that does nothing
type BlankBrain struct {
*BaseBrain
}
// NewBlankBrain creates a new blank brain
func NewBlankBrain(npc NPC, logger Logger) *BlankBrain {
brain := &BlankBrain{
BaseBrain: NewBaseBrain(npc, logger),
}
brain.brainType = BrainTypeBlank
brain.SetThinkTick(BlankBrainTick) // Very slow tick
return brain
}
// Think does nothing for blank brains
func (bb *BlankBrain) Think() error {
// Blank brain does nothing
return nil
}
// LuaBrain allows AI to be controlled by Lua scripts
type LuaBrain struct {
*BaseBrain
scriptInterface LuaInterface
}
// NewLuaBrain creates a new Lua-controlled brain
func NewLuaBrain(npc NPC, logger Logger, luaInterface LuaInterface) *LuaBrain {
brain := &LuaBrain{
BaseBrain: NewBaseBrain(npc, logger),
scriptInterface: luaInterface,
}
brain.brainType = BrainTypeLua
return brain
}
// Think calls the Lua script's Think function
func (lb *LuaBrain) Think() error {
if lb.scriptInterface == nil {
return fmt.Errorf("no Lua interface available")
}
if lb.npc == nil {
return fmt.Errorf("brain has no body")
}
script := lb.npc.GetSpawnScript()
if script == "" {
if lb.logger != nil {
lb.logger.LogError("Lua brain set on spawn without script")
}
return fmt.Errorf("no spawn script available")
}
// Call the Lua Think function
target := lb.npc.GetTarget()
err := lb.scriptInterface.RunSpawnScript(script, "Think", lb.npc, target)
if err != nil {
if lb.logger != nil {
lb.logger.LogError("Lua script Think function failed: %v", err)
}
return fmt.Errorf("Lua Think function failed: %w", err)
}
return nil
}
// DumbFirePetBrain handles dumbfire pets (temporary combat pets)
type DumbFirePetBrain struct {
*BaseBrain
expireTime int64
}
// NewDumbFirePetBrain creates a new dumbfire pet brain
func NewDumbFirePetBrain(npc NPC, target Entity, expireTimeMS int32, logger Logger) *DumbFirePetBrain {
brain := &DumbFirePetBrain{
BaseBrain: NewBaseBrain(npc, logger),
expireTime: time.Now().UnixMilli() + int64(expireTimeMS),
}
brain.brainType = BrainTypeDumbFire
// Add maximum hate for the target
if target != nil {
brain.AddHate(target.GetID(), MaxHateValue)
}
return brain
}
// AddHate only allows hate for the initial target
func (dfpb *DumbFirePetBrain) AddHate(entityID int32, hate int32) {
// Only add hate if we don't already have a target
if dfpb.GetMostHated() == 0 {
dfpb.BaseBrain.AddHate(entityID, hate)
}
}
// Think implements dumbfire pet AI
func (dfpb *DumbFirePetBrain) Think() error {
// Check if expired
if time.Now().UnixMilli() > dfpb.expireTime {
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s expired", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Get target
targetID := dfpb.GetMostHated()
if targetID == 0 {
// No target, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
if dfpb.logger != nil {
dfpb.logger.LogDebug("Dumbfire pet %s has no target", dfpb.npc.GetName())
}
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
target := dfpb.getEntityByID(targetID)
if target == nil {
// Target no longer exists, kill self
if dfpb.npc != nil && dfpb.npc.GetHP() > 0 {
dfpb.npc.KillSpawn(dfpb.npc)
}
return nil
}
// Skip if mezzed or stunned
if dfpb.npc.IsMezzedOrStunned() {
return nil
}
// Set target if not already set
if dfpb.npc.GetTarget() != target {
dfpb.npc.SetTarget(target)
dfpb.npc.FaceTarget(target, false)
}
// Enter combat if not already
if !dfpb.npc.GetInCombat() {
dfpb.npc.CalculateRunningLocation(true)
dfpb.npc.InCombat(true)
}
distance := dfpb.npc.GetDistance(target)
// Try to cast spells if we have line of sight
if dfpb.npc.CheckLoS(target) && !dfpb.npc.IsCasting() &&
(!dfpb.HasRecovered() || !dfpb.ProcessSpell(target, distance)) {
if dfpb.logger != nil && dfpb.state.GetDebugLevel() >= DebugLevelDetailed {
dfpb.logger.LogDebug("Dumbfire pet %s attempting melee on %s",
dfpb.npc.GetName(), target.GetName())
}
dfpb.npc.FaceTarget(target, false)
dfpb.ProcessMelee(target, distance)
}
return nil
}
// GetExpireTime returns when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) GetExpireTime() int64 {
return dfpb.expireTime
}
// SetExpireTime sets when this dumbfire pet will expire
func (dfpb *DumbFirePetBrain) SetExpireTime(expireTime int64) {
dfpb.expireTime = expireTime
}
// IsExpired checks if the dumbfire pet has expired
func (dfpb *DumbFirePetBrain) IsExpired() bool {
return time.Now().UnixMilli() > dfpb.expireTime
}
// ExtendExpireTime extends the expire time by the given duration
func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) {
dfpb.expireTime += int64(durationMS)
}
// Brain factory functions
// CreateBrain creates the appropriate brain type for an NPC
func CreateBrain(npc NPC, brainType int8, logger Logger, options ...any) Brain {
switch brainType {
case BrainTypeCombatPet:
return NewCombatPetBrain(npc, logger)
case BrainTypeNonCombatPet:
return NewNonCombatPetBrain(npc, logger)
case BrainTypeBlank:
return NewBlankBrain(npc, logger)
case BrainTypeLua:
if len(options) > 0 {
if luaInterface, ok := options[0].(LuaInterface); ok {
return NewLuaBrain(npc, logger, luaInterface)
}
}
return NewBaseBrain(npc, logger) // Fallback to default
case BrainTypeDumbFire:
if len(options) >= 2 {
if target, ok := options[0].(Entity); ok {
if expireTime, ok := options[1].(int32); ok {
return NewDumbFirePetBrain(npc, target, expireTime, logger)
}
}
}
return NewBaseBrain(npc, logger) // Fallback to default
default:
return NewBaseBrain(npc, logger)
}
}

View File

@ -1,93 +0,0 @@
package npc
// 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
)

View File

@ -1,572 +0,0 @@
package npc
import "fmt"
// Database interface for NPC persistence
type Database interface {
LoadAllNPCs() ([]*NPC, error)
SaveNPC(npc *NPC) error
DeleteNPC(npcID int32) error
LoadNPCSpells(npcID int32) ([]*NPCSpell, error)
SaveNPCSpells(npcID int32, spells []*NPCSpell) error
LoadNPCSkills(npcID int32) (map[string]*Skill, error)
SaveNPCSkills(npcID int32, skills map[string]*Skill) error
}
// Logger interface for NPC logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// Client interface for NPC-related client operations
type Client interface {
GetPlayer() Player
GetVersion() int16
SendNPCUpdate(npcData []byte) error
SendCombatUpdate(combatData []byte) error
SendSpellCast(spellData []byte) error
}
// Player interface for NPC-related player operations
type Player interface {
GetCharacterID() int32
GetName() string
GetLevel() int8
GetZoneID() int32
GetX() float32
GetY() float32
GetZ() float32
IsInCombat() bool
GetTarget() *NPC
SendMessage(message string)
}
// Zone interface for NPC zone operations
type Zone interface {
GetZoneID() int32
GetNPCs() []*NPC
AddNPC(npc *NPC) error
RemoveNPC(npcID int32) error
GetPlayersInRange(x, y, z, radius float32) []Player
ProcessEntityCommand(command string, client Client, target *NPC) error
CallSpawnScript(npc *NPC, scriptType string, args ...any) error
}
// SpellManager interface for spell system integration
type SpellManager interface {
GetSpell(spellID int32, tier int8) Spell
CastSpell(caster *NPC, target any, spell Spell) error
GetSpellEffect(entity any, spellID int32) SpellEffect
ProcessSpell(spell Spell, caster *NPC, target any) error
}
// Spell interface for spell data
type Spell interface {
GetSpellID() int32
GetName() string
GetTier() int8
GetRange() float32
GetMinRange() float32
GetPowerRequired() int32
IsFriendlySpell() bool
IsToggleSpell() bool
GetCastTime() int32
GetRecastTime() int32
}
// SpellEffect interface for active spell effects
type SpellEffect interface {
GetSpellID() int32
GetTier() int8
GetDuration() int32
GetRemainingTime() int32
IsExpired() bool
}
// SkillManager interface for skill system integration
type SkillManager interface {
GetSkill(skillID int32) MasterSkill
GetSkillByName(name string) MasterSkill
ApplySkillBonus(entity any, skillID int32, bonus float32) error
RemoveSkillBonus(entity any, skillID int32, bonus float32) error
}
// MasterSkill interface for skill definitions
type MasterSkill interface {
GetSkillID() int32
GetName() string
GetDescription() string
GetMaxValue() int16
}
// AppearanceManager interface for appearance system integration
type AppearanceManager interface {
GetAppearance(appearanceID int32) Appearance
GetAppearancesByName(name string) []Appearance
RandomizeAppearance(npc *NPC, flags int32) error
}
// Appearance interface for appearance data
type Appearance interface {
GetAppearanceID() int32
GetName() string
GetModelType() int16
GetRace() int16
GetGender() int8
}
// MovementManager interface for movement system integration
type MovementManager interface {
StartMovement(npc *NPC, x, y, z float32) error
StopMovement(npc *NPC) error
SetSpeed(npc *NPC, speed float32) error
NavigateToLocation(npc *NPC, x, y, z float32) error
IsMoving(npc *NPC) bool
}
// CombatManager interface for combat system integration
type CombatManager interface {
StartCombat(npc *NPC, target any) error
EndCombat(npc *NPC) error
ProcessCombatRound(npc *NPC) error
CalculateDamage(attacker *NPC, target any) int32
ApplyDamage(target any, damage int32) error
}
// NPCAware interface for entities that can interact with NPCs
type NPCAware interface {
GetNPC() *NPC
IsNPC() bool
HandleNPCInteraction(npc *NPC, interactionType string) error
ReceiveNPCCommand(npc *NPC, command string) error
}
// EntityAdapter provides NPC functionality for entities
type EntityAdapter struct {
npc *NPC
logger Logger
}
// NewEntityAdapter creates a new entity adapter
func NewEntityAdapter(npc *NPC, logger Logger) *EntityAdapter {
return &EntityAdapter{
npc: npc,
logger: logger,
}
}
// GetNPC returns the associated NPC
func (ea *EntityAdapter) GetNPC() *NPC {
return ea.npc
}
// IsNPC always returns true for entity adapters
func (ea *EntityAdapter) IsNPC() bool {
return true
}
// HandleNPCInteraction processes interactions with other NPCs
func (ea *EntityAdapter) HandleNPCInteraction(otherNPC *NPC, interactionType string) error {
if ea.npc == nil || otherNPC == nil {
return fmt.Errorf("invalid NPC for interaction")
}
// Handle different interaction types
switch interactionType {
case "aggro":
return ea.handleAggroInteraction(otherNPC)
case "assist":
return ea.handleAssistInteraction(otherNPC)
case "trade":
return ea.handleTradeInteraction(otherNPC)
default:
if ea.logger != nil {
ea.logger.LogWarning("Unknown NPC interaction type: %s", interactionType)
}
return fmt.Errorf("unknown interaction type: %s", interactionType)
}
}
// ReceiveNPCCommand processes commands from other NPCs
func (ea *EntityAdapter) ReceiveNPCCommand(otherNPC *NPC, command string) error {
if ea.npc == nil || otherNPC == nil {
return fmt.Errorf("invalid NPC for command")
}
// Process the command
switch command {
case "follow":
return ea.handleFollowCommand(otherNPC)
case "attack":
return ea.handleAttackCommand(otherNPC)
case "retreat":
return ea.handleRetreatCommand(otherNPC)
default:
if ea.logger != nil {
ea.logger.LogWarning("Unknown NPC command: %s", command)
}
return fmt.Errorf("unknown command: %s", command)
}
}
// handleAggroInteraction processes aggro interactions
func (ea *EntityAdapter) handleAggroInteraction(otherNPC *NPC) error {
// TODO: Implement aggro logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received aggro from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleAssistInteraction processes assist interactions
func (ea *EntityAdapter) handleAssistInteraction(otherNPC *NPC) error {
// TODO: Implement assist logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received assist request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleTradeInteraction processes trade interactions
func (ea *EntityAdapter) handleTradeInteraction(otherNPC *NPC) error {
// TODO: Implement trade logic between NPCs
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received trade request from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleFollowCommand processes follow commands
func (ea *EntityAdapter) handleFollowCommand(otherNPC *NPC) error {
// TODO: Implement follow logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received follow command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleAttackCommand processes attack commands
func (ea *EntityAdapter) handleAttackCommand(otherNPC *NPC) error {
// TODO: Implement attack logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received attack command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// handleRetreatCommand processes retreat commands
func (ea *EntityAdapter) handleRetreatCommand(otherNPC *NPC) error {
// TODO: Implement retreat logic
if ea.logger != nil {
ea.logger.LogDebug("NPC %d received retreat command from NPC %d",
ea.npc.GetNPCID(), otherNPC.GetNPCID())
}
return nil
}
// SpellCasterAdapter provides spell casting functionality for NPCs
type SpellCasterAdapter struct {
npc *NPC
spellManager SpellManager
logger Logger
}
// NewSpellCasterAdapter creates a new spell caster adapter
func NewSpellCasterAdapter(npc *NPC, spellManager SpellManager, logger Logger) *SpellCasterAdapter {
return &SpellCasterAdapter{
npc: npc,
spellManager: spellManager,
logger: logger,
}
}
// GetNextSpell selects the next spell to cast based on AI strategy
func (sca *SpellCasterAdapter) GetNextSpell(target any, distance float32) Spell {
if sca.npc == nil || sca.spellManager == nil {
return nil
}
// Check cast-on-aggro spells first
if !sca.npc.castOnAggroCompleted {
spell := sca.getNextCastOnAggroSpell(target)
if spell != nil {
return spell
}
sca.npc.castOnAggroCompleted = true
}
// Get spells based on AI strategy
strategy := sca.npc.GetAIStrategy()
return sca.getNextSpellByStrategy(target, distance, strategy)
}
// GetNextBuffSpell selects the next buff spell to cast
func (sca *SpellCasterAdapter) GetNextBuffSpell(target any) Spell {
if sca.npc == nil || sca.spellManager == nil {
return nil
}
// Check cast-on-spawn spells first
castOnSpells := sca.npc.castOnSpells[CastOnSpawn]
for _, npcSpell := range castOnSpells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil {
// Check if target already has this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil {
if effect.GetTier() < spell.GetTier() {
return spell // Upgrade existing effect
}
} else {
return spell // New effect
}
}
}
// Check regular spells for buffs
for _, npcSpell := range sca.npc.spells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil && spell.IsFriendlySpell() && spell.IsToggleSpell() {
// Check if target already has this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect != nil {
if effect.GetTier() < spell.GetTier() {
return spell // Upgrade existing effect
}
} else {
return spell // New effect
}
}
}
return nil
}
// CastSpell attempts to cast a spell
func (sca *SpellCasterAdapter) CastSpell(target any, spell Spell) error {
if sca.npc == nil || sca.spellManager == nil || spell == nil {
return fmt.Errorf("invalid parameters for spell casting")
}
// Check casting conditions
if err := sca.checkCastingConditions(spell); err != nil {
return fmt.Errorf("casting conditions not met: %w", err)
}
// Cast the spell
if err := sca.spellManager.CastSpell(sca.npc, target, spell); err != nil {
return fmt.Errorf("failed to cast spell: %w", err)
}
if sca.logger != nil {
sca.logger.LogDebug("NPC %d cast spell %s (%d)",
sca.npc.GetNPCID(), spell.GetName(), spell.GetSpellID())
}
return nil
}
// getNextCastOnAggroSpell selects cast-on-aggro spells
func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target any) Spell {
castOnSpells := sca.npc.castOnSpells[CastOnAggro]
for _, npcSpell := range castOnSpells {
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell != nil {
// Check if target doesn't already have this effect
if effect := sca.spellManager.GetSpellEffect(target, spell.GetSpellID()); effect == nil {
return spell
}
}
}
return nil
}
// getNextSpellByStrategy selects spells based on AI strategy
func (sca *SpellCasterAdapter) getNextSpellByStrategy(target any, distance float32, strategy int8) Spell {
// TODO: Implement more sophisticated spell selection based on strategy
for _, npcSpell := range sca.npc.spells {
// Check HP ratio requirements
if npcSpell.GetRequiredHPRatio() != 0 {
// TODO: Implement HP ratio checking
}
spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier())
if spell == nil {
continue
}
// Check strategy compatibility
if strategy == AIStrategyOffensive && spell.IsFriendlySpell() {
continue
}
if strategy == AIStrategyDefensive && !spell.IsFriendlySpell() {
continue
}
// Check range and power requirements
if distance <= spell.GetRange() && distance >= spell.GetMinRange() {
// TODO: Check power requirements
return spell
}
}
return nil
}
// checkCastingConditions validates spell casting conditions
func (sca *SpellCasterAdapter) checkCastingConditions(spell Spell) error {
if sca.npc.Entity == nil {
return fmt.Errorf("NPC entity is nil")
}
// TODO: Implement power checking, cooldown checking, etc.
return nil
}
// CombatAdapter provides combat functionality for NPCs
type CombatAdapter struct {
npc *NPC
combatManager CombatManager
logger Logger
}
// NewCombatAdapter creates a new combat adapter
func NewCombatAdapter(npc *NPC, combatManager CombatManager, logger Logger) *CombatAdapter {
return &CombatAdapter{
npc: npc,
combatManager: combatManager,
logger: logger,
}
}
// EnterCombat handles entering combat state
func (ca *CombatAdapter) EnterCombat(target any) error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
// Start combat through combat manager
if ca.combatManager != nil {
if err := ca.combatManager.StartCombat(ca.npc, target); err != nil {
return fmt.Errorf("failed to start combat: %w", err)
}
}
// Update NPC state
ca.npc.InCombat(true)
if ca.logger != nil {
ca.logger.LogDebug("NPC %d entered combat", ca.npc.GetNPCID())
}
return nil
}
// ExitCombat handles exiting combat state
func (ca *CombatAdapter) ExitCombat() error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
// End combat through combat manager
if ca.combatManager != nil {
if err := ca.combatManager.EndCombat(ca.npc); err != nil {
return fmt.Errorf("failed to end combat: %w", err)
}
}
// Update NPC state
ca.npc.InCombat(false)
if ca.logger != nil {
ca.logger.LogDebug("NPC %d exited combat", ca.npc.GetNPCID())
}
return nil
}
// ProcessCombat handles combat processing
func (ca *CombatAdapter) ProcessCombat() error {
if ca.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ca.combatManager != nil {
return ca.combatManager.ProcessCombatRound(ca.npc)
}
return nil
}
// MovementAdapter provides movement functionality for NPCs
type MovementAdapter struct {
npc *NPC
movementManager MovementManager
logger Logger
}
// NewMovementAdapter creates a new movement adapter
func NewMovementAdapter(npc *NPC, movementManager MovementManager, logger Logger) *MovementAdapter {
return &MovementAdapter{
npc: npc,
movementManager: movementManager,
logger: logger,
}
}
// MoveToLocation moves the NPC to a specific location
func (ma *MovementAdapter) MoveToLocation(x, y, z float32) error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ma.movementManager != nil {
return ma.movementManager.NavigateToLocation(ma.npc, x, y, z)
}
return fmt.Errorf("movement manager not available")
}
// StopMovement stops the NPC's movement
func (ma *MovementAdapter) StopMovement() error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
if ma.movementManager != nil {
return ma.movementManager.StopMovement(ma.npc)
}
return fmt.Errorf("movement manager not available")
}
// IsMoving checks if the NPC is currently moving
func (ma *MovementAdapter) IsMoving() bool {
if ma.npc == nil || ma.movementManager == nil {
return false
}
return ma.movementManager.IsMoving(ma.npc)
}
// RunbackToSpawn moves the NPC back to its spawn location
func (ma *MovementAdapter) RunbackToSpawn() error {
if ma.npc == nil {
return fmt.Errorf("NPC is nil")
}
runbackLocation := ma.npc.GetRunbackLocation()
if runbackLocation == nil {
return fmt.Errorf("no runback location set")
}
return ma.MoveToLocation(runbackLocation.X, runbackLocation.Y, runbackLocation.Z)
}

View File

@ -1,765 +0,0 @@
package npc
import (
"fmt"
"math/rand"
"strings"
"sync"
)
// 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
database Database // Database interface
logger Logger // Logger interface
spellManager SpellManager // Spell system interface
skillManager SkillManager // Skill system interface
appearanceManager AppearanceManager // Appearance system interface
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(database Database, logger Logger) *Manager {
return &Manager{
npcs: make(map[int32]*NPC),
npcsByZone: make(map[int32][]*NPC),
npcsByAppearance: make(map[int32][]*NPC),
database: database,
logger: logger,
aiStrategyCounts: make(map[int8]int64),
maxNPCs: 10000, // Default limit
defaultAggroRadius: DefaultAggroRadius,
enableAI: true,
}
}
// Initialize loads NPCs from database and sets up the system
func (m *Manager) Initialize() error {
if m.logger != nil {
m.logger.LogInfo("Initializing NPC manager...")
}
if m.database == nil {
if m.logger != nil {
m.logger.LogWarning("No database provided, starting with empty NPC list")
}
return nil
}
// Load NPCs from database
npcs, err := m.database.LoadAllNPCs()
if err != nil {
return fmt.Errorf("failed to load NPCs from database: %w", err)
}
for _, npc := range npcs {
if err := m.addNPCInternal(npc); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to add NPC %d: %v", npc.GetNPCID(), err)
}
}
}
if m.logger != nil {
m.logger.LogInfo("Loaded %d NPCs from database", len(npcs))
}
return nil
}
// 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 zone index
// TODO: Add zone support when Entity.GetZoneID() is available
// if npc.Entity != nil {
// zoneID := npc.Entity.GetZoneID()
// m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], 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]++
// Save to database if available
if m.database != nil {
if err := m.database.SaveNPC(npc); err != nil {
// Remove from indexes if database save failed
delete(m.npcs, npcID)
m.removeFromZoneIndex(npc)
m.removeFromAppearanceIndex(npc)
m.totalNPCs--
m.aiStrategyCounts[strategy]--
return fmt.Errorf("failed to save NPC to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Added NPC %d: %s", npcID, npc.String())
}
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 database first if available
if m.database != nil {
if err := m.database.DeleteNPC(id); err != nil {
return fmt.Errorf("failed to delete NPC from database: %w", err)
}
}
// Remove from all indexes
delete(m.npcs, id)
m.removeFromZoneIndex(npc)
m.removeFromAppearanceIndex(npc)
// Update statistics
m.totalNPCs--
strategy := npc.GetAIStrategy()
if count := m.aiStrategyCounts[strategy]; count > 0 {
m.aiStrategyCounts[strategy]--
}
if m.logger != nil {
m.logger.LogInfo("Removed NPC %d", id)
}
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)
}
// Update indexes if zone or appearance changed
// TODO: Add zone support when Entity.GetZoneID() is available
// if npc.Entity != nil && oldNPC.Entity != nil {
// if npc.Entity.GetZoneID() != oldNPC.Entity.GetZoneID() {
// m.removeFromZoneIndex(oldNPC)
// zoneID := npc.Entity.GetZoneID()
// m.npcsByZone[zoneID] = append(m.npcsByZone[zoneID], npc)
// }
// }
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
// Save to database if available
if m.database != nil {
if err := m.database.SaveNPC(npc); err != nil {
return fmt.Errorf("failed to save NPC to database: %w", err)
}
}
if m.logger != nil {
m.logger.LogInfo("Updated NPC %d: %s", npcID, npc.String())
}
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 {
// TODO: Add combat status check when GetInCombat() is available
// if npc.Entity != nil && npc.Entity.GetInCombat() {
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() {
if err := brain.Think(); err != nil && m.logger != nil {
m.logger.LogError("AI brain error for NPC %d: %v", npc.GetNPCID(), err)
}
}
}
}
// 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 { // Limit output
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 { // Limit output
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())
// TODO: Add zone and combat status when methods are available
// result += fmt.Sprintf("Zone: %d\n", npc.Entity.GetZoneID())
// result += fmt.Sprintf("In Combat: %v\n", npc.Entity.GetInCombat())
}
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 { // Limit output
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) removeFromZoneIndex(npc *NPC) {
if npc.Entity == nil {
return
}
// TODO: Implement zone index removal when Entity.GetZoneID() is available
// zoneID := npc.Entity.GetZoneID()
// npcs := m.npcsByZone[zoneID]
// for i, n := range npcs {
// if n == npc {
// // Remove from slice
// m.npcsByZone[zoneID] = append(npcs[:i], npcs[i+1:]...)
// break
// }
// }
// // Clean up empty slices
// if len(m.npcsByZone[zoneID]) == 0 {
// delete(m.npcsByZone, zoneID)
// }
}
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)
}
}
// SetManagers sets the external system managers
func (m *Manager) SetManagers(spellMgr SpellManager, skillMgr SkillManager, appearanceMgr AppearanceManager) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.spellManager = spellMgr
m.skillManager = skillMgr
m.appearanceManager = appearanceMgr
}
// 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() {
if m.logger != nil {
m.logger.LogInfo("Shutting down NPC manager...")
}
// 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()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,157 +0,0 @@
# Race Types System
The race types system manages creature and NPC classifications in EverQuest II, providing a hierarchical categorization of all non-player entities in the game world.
## Overview
Unlike the character races system (`internal/races`), which handles the 21 playable character races used by both players and NPCs, the race types system classifies all creatures and NPCs into categories and subcategories for game mechanics, AI behavior, combat calculations, and visual representation.
**Note**: This package is located under `internal/npc/race_types` because it specifically deals with NPC creature types. The `internal/races` package remains at the top level because it's used by the Entity system, which is inherited by both players and NPCs.
## Key Components
### Types and Constants (`types.go`, `constants.go`)
- **RaceType**: Core structure containing race type ID, category, subcategory, and model name
- **Constants**: Defines 300+ race type IDs organized into 10 major categories
- **Statistics**: Tracks usage and query patterns for performance monitoring
### Master Race Type List (`master_race_type_list.go`)
- Maps model IDs to race type information
- Thread-safe operations with read/write locking
- Provides lookups by model ID, category, and subcategory
- Maintains statistics on race type usage
### Database Operations (`database.go`)
- Loads race types from the `race_types` table
- Supports CRUD operations for race type management
- Creates necessary database schema and indexes
### Manager (`manager.go`)
- High-level interface for race type operations
- Creature type checking methods (IsUndead, IsDragonkind, etc.)
- Command processing for debugging and administration
- Integration point for other game systems
### Interfaces (`interfaces.go`)
- **RaceTypeProvider**: Core interface for accessing race type information
- **CreatureTypeChecker**: Interface for checking creature classifications
- **RaceTypeAware**: Interface for entities that have a model type
- **NPCRaceTypeAdapter**: Adapter for integrating race types with NPC system
- Additional interfaces for damage, loot, and AI behavior modifications
## Race Type Categories
The system organizes creatures into 10 major categories:
1. **DRAGONKIND** (101-110): Dragons, drakes, wyrms, wyverns
2. **FAY** (111-122): Fae creatures, sprites, wisps, treants
3. **MAGICAL** (123-154): Constructs, golems, elementals, magical beings
4. **MECHANIMAGICAL** (155-157): Clockwork creatures, iron guardians
5. **NATURAL** (158-239): Animals, insects, reptiles, natural creatures
6. **PLANAR** (240-267): Demons, elementals, planar beings
7. **PLANT** (268-274): Carnivorous plants, animated vegetation
8. **SENTIENT** (275-332): Intelligent humanoids, NPC versions of player races
9. **UNDEAD** (333-343): Ghosts, skeletons, zombies, vampires
10. **WERE** (344-347): Werewolves and other lycanthropes
## Usage Examples
### Basic Race Type Lookup
```go
manager := race_types.NewManager(db)
manager.Initialize()
// Get race type for a model
modelID := int16(1234)
raceType := manager.GetRaceType(modelID)
category := manager.GetRaceTypeCategory(modelID)
// Check creature type
if manager.IsUndead(modelID) {
// Apply undead-specific mechanics
}
```
### NPC Integration
```go
// Assuming npc implements RaceTypeAware interface
adapter := race_types.NewNPCRaceTypeAdapter(npc, manager)
if adapter.IsDragonkind() {
// Apply dragon-specific abilities
}
raceInfo := adapter.GetRaceTypeInfo()
fmt.Printf("NPC is a %s (%s)\n", raceInfo.Category, raceInfo.Subcategory)
```
### Category Queries
```go
// Get all creatures in a category
dragons := manager.GetRaceTypesByCategory("DRAGONKIND")
for modelID, raceType := range dragons {
fmt.Printf("Model %d: %s\n", modelID, raceType.ModelName)
}
// Get statistics
stats := manager.GetStatistics()
fmt.Printf("Total race types: %d\n", stats.TotalRaceTypes)
```
## Database Schema
```sql
CREATE TABLE race_types (
model_type INTEGER PRIMARY KEY,
race_id INTEGER NOT NULL CHECK (race_id > 0),
category TEXT,
subcategory TEXT,
model_name TEXT
);
```
## Integration with Other Systems
### NPC System
The race types system integrates with the NPC system to provide:
- Creature classification for AI behavior
- Visual model selection
- Combat mechanics (damage modifiers, resistances)
- Loot table selection
### Combat System
Race types can influence:
- Damage calculations (e.g., bonus damage vs undead)
- Resistance calculations (e.g., dragons resistant to fire)
- Special ability availability
### Spawn System
The spawn system uses race types to:
- Determine which creatures spawn in specific zones
- Apply race-specific spawn behaviors
- Control population distribution
## Performance Considerations
- The master list uses a hash map for O(1) lookups by model ID
- Category and subcategory queries iterate through the list
- Statistics tracking has minimal overhead
- Thread-safe operations ensure data consistency
## Future Enhancements
Potential areas for expansion include:
- Race-specific spell resistances
- Faction relationships based on race types
- Advanced AI behaviors per race category
- Race-specific loot table modifiers
- Integration with scripting system for race-based events
## Differences from C++ Implementation
The Go implementation maintains compatibility with the C++ version while adding:
- Thread-safe operations with proper mutex usage
- Comprehensive statistics tracking
- More extensive validation and error handling
- Additional helper methods for common operations
- Clean interface definitions for system integration

View File

@ -26,7 +26,7 @@ const (
Siren = 118 Siren = 118
Spirit = 119 Spirit = 119
Sprite = 120 Sprite = 120
Treant = 121 // L&L 8 Treant = 121
Wisp = 122 Wisp = 122
// MAGICAL category (123-154) // MAGICAL category (123-154)
@ -68,276 +68,201 @@ const (
Clockwork = 156 Clockwork = 156
IronGuardian = 157 IronGuardian = 157
// NATURAL category (158-239) // NATURAL category (158-239) - Essential ones
Natural = 158 Natural = 158
Animal = 159 Animal = 159
Aquatic = 160 Aquatic = 160
Avian = 161 Avian = 161
Canine = 162 Canine = 162
Equine = 163 Equine = 163
Feline = 164 Feline = 164
Insect = 165 Insect = 165
Primate = 166 Primate = 166
Reptile = 167 Reptile = 167
Anemone = 168 Rodent = 168
Apopheli = 169 Bear = 169
Armadillo = 170 Boar = 170
Badger = 171 Bull = 171
Barracuda = 172 Cat = 172
Basilisk = 173 Chimera = 173
Bat = 174 Cockatrice = 174
Bear = 175 Crab = 175
Beaver = 176 Crocodile = 176
Beetle = 177 Dog = 177
Bovine = 178 Elephant = 178
Brontotherium = 179 Fish = 179
Brute = 180 Frog = 180
Camel = 181 Gobbler = 181
Cat = 182 Griffin = 182
Centipede = 183 Hippogriff = 183
Cerberus = 184 Horse = 184
Chimera = 185 Jellyfish = 185
Chokidai = 186 Kraken = 186
Cobra = 187 Lion = 187
Cockatrice = 188 Lizard = 188
Crab = 189 Mammoth = 189
Crocodile = 190 Mantis = 190
Deer = 191 Monkey = 191
Dragonfly = 192 Octopus = 192
Duck = 193 Owl = 193
Eel = 194 Panther = 194
Elephant = 195 Pegasus = 195
FlyingSnake = 196 Phoenix = 196
Frog = 197 Pig = 197
Goat = 198 Rabbit = 198
Gorilla = 199 Raptor = 199
Griffin = 200 Rat = 200
Hawk = 201 Rhinoceros = 201
HiveQueen = 202 Scorpion = 202
Horse = 203 Shark = 203
Hyena = 204 Snake = 204
KhoalRat = 205 Spider = 205
Kybur = 206 Tiger = 206
Leech = 207 Turtle = 207
Leopard = 208 Whale = 208
Lion = 209 Wolf = 209
Lizard = 210 Worg = 210
Mammoth = 211
MantaRay = 212
MoleRat = 213
Monkey = 214
Mythical = 215
Octopus = 216
OwlBear = 217
Pig = 218
Piranha = 219
Raptor = 220
Rat = 221
Rhinoceros = 222
RockCrawler = 223
SaberTooth = 224
Scorpion = 225
SeaTurtle = 226
Shark = 227
Sheep = 228
Slug = 229
Snake = 230
Spider = 231
Stirge = 232
Swordfish = 233
Tiger = 234
Turtle = 235
Vermin = 236
Vulrich = 237
Wolf = 238
Yeti = 239
// PLANAR category (240-267) // PLANAR category (240-267)
Planar = 240 Planar = 240
Abomination = 241 Demon = 241
AirElemental = 242 Devil = 242
Amygdalan = 243 Djinni = 243
Avatar = 244 Efreet = 244
Cyclops = 245 Elemental = 245
Demon = 246 Genie = 246
Djinn = 247 Modoc = 247
EarthElemental = 248 Nightmare = 248
Efreeti = 249 Shadowman = 249
Elemental = 250
Ethereal = 251
Etherpine = 252
EvilEye = 253
FireElemental = 254
Gazer = 255
Gehein = 256
Geonid = 257
Giant = 258 // L&L 5
Salamander = 259
ShadowedMan = 260
Sphinx = 261
Spore = 262
Succubus = 263
Valkyrie = 264
VoidBeast = 265
WaterElemental = 266
Wraith = 267
// PLANT category (268-274) // PLANT category (268-274)
Plant = 268 Plant = 268
CarnivorousPlant = 269 Carniplant = 269
Catoplebas = 270 Ent = 270
Mantrap = 271 FlowerPot = 271
RootAbomination = 272 Maneater = 272
RootHorror = 273 Shambler = 273
Succulent = 274 VineCreeper = 274
// SENTIENT category (275-332)
Sentient = 275
Ashlok = 276
Aviak = 277
BarbarianNPC = 278
BirdMan = 279
BoarFiend = 280
Bugbear = 281
Burynai = 282
Centaur = 283 // L&L 4
Coldain = 284
Dal = 285
DarkElfNPC = 286
Dizok = 287
Drachnid = 288
Drafling = 289
Drolvarg = 290
DwarfNPC = 291
EruditeNPC = 292
Ettin = 293
FreebloodNPC = 294
FroglokNPC = 295
FrostfellElf = 296
FungusMan = 297
Gnoll = 298 // L&L 1
GnomeNPC = 299
Goblin = 300 // L&L 3
Gruengach = 301
HalfElfNPC = 302
HalflingNPC = 303
HighElfNPC = 304
Holgresh = 305
Hooluk = 306
Huamein = 307
HumanNPC = 308
Humanoid = 309
IksarNPC = 310
Kerigdal = 311
KerranNPC = 312
Kobold = 313
LizardMan = 314
Minotaur = 315
OgreNPC = 316
Orc = 317 // L&L 2
Othmir = 318
RatongaNPC = 319
Ravasect = 320
Rendadal = 321
Roekillik = 322
SarnakNPC = 323
Skorpikis = 324
Spiroc = 325
Troglodyte = 326
TrollNPC = 327
Ulthork = 328
Vultak = 329
WoodElfNPC = 330
WraithGuard = 331
Yhalei = 332
// SENTIENT category (275-332) - Essential player race NPCs
Sentient = 275
Barbarian = 276
DarkElf = 277
Dwarf = 278
Erudite = 279
Gnome = 280
HalfElf = 281
Halfling = 282
HighElf = 283
Human = 284
Iksar = 285
Ogre = 286
Ratonga = 287
Troll = 288
WoodElf = 289
Froglok = 290
Fairy2 = 291
Arasai = 292
Sarnak = 293
Vampire = 294
Aerakyn = 295
// UNDEAD category (333-343) // UNDEAD category (333-343)
Undead = 333 Undead = 333
Ghost = 334 Apparition = 334
Ghoul = 335 Banshee = 335
Gunthak = 336 Ghost = 336
Horror = 337 Ghoul = 337
Mummy = 338 Lich = 338
ShinreeOrcs = 339 Mummy = 339
Skeleton = 340 // L&L 6 Revenant = 340
Spectre = 341 Skeleton = 341
VampireNPC = 342 Spectre = 342
Zombie = 343 // L&L 7 Zombie = 343
// WERE category (344-347) // WERE category (344-347)
Were = 344 Were = 344
AhrounWerewolves = 345 Werewolf = 345
LykulakWerewolves = 346 Werebear = 346
Werewolf = 347 Werebat = 347
) )
// Category name constants // Category base ranges for type checking
const ( const (
CategoryDragonkind = "DRAGONKIND" DragonkindBase = 101
CategoryFay = "FAY" DragonkindMax = 110
CategoryMagical = "MAGICAL" FayBase = 111
CategoryMechanimagical = "MECHANIMAGICAL" FayMax = 122
CategoryNatural = "NATURAL" MagicalBase = 123
CategoryPlanar = "PLANAR" MagicalMax = 154
CategoryPlant = "PLANT" MechanimagicalBase = 155
CategorySentient = "SENTIENT" MechanimagicalMax = 157
CategoryUndead = "UNDEAD" NaturalBase = 158
CategoryWere = "WERE" NaturalMax = 239
PlanarBase = 240
PlanarMax = 267
PlantBase = 268
PlantMax = 274
SentientBase = 275
SentientMax = 332
UndeadBase = 333
UndeadMax = 343
WereBase = 344
WereMax = 347
) )
// GetRaceTypeCategory returns the base category ID for a given race type ID // GetCategoryName returns the category name for a race type ID
// Converted from C++ GetRaceTypeCategory function func GetCategoryName(raceTypeID int16) string {
func GetRaceTypeCategory(raceTypeID int16) int16 {
switch { switch {
case raceTypeID >= Dragonkind && raceTypeID <= Wyvern: case raceTypeID >= DragonkindBase && raceTypeID <= DragonkindMax:
return Dragonkind return "DRAGONKIND"
case raceTypeID >= Fay && raceTypeID <= Wisp: case raceTypeID >= FayBase && raceTypeID <= FayMax:
return Fay return "FAY"
case raceTypeID >= Magical && raceTypeID <= WoodElemental: case raceTypeID >= MagicalBase && raceTypeID <= MagicalMax:
return Magical return "MAGICAL"
case raceTypeID >= Mechanimagical && raceTypeID <= IronGuardian: case raceTypeID >= MechanimagicalBase && raceTypeID <= MechanimagicalMax:
return Mechanimagical return "MECHANIMAGICAL"
case raceTypeID >= Natural && raceTypeID <= Yeti: case raceTypeID >= NaturalBase && raceTypeID <= NaturalMax:
return Natural return "NATURAL"
case raceTypeID >= Planar && raceTypeID <= Wraith: case raceTypeID >= PlanarBase && raceTypeID <= PlanarMax:
return Planar return "PLANAR"
case raceTypeID >= Plant && raceTypeID <= Succulent: case raceTypeID >= PlantBase && raceTypeID <= PlantMax:
return Plant return "PLANT"
case raceTypeID >= Sentient && raceTypeID <= Yhalei: case raceTypeID >= SentientBase && raceTypeID <= SentientMax:
return Sentient return "SENTIENT"
case raceTypeID >= Undead && raceTypeID <= Zombie: case raceTypeID >= UndeadBase && raceTypeID <= UndeadMax:
return Undead return "UNDEAD"
case raceTypeID >= Were && raceTypeID <= Werewolf: case raceTypeID >= WereBase && raceTypeID <= WereMax:
return Were return "WERE"
default: default:
return 0 return "UNKNOWN"
} }
} }
// GetCategoryName returns the category name for a given category ID // GetCategoryBase returns the base ID for a category
func GetCategoryName(categoryID int16) string { func GetCategoryBase(categoryName string) int16 {
switch categoryID { switch categoryName {
case Dragonkind: case "DRAGONKIND":
return CategoryDragonkind return DragonkindBase
case Fay: case "FAY":
return CategoryFay return FayBase
case Magical: case "MAGICAL":
return CategoryMagical return MagicalBase
case Mechanimagical: case "MECHANIMAGICAL":
return CategoryMechanimagical return MechanimagicalBase
case Natural: case "NATURAL":
return CategoryNatural return NaturalBase
case Planar: case "PLANAR":
return CategoryPlanar return PlanarBase
case Plant: case "PLANT":
return CategoryPlant return PlantBase
case Sentient: case "SENTIENT":
return CategorySentient return SentientBase
case Undead: case "UNDEAD":
return CategoryUndead return UndeadBase
case Were: case "WERE":
return CategoryWere return WereBase
default: default:
return "" return 0
} }
} }

View File

@ -1,149 +1,145 @@
package race_types package race_types
import ( import (
"context" "database/sql"
"fmt" "fmt"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
) )
// SQLiteDatabase provides SQLite database operations for race types // SQLiteDatabase implements Database interface for SQLite
type SQLiteDatabase struct { type SQLiteDatabase struct {
pool *sqlitex.Pool db *sql.DB
} }
// NewSQLiteDatabase creates a new SQLite database implementation // NewSQLiteDatabase creates a new SQLite database implementation
func NewSQLiteDatabase(pool *sqlitex.Pool) *SQLiteDatabase { func NewSQLiteDatabase(db *sql.DB) *SQLiteDatabase {
return &SQLiteDatabase{pool: pool} return &SQLiteDatabase{db: db}
}
// LoadRaceTypes loads all race types from the database
func (db *SQLiteDatabase) LoadRaceTypes(masterList *MasterRaceTypeList) error {
conn, err := db.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer db.pool.Put(conn)
query := `
SELECT model_type, race_id, category, subcategory, model_name
FROM race_types
WHERE race_id > 0
`
count := 0
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
modelType := int16(stmt.ColumnInt(0))
raceID := int16(stmt.ColumnInt(1))
category := stmt.ColumnText(2)
subcategory := stmt.ColumnText(3)
modelName := stmt.ColumnText(4)
// Add to master list
if masterList.AddRaceType(modelType, raceID, category, subcategory, modelName, false) {
count++
}
return nil
},
})
if err != nil {
return fmt.Errorf("failed to query race types: %w", err)
}
return nil
}
// SaveRaceType saves a single race type to the database
func (db *SQLiteDatabase) SaveRaceType(modelType int16, raceType *RaceType) error {
if raceType == nil || !raceType.IsValid() {
return fmt.Errorf("invalid race type")
}
conn, err := db.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer db.pool.Put(conn)
query := `
INSERT OR REPLACE INTO race_types (model_type, race_id, category, subcategory, model_name)
VALUES (?, ?, ?, ?, ?)
`
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
Args: []any{modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName},
})
if err != nil {
return fmt.Errorf("failed to save race type: %w", err)
}
return nil
}
// DeleteRaceType removes a race type from the database
func (db *SQLiteDatabase) DeleteRaceType(modelType int16) error {
conn, err := db.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer db.pool.Put(conn)
query := `DELETE FROM race_types WHERE model_type = ?`
err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{
Args: []any{modelType},
})
if err != nil {
return fmt.Errorf("failed to delete race type: %w", err)
}
rowsAffected := int64(conn.Changes())
if rowsAffected == 0 {
return fmt.Errorf("race type with model_type %d not found", modelType)
}
return nil
} }
// CreateRaceTypesTable creates the race_types table if it doesn't exist // CreateRaceTypesTable creates the race_types table if it doesn't exist
func (db *SQLiteDatabase) CreateRaceTypesTable() error { func (sdb *SQLiteDatabase) CreateRaceTypesTable() error {
conn, err := db.pool.Take(context.Background())
if err != nil {
return fmt.Errorf("failed to get connection: %w", err)
}
defer db.pool.Put(conn)
query := ` query := `
CREATE TABLE IF NOT EXISTS race_types ( CREATE TABLE IF NOT EXISTS race_types (
model_type INTEGER PRIMARY KEY, model_type INTEGER PRIMARY KEY,
race_id INTEGER NOT NULL, race_id INTEGER NOT NULL CHECK (race_id > 0),
category TEXT, category TEXT,
subcategory TEXT, subcategory TEXT,
model_name TEXT, model_name TEXT
CHECK (race_id > 0) )`
)
`
if err := sqlitex.ExecuteTransient(conn, query, nil); err != nil { _, err := sdb.db.Exec(query)
if err != nil {
return fmt.Errorf("failed to create race_types table: %w", err) return fmt.Errorf("failed to create race_types table: %w", err)
} }
// Create index on race_id for faster lookups // Create index for faster category lookups
indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_race_id ON race_types(race_id)` indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_category ON race_types(category)`
if err := sqlitex.ExecuteTransient(conn, indexQuery, nil); err != nil { _, err = sdb.db.Exec(indexQuery)
return fmt.Errorf("failed to create race_id index: %w", err) if err != nil {
}
// Create index on category for category-based queries
categoryIndexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_category ON race_types(category)`
if err := sqlitex.ExecuteTransient(conn, categoryIndexQuery, nil); err != nil {
return fmt.Errorf("failed to create category index: %w", err) return fmt.Errorf("failed to create category index: %w", err)
} }
return nil return nil
} }
// LoadRaceTypes loads all race types from the database
func (sdb *SQLiteDatabase) LoadRaceTypes(masterList *MasterRaceTypeList) error {
query := `SELECT model_type, race_id, category, subcategory, model_name FROM race_types`
rows, err := sdb.db.Query(query)
if err != nil {
return fmt.Errorf("failed to query race types: %w", err)
}
defer rows.Close()
loadedCount := 0
for rows.Next() {
var modelType, raceID int16
var category, subcategory, modelName sql.NullString
err := rows.Scan(&modelType, &raceID, &category, &subcategory, &modelName)
if err != nil {
return fmt.Errorf("failed to scan race type row: %w", err)
}
// Create race type with proper defaults
raceType := &RaceType{
RaceTypeID: raceID,
Category: category.String,
Subcategory: subcategory.String,
ModelName: modelName.String,
}
// If category is empty, derive it from race type ID
if raceType.Category == "" {
raceType.Category = GetCategoryName(raceID)
}
// Add to master list
if err := masterList.AddRaceType(modelType, raceType); err != nil {
return fmt.Errorf("failed to add race type %d: %w", modelType, err)
}
loadedCount++
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating race type rows: %w", err)
}
return nil
}
// SaveRaceType saves a single race type to the database (for admin/testing)
func (sdb *SQLiteDatabase) SaveRaceType(modelType int16, raceType *RaceType) error {
if raceType == nil {
return fmt.Errorf("race type is nil")
}
query := `
INSERT OR REPLACE INTO race_types
(model_type, race_id, category, subcategory, model_name)
VALUES (?, ?, ?, ?, ?)`
_, err := sdb.db.Exec(query, modelType, raceType.RaceTypeID,
raceType.Category, raceType.Subcategory, raceType.ModelName)
if err != nil {
return fmt.Errorf("failed to save race type %d: %w", modelType, err)
}
return nil
}
// DeleteRaceType removes a race type from the database (for admin/testing)
func (sdb *SQLiteDatabase) DeleteRaceType(modelType int16) error {
query := `DELETE FROM race_types WHERE model_type = ?`
result, err := sdb.db.Exec(query, modelType)
if err != nil {
return fmt.Errorf("failed to delete race type %d: %w", modelType, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("race type %d not found", modelType)
}
return nil
}
// GetRaceTypeCount returns the total number of race types in the database
func (sdb *SQLiteDatabase) GetRaceTypeCount() (int64, error) {
query := `SELECT COUNT(*) FROM race_types`
var count int64
err := sdb.db.QueryRow(query).Scan(&count)
if err != nil {
return 0, fmt.Errorf("failed to get race type count: %w", err)
}
return count, nil
}

View File

@ -1,171 +0,0 @@
package race_types
// Database interface for race type persistence
type Database interface {
LoadRaceTypes(masterList *MasterRaceTypeList) error
SaveRaceType(modelType int16, raceType *RaceType) error
DeleteRaceType(modelType int16) error
CreateRaceTypesTable() error
}
// Logger interface for race type logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// RaceTypeProvider defines the interface for accessing race type information
type RaceTypeProvider interface {
// GetRaceType returns the race type ID for a given model ID
GetRaceType(modelID int16) int16
// GetRaceBaseType returns the base category type for a model ID
GetRaceBaseType(modelID int16) int16
// GetRaceTypeCategory returns the category name for a model ID
GetRaceTypeCategory(modelID int16) string
// GetRaceTypeSubCategory returns the subcategory name for a model ID
GetRaceTypeSubCategory(modelID int16) string
// GetRaceTypeModelName returns the model name for a model ID
GetRaceTypeModelName(modelID int16) string
// GetRaceTypeInfo returns complete race type information for a model ID
GetRaceTypeInfo(modelID int16) *RaceType
}
// CreatureTypeChecker defines the interface for checking creature types
type CreatureTypeChecker interface {
// IsCreatureType checks if a model ID represents a specific creature type
IsCreatureType(modelID int16, creatureType int16) bool
// IsUndead checks if a model ID represents an undead creature
IsUndead(modelID int16) bool
// IsDragonkind checks if a model ID represents a dragon-type creature
IsDragonkind(modelID int16) bool
// IsMagical checks if a model ID represents a magical creature
IsMagical(modelID int16) bool
// IsNatural checks if a model ID represents a natural creature
IsNatural(modelID int16) bool
// IsSentient checks if a model ID represents a sentient being
IsSentient(modelID int16) bool
}
// RaceTypeAware defines the interface for entities that have a race type
type RaceTypeAware interface {
// GetModelType returns the model type ID of the entity
GetModelType() int16
// SetModelType sets the model type ID of the entity
SetModelType(modelType int16)
}
// NPCRaceTypeAdapter provides race type functionality for NPCs
type NPCRaceTypeAdapter struct {
npc RaceTypeAware
raceTypeProvider RaceTypeProvider
}
// NewNPCRaceTypeAdapter creates a new NPC race type adapter
func NewNPCRaceTypeAdapter(npc RaceTypeAware, provider RaceTypeProvider) *NPCRaceTypeAdapter {
return &NPCRaceTypeAdapter{
npc: npc,
raceTypeProvider: provider,
}
}
// GetRaceType returns the race type ID for the NPC
func (a *NPCRaceTypeAdapter) GetRaceType() int16 {
if a.raceTypeProvider == nil {
return 0
}
return a.raceTypeProvider.GetRaceType(a.npc.GetModelType())
}
// GetRaceBaseType returns the base category type for the NPC
func (a *NPCRaceTypeAdapter) GetRaceBaseType() int16 {
if a.raceTypeProvider == nil {
return 0
}
return a.raceTypeProvider.GetRaceBaseType(a.npc.GetModelType())
}
// GetRaceTypeCategory returns the category name for the NPC
func (a *NPCRaceTypeAdapter) GetRaceTypeCategory() string {
if a.raceTypeProvider == nil {
return ""
}
return a.raceTypeProvider.GetRaceTypeCategory(a.npc.GetModelType())
}
// GetRaceTypeInfo returns complete race type information for the NPC
func (a *NPCRaceTypeAdapter) GetRaceTypeInfo() *RaceType {
if a.raceTypeProvider == nil {
return nil
}
return a.raceTypeProvider.GetRaceTypeInfo(a.npc.GetModelType())
}
// IsUndead checks if the NPC is an undead creature
func (a *NPCRaceTypeAdapter) IsUndead() bool {
baseType := a.GetRaceBaseType()
return baseType == Undead
}
// IsDragonkind checks if the NPC is a dragon-type creature
func (a *NPCRaceTypeAdapter) IsDragonkind() bool {
baseType := a.GetRaceBaseType()
return baseType == Dragonkind
}
// IsMagical checks if the NPC is a magical creature
func (a *NPCRaceTypeAdapter) IsMagical() bool {
baseType := a.GetRaceBaseType()
return baseType == Magical || baseType == Mechanimagical
}
// IsNatural checks if the NPC is a natural creature
func (a *NPCRaceTypeAdapter) IsNatural() bool {
baseType := a.GetRaceBaseType()
return baseType == Natural
}
// IsSentient checks if the NPC is a sentient being
func (a *NPCRaceTypeAdapter) IsSentient() bool {
baseType := a.GetRaceBaseType()
return baseType == Sentient
}
// DamageModifier defines an interface for race-type-based damage calculations
type DamageModifier interface {
// GetDamageModifier returns damage modifier based on attacker and target race types
// This could be used for implementing race-specific damage bonuses/penalties
GetDamageModifier(attackerModelID, targetModelID int16) float32
}
// LootModifier defines an interface for race-type-based loot modifications
type LootModifier interface {
// GetLootTableModifier returns loot table modifications based on creature race type
// This could be used for implementing race-specific loot tables
GetLootTableModifier(modelID int16) string
}
// AIBehaviorModifier defines an interface for race-type-based AI behavior
type AIBehaviorModifier interface {
// GetAIBehaviorFlags returns AI behavior flags based on race type
// This could be used for implementing race-specific AI behaviors
GetAIBehaviorFlags(modelID int16) uint32
// GetAggressionLevel returns aggression level based on race type
GetAggressionLevel(modelID int16) int8
// GetFleeHealthPercent returns health percentage at which creature flees
GetFleeHealthPercent(modelID int16) float32
}

View File

@ -2,18 +2,13 @@ package race_types
import ( import (
"fmt" "fmt"
"strings"
"sync"
) )
// Manager provides high-level race type management // Manager provides high-level race type operations
type Manager struct { type Manager struct {
masterList *MasterRaceTypeList masterList *MasterRaceTypeList
database Database database Database
logger Logger logger Logger
// Thread safety for manager operations
mutex sync.RWMutex
} }
// NewManager creates a new race type manager // NewManager creates a new race type manager
@ -25,297 +20,190 @@ func NewManager(database Database, logger Logger) *Manager {
} }
} }
// Initialize sets up the race type system // Initialize loads race types from database
func (m *Manager) Initialize() error { func (m *Manager) Initialize() error {
m.mutex.Lock() if m.database == nil {
defer m.mutex.Unlock() return fmt.Errorf("no database provided")
}
// Create table if needed
// Create database table if needed
if err := m.database.CreateRaceTypesTable(); err != nil { if err := m.database.CreateRaceTypesTable(); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to create race types table: %v", err)
}
return fmt.Errorf("failed to create race types table: %w", err) return fmt.Errorf("failed to create race types table: %w", err)
} }
// Load race types from database // Load race types from database
if err := m.database.LoadRaceTypes(m.masterList); err != nil { if err := m.database.LoadRaceTypes(m.masterList); err != nil {
if m.logger != nil {
m.logger.LogError("Failed to load race types: %v", err)
}
return fmt.Errorf("failed to load race types: %w", err) return fmt.Errorf("failed to load race types: %w", err)
} }
if m.logger != nil { if m.logger != nil {
m.logger.LogInfo("Race type system initialized with %d race types", m.masterList.Count()) stats := m.masterList.GetStatistics()
m.logger.LogInfo("Loaded %d race types", stats.TotalRaceTypes)
} }
return nil return nil
} }
// GetRaceType returns the race type ID for a given model ID // Core RaceTypeProvider interface implementation
// GetRaceType returns the race type ID for a model ID
func (m *Manager) GetRaceType(modelID int16) int16 { func (m *Manager) GetRaceType(modelID int16) int16 {
return m.masterList.GetRaceType(modelID) raceType := m.masterList.GetRaceType(modelID)
} if raceType != nil {
return raceType.RaceTypeID
// GetRaceTypeInfo returns complete race type information for a model ID }
func (m *Manager) GetRaceTypeInfo(modelID int16) *RaceType { return 0
return m.masterList.GetRaceTypeByModelID(modelID)
}
// GetRaceBaseType returns the base category type for a model ID
func (m *Manager) GetRaceBaseType(modelID int16) int16 {
return m.masterList.GetRaceBaseType(modelID)
} }
// GetRaceTypeCategory returns the category name for a model ID // GetRaceTypeCategory returns the category name for a model ID
func (m *Manager) GetRaceTypeCategory(modelID int16) string { func (m *Manager) GetRaceTypeCategory(modelID int16) string {
return m.masterList.GetRaceTypeCategory(modelID)
}
// GetRaceTypeSubCategory returns the subcategory name for a model ID
func (m *Manager) GetRaceTypeSubCategory(modelID int16) string {
return m.masterList.GetRaceTypeSubCategory(modelID)
}
// GetRaceTypeModelName returns the model name for a model ID
func (m *Manager) GetRaceTypeModelName(modelID int16) string {
return m.masterList.GetRaceTypeModelName(modelID)
}
// AddRaceType adds a new race type to the system
func (m *Manager) AddRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Validate input
if raceTypeID <= 0 {
return fmt.Errorf("invalid race type ID: %d", raceTypeID)
}
// Add to master list
if !m.masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) {
return fmt.Errorf("race type already exists for model ID %d", modelID)
}
// Save to database
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
if err := m.database.SaveRaceType(modelID, raceType); err != nil {
// Rollback from master list
m.masterList.Clear() // This is not ideal but ensures consistency
m.database.LoadRaceTypes(m.masterList)
return fmt.Errorf("failed to save race type: %w", err)
}
return nil
}
// UpdateRaceType updates an existing race type
func (m *Manager) UpdateRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists
if m.masterList.GetRaceType(modelID) == 0 {
return fmt.Errorf("race type not found for model ID %d", modelID)
}
// Update in master list
if !m.masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, true) {
return fmt.Errorf("failed to update race type in master list")
}
// Save to database
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
if err := m.database.SaveRaceType(modelID, raceType); err != nil {
// Reload from database to ensure consistency
m.masterList.Clear()
m.database.LoadRaceTypes(m.masterList)
return fmt.Errorf("failed to update race type in database: %w", err)
}
return nil
}
// RemoveRaceType removes a race type from the system
func (m *Manager) RemoveRaceType(modelID int16) error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists
if m.masterList.GetRaceType(modelID) == 0 {
return fmt.Errorf("race type not found for model ID %d", modelID)
}
// Delete from database first
if err := m.database.DeleteRaceType(modelID); err != nil {
return fmt.Errorf("failed to delete race type from database: %w", err)
}
// Reload master list to ensure consistency
m.masterList.Clear()
m.database.LoadRaceTypes(m.masterList)
return nil
}
// GetRaceTypesByCategory returns all race types for a given category
func (m *Manager) GetRaceTypesByCategory(category string) map[int16]*RaceType {
return m.masterList.GetRaceTypesByCategory(category)
}
// GetRaceTypesBySubcategory returns all race types for a given subcategory
func (m *Manager) GetRaceTypesBySubcategory(subcategory string) map[int16]*RaceType {
return m.masterList.GetRaceTypesBySubcategory(subcategory)
}
// GetAllRaceTypes returns all race types in the system
func (m *Manager) GetAllRaceTypes() map[int16]*RaceType {
return m.masterList.GetAllRaceTypes()
}
// GetStatistics returns race type usage statistics
func (m *Manager) GetStatistics() *Statistics {
return m.masterList.GetStatistics()
}
// IsCreatureType checks if a model ID represents a specific creature type
func (m *Manager) IsCreatureType(modelID int16, creatureType int16) bool {
raceType := m.masterList.GetRaceType(modelID) raceType := m.masterList.GetRaceType(modelID)
if raceType == 0 { if raceType != nil {
return false return raceType.Category
} }
return "UNKNOWN"
// Check exact match
if raceType == creatureType {
return true
}
// Check if it's in the same category
return GetRaceTypeCategory(raceType) == GetRaceTypeCategory(creatureType)
} }
// GetRaceTypeInfo returns complete race type information for a model ID
func (m *Manager) GetRaceTypeInfo(modelID int16) *RaceType {
return m.masterList.GetRaceType(modelID)
}
// Creature type checking methods
// IsUndead checks if a model ID represents an undead creature // IsUndead checks if a model ID represents an undead creature
func (m *Manager) IsUndead(modelID int16) bool { func (m *Manager) IsUndead(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID) raceTypeID := m.GetRaceType(modelID)
return baseType == Undead return raceTypeID >= UndeadBase && raceTypeID <= UndeadMax
} }
// IsDragonkind checks if a model ID represents a dragon-type creature // IsDragonkind checks if a model ID represents a dragon-type creature
func (m *Manager) IsDragonkind(modelID int16) bool { func (m *Manager) IsDragonkind(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID) raceTypeID := m.GetRaceType(modelID)
return baseType == Dragonkind return raceTypeID >= DragonkindBase && raceTypeID <= DragonkindMax
}
// IsFay checks if a model ID represents a fay creature
func (m *Manager) IsFay(modelID int16) bool {
raceTypeID := m.GetRaceType(modelID)
return raceTypeID >= FayBase && raceTypeID <= FayMax
} }
// IsMagical checks if a model ID represents a magical creature // IsMagical checks if a model ID represents a magical creature
func (m *Manager) IsMagical(modelID int16) bool { func (m *Manager) IsMagical(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID) raceTypeID := m.GetRaceType(modelID)
return baseType == Magical || baseType == Mechanimagical return raceTypeID >= MagicalBase && raceTypeID <= MagicalMax
} }
// IsNatural checks if a model ID represents a natural creature // IsNatural checks if a model ID represents a natural creature
func (m *Manager) IsNatural(modelID int16) bool { func (m *Manager) IsNatural(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID) raceTypeID := m.GetRaceType(modelID)
return baseType == Natural return raceTypeID >= NaturalBase && raceTypeID <= NaturalMax
} }
// IsSentient checks if a model ID represents a sentient being // IsPlanar checks if a model ID represents a planar creature
func (m *Manager) IsPlanar(modelID int16) bool {
raceTypeID := m.GetRaceType(modelID)
return raceTypeID >= PlanarBase && raceTypeID <= PlanarMax
}
// IsPlant checks if a model ID represents a plant creature
func (m *Manager) IsPlant(modelID int16) bool {
raceTypeID := m.GetRaceType(modelID)
return raceTypeID >= PlantBase && raceTypeID <= PlantMax
}
// IsSentient checks if a model ID represents a sentient creature
func (m *Manager) IsSentient(modelID int16) bool { func (m *Manager) IsSentient(modelID int16) bool {
baseType := m.masterList.GetRaceBaseType(modelID) raceTypeID := m.GetRaceType(modelID)
return baseType == Sentient return raceTypeID >= SentientBase && raceTypeID <= SentientMax
} }
// GetCategoryDescription returns a human-readable description of a category // IsWere checks if a model ID represents a were creature
func (m *Manager) GetCategoryDescription(category string) string { func (m *Manager) IsWere(modelID int16) bool {
switch strings.ToUpper(category) { raceTypeID := m.GetRaceType(modelID)
case CategoryDragonkind: return raceTypeID >= WereBase && raceTypeID <= WereMax
return "Dragons and dragon-like creatures" }
case CategoryFay:
return "Fae and fairy-type creatures" // Bulk operations
case CategoryMagical:
return "Magical constructs and animated beings" // GetRaceTypesByCategory returns all race types in a category
case CategoryMechanimagical: func (m *Manager) GetRaceTypesByCategory(category string) map[int16]*RaceType {
return "Mechanical and magical hybrid creatures" return m.masterList.GetRaceTypesByCategory(category)
case CategoryNatural: }
return "Natural animals and beasts"
case CategoryPlanar: // GetStatistics returns usage statistics
return "Planar and elemental beings" func (m *Manager) GetStatistics() *Statistics {
case CategoryPlant: return m.masterList.GetStatistics()
return "Plant-based creatures" }
case CategorySentient:
return "Sentient humanoid races" // Utility methods
case CategoryUndead:
return "Undead creatures" // GetAllCategories returns a list of all race type categories
case CategoryWere: func (m *Manager) GetAllCategories() []string {
return "Lycanthropes and shape-shifters" return []string{
default: "DRAGONKIND",
return "Unknown category" "FAY",
"MAGICAL",
"MECHANIMAGICAL",
"NATURAL",
"PLANAR",
"PLANT",
"SENTIENT",
"UNDEAD",
"WERE",
} }
} }
// ProcessCommand handles race type related commands // ValidateRaceType validates a race type structure
func (m *Manager) ProcessCommand(args []string) string { func (m *Manager) ValidateRaceType(raceType *RaceType) error {
if len(args) == 0 { if raceType == nil {
return "Usage: racetype <stats|list|info|category> [args...]" return fmt.Errorf("race type is nil")
} }
if raceType.RaceTypeID <= 0 {
return fmt.Errorf("invalid race type ID: %d", raceType.RaceTypeID)
}
if raceType.Category == "" {
return fmt.Errorf("empty category")
}
// Validate that the race type ID is in the correct category range
expectedCategory := GetCategoryName(raceType.RaceTypeID)
if expectedCategory != raceType.Category {
return fmt.Errorf("race type ID %d should be in category %s, not %s",
raceType.RaceTypeID, expectedCategory, raceType.Category)
}
return nil
}
// AddRaceType adds a race type (for testing/admin purposes)
func (m *Manager) AddRaceType(modelID int16, raceType *RaceType) error {
if err := m.ValidateRaceType(raceType); err != nil {
return fmt.Errorf("invalid race type: %w", err)
}
return m.masterList.AddRaceType(modelID, raceType)
}
// Clear removes all race types (for testing purposes)
func (m *Manager) Clear() {
m.masterList.Clear()
switch strings.ToLower(args[0]) { if m.logger != nil {
case "stats": m.logger.LogDebug("Cleared all race types")
stats := m.GetStatistics()
result := fmt.Sprintf("Race Type Statistics:\n")
result += fmt.Sprintf("Total Race Types: %d\n", stats.TotalRaceTypes)
result += fmt.Sprintf("Queries by Model: %d\n", stats.QueriesByModel)
result += fmt.Sprintf("Queries by Category: %d\n", stats.QueriesByCategory)
result += fmt.Sprintf("\nCategory Counts:\n")
for cat, count := range stats.CategoryCounts {
result += fmt.Sprintf(" %s: %d\n", cat, count)
}
return result
case "list":
if len(args) < 2 {
return "Usage: racetype list <category>"
}
raceTypes := m.GetRaceTypesByCategory(args[1])
if len(raceTypes) == 0 {
return fmt.Sprintf("No race types found for category: %s", args[1])
}
result := fmt.Sprintf("Race types in category %s:\n", args[1])
for modelID, rt := range raceTypes {
result += fmt.Sprintf(" Model %d: %s (%d)\n", modelID, rt.ModelName, rt.RaceTypeID)
}
return result
case "info":
if len(args) < 2 {
return "Usage: racetype info <model_id>"
}
var modelID int16
if _, err := fmt.Sscanf(args[1], "%d", &modelID); err != nil {
return "Invalid model ID"
}
rt := m.GetRaceTypeInfo(modelID)
if rt == nil {
return fmt.Sprintf("No race type found for model ID: %d", modelID)
}
return fmt.Sprintf("Model %d:\n Race Type: %d\n Category: %s\n Subcategory: %s\n Model Name: %s\n Base Type: %s (%d)",
modelID, rt.RaceTypeID, rt.Category, rt.Subcategory, rt.ModelName,
GetCategoryName(GetRaceTypeCategory(rt.RaceTypeID)), GetRaceTypeCategory(rt.RaceTypeID))
case "category":
result := "Race Type Categories:\n"
for _, cat := range []string{CategoryDragonkind, CategoryFay, CategoryMagical, CategoryMechanimagical,
CategoryNatural, CategoryPlanar, CategoryPlant, CategorySentient, CategoryUndead, CategoryWere} {
result += fmt.Sprintf(" %s: %s\n", cat, m.GetCategoryDescription(cat))
}
return result
default:
return "Unknown racetype command. Use: stats, list, info, or category"
} }
}
// Size returns the number of race types loaded
func (m *Manager) Size() int {
return m.masterList.Size()
} }

View File

@ -1,233 +0,0 @@
package race_types
import (
"strings"
"sync"
)
// MasterRaceTypeList manages the mapping between model IDs and race types
// Converted from C++ MasterRaceTypeList class
type MasterRaceTypeList struct {
// Maps model_id -> RaceType
raceList map[int16]*RaceType
// Thread safety
mutex sync.RWMutex
// Statistics tracking
stats *Statistics
}
// NewMasterRaceTypeList creates a new master race type list
// Converted from C++ MasterRaceTypeList constructor
func NewMasterRaceTypeList() *MasterRaceTypeList {
return &MasterRaceTypeList{
raceList: make(map[int16]*RaceType),
stats: NewStatistics(),
}
}
// AddRaceType adds a race type define to the list
// Converted from C++ AddRaceType method
func (m *MasterRaceTypeList) AddRaceType(modelID int16, raceTypeID int16, category, subcategory, modelName string, allowOverride bool) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
// Check if exists and not allowing override
if _, exists := m.raceList[modelID]; exists && !allowOverride {
return false
}
// Create new race type
raceType := &RaceType{
RaceTypeID: raceTypeID,
Category: category,
Subcategory: subcategory,
ModelName: modelName,
}
// Update statistics
if _, exists := m.raceList[modelID]; !exists {
m.stats.TotalRaceTypes++
if category != "" {
m.stats.CategoryCounts[category]++
}
if subcategory != "" {
m.stats.SubcategoryCounts[subcategory]++
}
}
m.raceList[modelID] = raceType
return true
}
// GetRaceType gets the race type for the given model
// Converted from C++ GetRaceType method
func (m *MasterRaceTypeList) GetRaceType(modelID int16) int16 {
m.mutex.RLock()
defer m.mutex.RUnlock()
m.stats.QueriesByModel++
if raceType, exists := m.raceList[modelID]; exists {
return raceType.RaceTypeID
}
return 0
}
// GetRaceTypeCategory gets the category for the given model
// Converted from C++ GetRaceTypeCategory method
func (m *MasterRaceTypeList) GetRaceTypeCategory(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
m.stats.QueriesByCategory++
if raceType, exists := m.raceList[modelID]; exists && raceType.Category != "" {
return raceType.Category
}
return ""
}
// GetRaceTypeSubCategory gets the subcategory for the given model
// Converted from C++ GetRaceTypeSubCategory method
func (m *MasterRaceTypeList) GetRaceTypeSubCategory(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists && raceType.Subcategory != "" {
return raceType.Subcategory
}
return ""
}
// GetRaceTypeModelName gets the model name for the given model
// Converted from C++ GetRaceTypeModelName method
func (m *MasterRaceTypeList) GetRaceTypeModelName(modelID int16) string {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists && raceType.ModelName != "" {
return raceType.ModelName
}
return ""
}
// GetRaceBaseType gets the base race type for the given model
// Converted from C++ GetRaceBaseType method
func (m *MasterRaceTypeList) GetRaceBaseType(modelID int16) int16 {
m.mutex.RLock()
defer m.mutex.RUnlock()
raceType, exists := m.raceList[modelID]
if !exists {
return 0
}
return GetRaceTypeCategory(raceType.RaceTypeID)
}
// GetRaceTypeByModelID returns the full RaceType structure for a model ID
func (m *MasterRaceTypeList) GetRaceTypeByModelID(modelID int16) *RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
if raceType, exists := m.raceList[modelID]; exists {
return raceType.Copy()
}
return nil
}
// GetAllRaceTypes returns a copy of all race types
func (m *MasterRaceTypeList) GetAllRaceTypes() map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType, len(m.raceList))
for modelID, raceType := range m.raceList {
result[modelID] = raceType.Copy()
}
return result
}
// GetRaceTypesByCategory returns all race types for a given category
func (m *MasterRaceTypeList) GetRaceTypesByCategory(category string) map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType)
categoryUpper := strings.ToUpper(category)
for modelID, raceType := range m.raceList {
if strings.ToUpper(raceType.Category) == categoryUpper {
result[modelID] = raceType.Copy()
}
}
return result
}
// GetRaceTypesBySubcategory returns all race types for a given subcategory
func (m *MasterRaceTypeList) GetRaceTypesBySubcategory(subcategory string) map[int16]*RaceType {
m.mutex.RLock()
defer m.mutex.RUnlock()
result := make(map[int16]*RaceType)
subcategoryUpper := strings.ToUpper(subcategory)
for modelID, raceType := range m.raceList {
if strings.ToUpper(raceType.Subcategory) == subcategoryUpper {
result[modelID] = raceType.Copy()
}
}
return result
}
// Count returns the total number of race types
func (m *MasterRaceTypeList) Count() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.raceList)
}
// GetStatistics returns a copy of the statistics
func (m *MasterRaceTypeList) GetStatistics() *Statistics {
m.mutex.RLock()
defer m.mutex.RUnlock()
// Create a copy of the statistics
statsCopy := &Statistics{
TotalRaceTypes: m.stats.TotalRaceTypes,
QueriesByModel: m.stats.QueriesByModel,
QueriesByCategory: m.stats.QueriesByCategory,
CategoryCounts: make(map[string]int64),
SubcategoryCounts: make(map[string]int64),
}
for k, v := range m.stats.CategoryCounts {
statsCopy.CategoryCounts[k] = v
}
for k, v := range m.stats.SubcategoryCounts {
statsCopy.SubcategoryCounts[k] = v
}
return statsCopy
}
// Clear removes all race types from the list
func (m *MasterRaceTypeList) Clear() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.raceList = make(map[int16]*RaceType)
m.stats = NewStatistics()
}

View File

@ -0,0 +1,166 @@
package race_types
import (
"fmt"
"sync"
)
// Core data structures
// RaceType represents a race type structure for NPCs and creatures
type RaceType struct {
RaceTypeID int16 // The actual race type ID
Category string // Main category (e.g., "DRAGONKIND", "NATURAL")
Subcategory string // Subcategory (e.g., "DRAGON", "ANIMAL")
ModelName string // 3D model name for rendering
}
// IsValid checks if the race type has valid data
func (rt *RaceType) IsValid() bool {
return rt != nil && rt.RaceTypeID > 0 && rt.Category != ""
}
// Statistics tracks race type usage
type Statistics struct {
TotalRaceTypes int64 // Total number of race types loaded
Queries int64 // Total queries performed
CategoryCounts map[string]int64 // Count of race types per category
}
// NewStatistics creates a new statistics tracker
func NewStatistics() *Statistics {
return &Statistics{
CategoryCounts: make(map[string]int64),
}
}
// Core interfaces
// Logger interface for race type logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// Database interface for race type persistence
type Database interface {
LoadRaceTypes(masterList *MasterRaceTypeList) error
CreateRaceTypesTable() error
}
// RaceTypeProvider defines the core interface for race type operations
type RaceTypeProvider interface {
// Core lookups
GetRaceType(modelID int16) int16
GetRaceTypeCategory(modelID int16) string
GetRaceTypeInfo(modelID int16) *RaceType
// Creature type checks
IsUndead(modelID int16) bool
IsDragonkind(modelID int16) bool
IsFay(modelID int16) bool
IsMagical(modelID int16) bool
IsNatural(modelID int16) bool
IsPlanar(modelID int16) bool
IsPlant(modelID int16) bool
IsSentient(modelID int16) bool
IsWere(modelID int16) bool
// Bulk operations
GetRaceTypesByCategory(category string) map[int16]*RaceType
GetStatistics() *Statistics
}
// Master race type list
// MasterRaceTypeList manages the collection of all race types
type MasterRaceTypeList struct {
raceTypes map[int16]*RaceType // Map of model ID to race type
statistics *Statistics // Usage statistics
mutex sync.RWMutex // Thread safety
}
// NewMasterRaceTypeList creates a new master race type list
func NewMasterRaceTypeList() *MasterRaceTypeList {
return &MasterRaceTypeList{
raceTypes: make(map[int16]*RaceType),
statistics: NewStatistics(),
}
}
// AddRaceType adds a race type to the list
func (mrtl *MasterRaceTypeList) AddRaceType(modelID int16, raceType *RaceType) error {
if raceType == nil || !raceType.IsValid() {
return fmt.Errorf("invalid race type")
}
mrtl.mutex.Lock()
defer mrtl.mutex.Unlock()
mrtl.raceTypes[modelID] = raceType
mrtl.statistics.TotalRaceTypes++
mrtl.statistics.CategoryCounts[raceType.Category]++
return nil
}
// GetRaceType gets race type by model ID
func (mrtl *MasterRaceTypeList) GetRaceType(modelID int16) *RaceType {
mrtl.mutex.RLock()
defer mrtl.mutex.RUnlock()
mrtl.statistics.Queries++
return mrtl.raceTypes[modelID]
}
// GetRaceTypesByCategory returns all race types in a category
func (mrtl *MasterRaceTypeList) GetRaceTypesByCategory(category string) map[int16]*RaceType {
mrtl.mutex.RLock()
defer mrtl.mutex.RUnlock()
result := make(map[int16]*RaceType)
for modelID, raceType := range mrtl.raceTypes {
if raceType.Category == category {
result[modelID] = raceType
}
}
return result
}
// GetStatistics returns usage statistics
func (mrtl *MasterRaceTypeList) GetStatistics() *Statistics {
mrtl.mutex.RLock()
defer mrtl.mutex.RUnlock()
// Return a copy to prevent race conditions
statsCopy := &Statistics{
TotalRaceTypes: mrtl.statistics.TotalRaceTypes,
Queries: mrtl.statistics.Queries,
CategoryCounts: make(map[string]int64),
}
for k, v := range mrtl.statistics.CategoryCounts {
statsCopy.CategoryCounts[k] = v
}
return statsCopy
}
// Clear removes all race types
func (mrtl *MasterRaceTypeList) Clear() {
mrtl.mutex.Lock()
defer mrtl.mutex.Unlock()
mrtl.raceTypes = make(map[int16]*RaceType)
mrtl.statistics = NewStatistics()
}
// Size returns the number of race types
func (mrtl *MasterRaceTypeList) Size() int {
mrtl.mutex.RLock()
defer mrtl.mutex.RUnlock()
return len(mrtl.raceTypes)
}

View File

@ -1,45 +0,0 @@
package race_types
// RaceType represents a race type structure for NPCs and creatures
// Converted from C++ RaceTypeStructure
type RaceType struct {
RaceTypeID int16 // The actual race type ID
Category string // Main category (e.g., "DRAGONKIND", "NATURAL")
Subcategory string // Subcategory (e.g., "DRAGON", "ANIMAL")
ModelName string // 3D model name for rendering
}
// Copy creates a deep copy of the RaceType
func (rt *RaceType) Copy() *RaceType {
if rt == nil {
return nil
}
return &RaceType{
RaceTypeID: rt.RaceTypeID,
Category: rt.Category,
Subcategory: rt.Subcategory,
ModelName: rt.ModelName,
}
}
// IsValid checks if the race type has valid data
func (rt *RaceType) IsValid() bool {
return rt != nil && rt.RaceTypeID > 0 && rt.Category != ""
}
// Statistics tracks race type usage and queries
type Statistics struct {
TotalRaceTypes int64 // Total number of race types loaded
QueriesByModel int64 // Number of queries by model ID
QueriesByCategory int64 // Number of queries by category
CategoryCounts map[string]int64 // Count of race types per category
SubcategoryCounts map[string]int64 // Count of race types per subcategory
}
// NewStatistics creates a new statistics tracker
func NewStatistics() *Statistics {
return &Statistics{
CategoryCounts: make(map[string]int64),
SubcategoryCounts: make(map[string]int64),
}
}

View File

@ -1,438 +0,0 @@
package npc
import (
"sync"
"time"
"eq2emu/internal/entity"
)
// 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"`
}