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) } }