simplify npc
This commit is contained in:
parent
afded7da3b
commit
d74f309ade
@ -19,6 +19,9 @@ This document outlines how we successfully simplified the EverQuest II housing p
|
||||
- Items
|
||||
- Items/Loot
|
||||
- Languages
|
||||
- NPC
|
||||
- NPC/AI
|
||||
- NPC/Race Types
|
||||
|
||||
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
||||
|
||||
|
@ -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
422
internal/npc/ai/ai.go
Normal 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
|
||||
}
|
@ -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
664
internal/npc/ai/brains.go
Normal 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)
|
||||
}
|
@ -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
227
internal/npc/ai/custom.go
Normal 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))
|
||||
}
|
@ -1,169 +1,26 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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
|
||||
type AIManager struct {
|
||||
brains map[int32]Brain // Map of NPC ID to brain
|
||||
activeCount int64 // Number of active brains
|
||||
totalThinks int64 // Total think cycles processed
|
||||
logger Logger // Logger for AI events
|
||||
luaInterface LuaInterface // Lua script interface
|
||||
brains map[int32]Brain // Map of NPC ID to brain
|
||||
activeCount int64 // Number of active brains
|
||||
totalThinks int64 // Total think cycles processed
|
||||
logger Logger // Logger for AI events
|
||||
// No external dependencies needed
|
||||
}
|
||||
|
||||
// NewAIManager creates a new AI manager
|
||||
func NewAIManager(logger Logger, luaInterface LuaInterface) *AIManager {
|
||||
func NewAIManager(logger Logger) *AIManager {
|
||||
return &AIManager{
|
||||
brains: make(map[int32]Brain),
|
||||
activeCount: 0,
|
||||
totalThinks: 0,
|
||||
logger: logger,
|
||||
luaInterface: luaInterface,
|
||||
brains: make(map[int32]Brain),
|
||||
activeCount: 0,
|
||||
totalThinks: 0,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,8 +85,10 @@ func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...any)
|
||||
case BrainTypeBlank:
|
||||
brain = NewBlankBrain(npc, am.logger)
|
||||
|
||||
case BrainTypeLua:
|
||||
brain = NewLuaBrain(npc, am.logger, am.luaInterface)
|
||||
case BrainTypeCustom:
|
||||
// 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:
|
||||
if len(options) >= 2 {
|
||||
@ -347,14 +206,137 @@ func (am *AIManager) getBrainCountsByType() map[string]int {
|
||||
return counts
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// AI Communication and Packets
|
||||
|
||||
// SendAIUpdate sends an AI update packet for an NPC
|
||||
func (am *AIManager) SendAIUpdate(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(),
|
||||
"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
|
||||
type AIBrainAdapter struct {
|
||||
npc NPC
|
||||
@ -400,33 +382,6 @@ func (aba *AIBrainAdapter) SetupPetBrain(combatPet bool) Brain {
|
||||
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
|
||||
type HateListDebugger struct {
|
||||
logger Logger
|
||||
@ -484,3 +439,13 @@ func (hld *HateListDebugger) PrintEncounterList(npcName string, encounterList ma
|
||||
|
||||
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)
|
||||
}
|
@ -475,30 +475,4 @@ func (bs *BrainState) SetDebugLevel(level int8) {
|
||||
bs.mutex.Lock()
|
||||
defer bs.mutex.Unlock()
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
1569
internal/npc/npc.go
1569
internal/npc/npc.go
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -26,7 +26,7 @@ const (
|
||||
Siren = 118
|
||||
Spirit = 119
|
||||
Sprite = 120
|
||||
Treant = 121 // L&L 8
|
||||
Treant = 121
|
||||
Wisp = 122
|
||||
|
||||
// MAGICAL category (123-154)
|
||||
@ -68,276 +68,201 @@ const (
|
||||
Clockwork = 156
|
||||
IronGuardian = 157
|
||||
|
||||
// NATURAL category (158-239)
|
||||
Natural = 158
|
||||
Animal = 159
|
||||
Aquatic = 160
|
||||
Avian = 161
|
||||
Canine = 162
|
||||
Equine = 163
|
||||
Feline = 164
|
||||
Insect = 165
|
||||
Primate = 166
|
||||
Reptile = 167
|
||||
Anemone = 168
|
||||
Apopheli = 169
|
||||
Armadillo = 170
|
||||
Badger = 171
|
||||
Barracuda = 172
|
||||
Basilisk = 173
|
||||
Bat = 174
|
||||
Bear = 175
|
||||
Beaver = 176
|
||||
Beetle = 177
|
||||
Bovine = 178
|
||||
Brontotherium = 179
|
||||
Brute = 180
|
||||
Camel = 181
|
||||
Cat = 182
|
||||
Centipede = 183
|
||||
Cerberus = 184
|
||||
Chimera = 185
|
||||
Chokidai = 186
|
||||
Cobra = 187
|
||||
Cockatrice = 188
|
||||
Crab = 189
|
||||
Crocodile = 190
|
||||
Deer = 191
|
||||
Dragonfly = 192
|
||||
Duck = 193
|
||||
Eel = 194
|
||||
Elephant = 195
|
||||
FlyingSnake = 196
|
||||
Frog = 197
|
||||
Goat = 198
|
||||
Gorilla = 199
|
||||
Griffin = 200
|
||||
Hawk = 201
|
||||
HiveQueen = 202
|
||||
Horse = 203
|
||||
Hyena = 204
|
||||
KhoalRat = 205
|
||||
Kybur = 206
|
||||
Leech = 207
|
||||
Leopard = 208
|
||||
Lion = 209
|
||||
Lizard = 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
|
||||
|
||||
// NATURAL category (158-239) - Essential ones
|
||||
Natural = 158
|
||||
Animal = 159
|
||||
Aquatic = 160
|
||||
Avian = 161
|
||||
Canine = 162
|
||||
Equine = 163
|
||||
Feline = 164
|
||||
Insect = 165
|
||||
Primate = 166
|
||||
Reptile = 167
|
||||
Rodent = 168
|
||||
Bear = 169
|
||||
Boar = 170
|
||||
Bull = 171
|
||||
Cat = 172
|
||||
Chimera = 173
|
||||
Cockatrice = 174
|
||||
Crab = 175
|
||||
Crocodile = 176
|
||||
Dog = 177
|
||||
Elephant = 178
|
||||
Fish = 179
|
||||
Frog = 180
|
||||
Gobbler = 181
|
||||
Griffin = 182
|
||||
Hippogriff = 183
|
||||
Horse = 184
|
||||
Jellyfish = 185
|
||||
Kraken = 186
|
||||
Lion = 187
|
||||
Lizard = 188
|
||||
Mammoth = 189
|
||||
Mantis = 190
|
||||
Monkey = 191
|
||||
Octopus = 192
|
||||
Owl = 193
|
||||
Panther = 194
|
||||
Pegasus = 195
|
||||
Phoenix = 196
|
||||
Pig = 197
|
||||
Rabbit = 198
|
||||
Raptor = 199
|
||||
Rat = 200
|
||||
Rhinoceros = 201
|
||||
Scorpion = 202
|
||||
Shark = 203
|
||||
Snake = 204
|
||||
Spider = 205
|
||||
Tiger = 206
|
||||
Turtle = 207
|
||||
Whale = 208
|
||||
Wolf = 209
|
||||
Worg = 210
|
||||
|
||||
// PLANAR category (240-267)
|
||||
Planar = 240
|
||||
Abomination = 241
|
||||
AirElemental = 242
|
||||
Amygdalan = 243
|
||||
Avatar = 244
|
||||
Cyclops = 245
|
||||
Demon = 246
|
||||
Djinn = 247
|
||||
EarthElemental = 248
|
||||
Efreeti = 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
|
||||
Planar = 240
|
||||
Demon = 241
|
||||
Devil = 242
|
||||
Djinni = 243
|
||||
Efreet = 244
|
||||
Elemental = 245
|
||||
Genie = 246
|
||||
Modoc = 247
|
||||
Nightmare = 248
|
||||
Shadowman = 249
|
||||
|
||||
// PLANT category (268-274)
|
||||
Plant = 268
|
||||
CarnivorousPlant = 269
|
||||
Catoplebas = 270
|
||||
Mantrap = 271
|
||||
RootAbomination = 272
|
||||
RootHorror = 273
|
||||
Succulent = 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
|
||||
Plant = 268
|
||||
Carniplant = 269
|
||||
Ent = 270
|
||||
FlowerPot = 271
|
||||
Maneater = 272
|
||||
Shambler = 273
|
||||
VineCreeper = 274
|
||||
|
||||
// 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 = 333
|
||||
Ghost = 334
|
||||
Ghoul = 335
|
||||
Gunthak = 336
|
||||
Horror = 337
|
||||
Mummy = 338
|
||||
ShinreeOrcs = 339
|
||||
Skeleton = 340 // L&L 6
|
||||
Spectre = 341
|
||||
VampireNPC = 342
|
||||
Zombie = 343 // L&L 7
|
||||
Apparition = 334
|
||||
Banshee = 335
|
||||
Ghost = 336
|
||||
Ghoul = 337
|
||||
Lich = 338
|
||||
Mummy = 339
|
||||
Revenant = 340
|
||||
Skeleton = 341
|
||||
Spectre = 342
|
||||
Zombie = 343
|
||||
|
||||
// WERE category (344-347)
|
||||
Were = 344
|
||||
AhrounWerewolves = 345
|
||||
LykulakWerewolves = 346
|
||||
Werewolf = 347
|
||||
Were = 344
|
||||
Werewolf = 345
|
||||
Werebear = 346
|
||||
Werebat = 347
|
||||
)
|
||||
|
||||
// Category name constants
|
||||
// Category base ranges for type checking
|
||||
const (
|
||||
CategoryDragonkind = "DRAGONKIND"
|
||||
CategoryFay = "FAY"
|
||||
CategoryMagical = "MAGICAL"
|
||||
CategoryMechanimagical = "MECHANIMAGICAL"
|
||||
CategoryNatural = "NATURAL"
|
||||
CategoryPlanar = "PLANAR"
|
||||
CategoryPlant = "PLANT"
|
||||
CategorySentient = "SENTIENT"
|
||||
CategoryUndead = "UNDEAD"
|
||||
CategoryWere = "WERE"
|
||||
DragonkindBase = 101
|
||||
DragonkindMax = 110
|
||||
FayBase = 111
|
||||
FayMax = 122
|
||||
MagicalBase = 123
|
||||
MagicalMax = 154
|
||||
MechanimagicalBase = 155
|
||||
MechanimagicalMax = 157
|
||||
NaturalBase = 158
|
||||
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
|
||||
// Converted from C++ GetRaceTypeCategory function
|
||||
func GetRaceTypeCategory(raceTypeID int16) int16 {
|
||||
// GetCategoryName returns the category name for a race type ID
|
||||
func GetCategoryName(raceTypeID int16) string {
|
||||
switch {
|
||||
case raceTypeID >= Dragonkind && raceTypeID <= Wyvern:
|
||||
return Dragonkind
|
||||
case raceTypeID >= Fay && raceTypeID <= Wisp:
|
||||
return Fay
|
||||
case raceTypeID >= Magical && raceTypeID <= WoodElemental:
|
||||
return Magical
|
||||
case raceTypeID >= Mechanimagical && raceTypeID <= IronGuardian:
|
||||
return Mechanimagical
|
||||
case raceTypeID >= Natural && raceTypeID <= Yeti:
|
||||
return Natural
|
||||
case raceTypeID >= Planar && raceTypeID <= Wraith:
|
||||
return Planar
|
||||
case raceTypeID >= Plant && raceTypeID <= Succulent:
|
||||
return Plant
|
||||
case raceTypeID >= Sentient && raceTypeID <= Yhalei:
|
||||
return Sentient
|
||||
case raceTypeID >= Undead && raceTypeID <= Zombie:
|
||||
return Undead
|
||||
case raceTypeID >= Were && raceTypeID <= Werewolf:
|
||||
return Were
|
||||
case raceTypeID >= DragonkindBase && raceTypeID <= DragonkindMax:
|
||||
return "DRAGONKIND"
|
||||
case raceTypeID >= FayBase && raceTypeID <= FayMax:
|
||||
return "FAY"
|
||||
case raceTypeID >= MagicalBase && raceTypeID <= MagicalMax:
|
||||
return "MAGICAL"
|
||||
case raceTypeID >= MechanimagicalBase && raceTypeID <= MechanimagicalMax:
|
||||
return "MECHANIMAGICAL"
|
||||
case raceTypeID >= NaturalBase && raceTypeID <= NaturalMax:
|
||||
return "NATURAL"
|
||||
case raceTypeID >= PlanarBase && raceTypeID <= PlanarMax:
|
||||
return "PLANAR"
|
||||
case raceTypeID >= PlantBase && raceTypeID <= PlantMax:
|
||||
return "PLANT"
|
||||
case raceTypeID >= SentientBase && raceTypeID <= SentientMax:
|
||||
return "SENTIENT"
|
||||
case raceTypeID >= UndeadBase && raceTypeID <= UndeadMax:
|
||||
return "UNDEAD"
|
||||
case raceTypeID >= WereBase && raceTypeID <= WereMax:
|
||||
return "WERE"
|
||||
default:
|
||||
return 0
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// GetCategoryName returns the category name for a given category ID
|
||||
func GetCategoryName(categoryID int16) string {
|
||||
switch categoryID {
|
||||
case Dragonkind:
|
||||
return CategoryDragonkind
|
||||
case Fay:
|
||||
return CategoryFay
|
||||
case Magical:
|
||||
return CategoryMagical
|
||||
case Mechanimagical:
|
||||
return CategoryMechanimagical
|
||||
case Natural:
|
||||
return CategoryNatural
|
||||
case Planar:
|
||||
return CategoryPlanar
|
||||
case Plant:
|
||||
return CategoryPlant
|
||||
case Sentient:
|
||||
return CategorySentient
|
||||
case Undead:
|
||||
return CategoryUndead
|
||||
case Were:
|
||||
return CategoryWere
|
||||
// GetCategoryBase returns the base ID for a category
|
||||
func GetCategoryBase(categoryName string) int16 {
|
||||
switch categoryName {
|
||||
case "DRAGONKIND":
|
||||
return DragonkindBase
|
||||
case "FAY":
|
||||
return FayBase
|
||||
case "MAGICAL":
|
||||
return MagicalBase
|
||||
case "MECHANIMAGICAL":
|
||||
return MechanimagicalBase
|
||||
case "NATURAL":
|
||||
return NaturalBase
|
||||
case "PLANAR":
|
||||
return PlanarBase
|
||||
case "PLANT":
|
||||
return PlantBase
|
||||
case "SENTIENT":
|
||||
return SentientBase
|
||||
case "UNDEAD":
|
||||
return UndeadBase
|
||||
case "WERE":
|
||||
return WereBase
|
||||
default:
|
||||
return ""
|
||||
return 0
|
||||
}
|
||||
}
|
@ -1,149 +1,145 @@
|
||||
package race_types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"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 {
|
||||
pool *sqlitex.Pool
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewSQLiteDatabase creates a new SQLite database implementation
|
||||
func NewSQLiteDatabase(pool *sqlitex.Pool) *SQLiteDatabase {
|
||||
return &SQLiteDatabase{pool: pool}
|
||||
}
|
||||
|
||||
// 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
|
||||
func NewSQLiteDatabase(db *sql.DB) *SQLiteDatabase {
|
||||
return &SQLiteDatabase{db: db}
|
||||
}
|
||||
|
||||
// CreateRaceTypesTable creates the race_types table if it doesn't exist
|
||||
func (db *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)
|
||||
|
||||
func (sdb *SQLiteDatabase) CreateRaceTypesTable() error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS race_types (
|
||||
model_type INTEGER PRIMARY KEY,
|
||||
race_id INTEGER NOT NULL,
|
||||
category TEXT,
|
||||
subcategory TEXT,
|
||||
model_name TEXT,
|
||||
CHECK (race_id > 0)
|
||||
)
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS race_types (
|
||||
model_type INTEGER PRIMARY KEY,
|
||||
race_id INTEGER NOT NULL CHECK (race_id > 0),
|
||||
category TEXT,
|
||||
subcategory TEXT,
|
||||
model_name TEXT
|
||||
)`
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Create index on race_id for faster lookups
|
||||
indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_race_id ON race_types(race_id)`
|
||||
if err := sqlitex.ExecuteTransient(conn, indexQuery, nil); err != nil {
|
||||
return fmt.Errorf("failed to create race_id index: %w", err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Create index for faster category lookups
|
||||
indexQuery := `CREATE INDEX IF NOT EXISTS idx_race_types_category ON race_types(category)`
|
||||
_, err = sdb.db.Exec(indexQuery)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create category index: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -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
|
||||
}
|
@ -2,18 +2,13 @@ package race_types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Manager provides high-level race type management
|
||||
// Manager provides high-level race type operations
|
||||
type Manager struct {
|
||||
masterList *MasterRaceTypeList
|
||||
database Database
|
||||
logger Logger
|
||||
|
||||
// Thread safety for manager operations
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// 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 {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// Create table if needed
|
||||
if m.database == nil {
|
||||
return fmt.Errorf("no database provided")
|
||||
}
|
||||
|
||||
// Create database table if needed
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
// Load race types from database
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return m.masterList.GetRaceType(modelID)
|
||||
}
|
||||
|
||||
// GetRaceTypeInfo returns complete race type information for a model ID
|
||||
func (m *Manager) GetRaceTypeInfo(modelID int16) *RaceType {
|
||||
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)
|
||||
raceType := m.masterList.GetRaceType(modelID)
|
||||
if raceType != nil {
|
||||
return raceType.RaceTypeID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetRaceTypeCategory returns the category name for a model ID
|
||||
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)
|
||||
if raceType == 0 {
|
||||
return false
|
||||
if raceType != nil {
|
||||
return raceType.Category
|
||||
}
|
||||
|
||||
// Check exact match
|
||||
if raceType == creatureType {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if it's in the same category
|
||||
return GetRaceTypeCategory(raceType) == GetRaceTypeCategory(creatureType)
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
// 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
|
||||
func (m *Manager) IsUndead(modelID int16) bool {
|
||||
baseType := m.masterList.GetRaceBaseType(modelID)
|
||||
return baseType == Undead
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
return raceTypeID >= UndeadBase && raceTypeID <= UndeadMax
|
||||
}
|
||||
|
||||
// IsDragonkind checks if a model ID represents a dragon-type creature
|
||||
func (m *Manager) IsDragonkind(modelID int16) bool {
|
||||
baseType := m.masterList.GetRaceBaseType(modelID)
|
||||
return baseType == Dragonkind
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
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
|
||||
func (m *Manager) IsMagical(modelID int16) bool {
|
||||
baseType := m.masterList.GetRaceBaseType(modelID)
|
||||
return baseType == Magical || baseType == Mechanimagical
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
return raceTypeID >= MagicalBase && raceTypeID <= MagicalMax
|
||||
}
|
||||
|
||||
// IsNatural checks if a model ID represents a natural creature
|
||||
func (m *Manager) IsNatural(modelID int16) bool {
|
||||
baseType := m.masterList.GetRaceBaseType(modelID)
|
||||
return baseType == Natural
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
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 {
|
||||
baseType := m.masterList.GetRaceBaseType(modelID)
|
||||
return baseType == Sentient
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
return raceTypeID >= SentientBase && raceTypeID <= SentientMax
|
||||
}
|
||||
|
||||
// GetCategoryDescription returns a human-readable description of a category
|
||||
func (m *Manager) GetCategoryDescription(category string) string {
|
||||
switch strings.ToUpper(category) {
|
||||
case CategoryDragonkind:
|
||||
return "Dragons and dragon-like creatures"
|
||||
case CategoryFay:
|
||||
return "Fae and fairy-type creatures"
|
||||
case CategoryMagical:
|
||||
return "Magical constructs and animated beings"
|
||||
case CategoryMechanimagical:
|
||||
return "Mechanical and magical hybrid creatures"
|
||||
case CategoryNatural:
|
||||
return "Natural animals and beasts"
|
||||
case CategoryPlanar:
|
||||
return "Planar and elemental beings"
|
||||
case CategoryPlant:
|
||||
return "Plant-based creatures"
|
||||
case CategorySentient:
|
||||
return "Sentient humanoid races"
|
||||
case CategoryUndead:
|
||||
return "Undead creatures"
|
||||
case CategoryWere:
|
||||
return "Lycanthropes and shape-shifters"
|
||||
default:
|
||||
return "Unknown category"
|
||||
// IsWere checks if a model ID represents a were creature
|
||||
func (m *Manager) IsWere(modelID int16) bool {
|
||||
raceTypeID := m.GetRaceType(modelID)
|
||||
return raceTypeID >= WereBase && raceTypeID <= WereMax
|
||||
}
|
||||
|
||||
// Bulk operations
|
||||
|
||||
// GetRaceTypesByCategory returns all race types in a category
|
||||
func (m *Manager) GetRaceTypesByCategory(category string) map[int16]*RaceType {
|
||||
return m.masterList.GetRaceTypesByCategory(category)
|
||||
}
|
||||
|
||||
// GetStatistics returns usage statistics
|
||||
func (m *Manager) GetStatistics() *Statistics {
|
||||
return m.masterList.GetStatistics()
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
// GetAllCategories returns a list of all race type categories
|
||||
func (m *Manager) GetAllCategories() []string {
|
||||
return []string{
|
||||
"DRAGONKIND",
|
||||
"FAY",
|
||||
"MAGICAL",
|
||||
"MECHANIMAGICAL",
|
||||
"NATURAL",
|
||||
"PLANAR",
|
||||
"PLANT",
|
||||
"SENTIENT",
|
||||
"UNDEAD",
|
||||
"WERE",
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessCommand handles race type related commands
|
||||
func (m *Manager) ProcessCommand(args []string) string {
|
||||
if len(args) == 0 {
|
||||
return "Usage: racetype <stats|list|info|category> [args...]"
|
||||
// ValidateRaceType validates a race type structure
|
||||
func (m *Manager) ValidateRaceType(raceType *RaceType) error {
|
||||
if raceType == nil {
|
||||
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]) {
|
||||
case "stats":
|
||||
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"
|
||||
if m.logger != nil {
|
||||
m.logger.LogDebug("Cleared all race types")
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the number of race types loaded
|
||||
func (m *Manager) Size() int {
|
||||
return m.masterList.Size()
|
||||
}
|
@ -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()
|
||||
}
|
166
internal/npc/race_types/race_types.go
Normal file
166
internal/npc/race_types/race_types.go
Normal 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)
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
@ -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"`
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user