325 lines
7.9 KiB
Go
325 lines
7.9 KiB
Go
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 ...interface{}) 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)
|
|
}
|
|
}
|