eq2go/internal/npc/ai/variants.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 ...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)
}
}