diff --git a/HeroAI/combat.py b/HeroAI/combat.py index 67a015c25..61d82dff4 100644 --- a/HeroAI/combat.py +++ b/HeroAI/combat.py @@ -53,6 +53,7 @@ def __init__(self): self.fast_casting_level = 0 self.expertise_exists = False self.expertise_level = 0 + self.aggressive_interrupts = False # Aggressive interrupt mode self.nearest_enemy = Routines.Agents.GetNearestEnemy(self.get_combat_distance()) self.lowest_ally = TargetLowestAlly() @@ -129,6 +130,10 @@ def Update(self, cached_data): self.expertise_exists = cached_data.expertise_exists self.expertise_level = cached_data.expertise_level + from HeroAI.settings import Settings + settings = Settings() + self.aggressive_interrupts = settings.AggressiveInterrupts + def PrioritizeSkills(self): """ @@ -975,6 +980,57 @@ def SpiritBuffExists(self, skill_id): return False + def FindReadyInterruptForCastingEnemy(self): + """ + When aggressive interrupts are enabled, find a ready interrupt skill and a casting enemy. + Returns (slot, target_id) if found, otherwise (None, None) + """ + if not self.aggressive_interrupts: + return None, None + + # First, find any enemy that is casting with a significant activation time + casting_enemy = None + enemy_array = GLOBAL_CACHE.AgentArray.GetEnemyArray() + distance = self.get_combat_distance() + enemy_array = AgentArray.Filter.ByDistance(enemy_array, GLOBAL_CACHE.Player.GetXY(), distance) + enemy_array = AgentArray.Filter.ByCondition(enemy_array, lambda agent_id: GLOBAL_CACHE.Agent.IsAlive(agent_id)) + + for enemy_id in enemy_array: + if GLOBAL_CACHE.Agent.IsCasting(enemy_id): + casting_skill_id = GLOBAL_CACHE.Agent.GetCastingSkill(enemy_id) + # Only interrupt skills with activation time >= 0.25 seconds + if casting_skill_id > 0 and GLOBAL_CACHE.Skill.Data.GetActivation(casting_skill_id) >= 0.25: + casting_enemy = enemy_id + break + + if not casting_enemy: + return None, None + + # Now find an interrupt skill that's ready to cast + for slot in range(MAX_SKILLS): + skill = self.skills[slot] + + # Check if this is an interrupt skill + if skill.custom_skill_data.Nature != SkillNature.Interrupt.value: + continue + + # Check if the skill is ready (recharge, energy, adrenaline, etc.) + if not self.IsSkillReady(slot): + continue + + # Verify this interrupt skill can target enemies + target_allegiance = skill.custom_skill_data.TargetAllegiance + if target_allegiance not in [Skilltarget.Enemy.value, Skilltarget.EnemyCasting.value, Skilltarget.EnemyCastingSpell.value]: + continue + + # Check if we can cast it on the casting enemy (validates conditions, range, etc.) + if GLOBAL_CACHE.Agent.IsLiving(casting_enemy): + is_ready, _ = self.IsReadyToCast(slot) + if is_ready: + return slot, casting_enemy + + return None, None + def IsReadyToCast(self, slot): @@ -1195,6 +1251,36 @@ def HandleCombat(self,ooc=False): # If we couldn't cast it, clear the pending state after a timeout if self.followup_skill_timer.GetElapsedTime() > 3000: # 3 second timeout self.pending_followup_skill_slot = -1 + + # Aggressive interrupt mode: immediately interrupt any casting enemy + if self.aggressive_interrupts and not ooc and self.is_combat_enabled: + interrupt_slot, casting_enemy = self.FindReadyInterruptForCastingEnemy() + if interrupt_slot is not None and casting_enemy is not None: + # Found an interrupt skill and a casting enemy - use it immediately + self.in_casting_routine = True + skill_id = self.skills[interrupt_slot].skill_id + + if self.fast_casting_exists: + activation, recharge = Routines.Checks.Skills.apply_fast_casting(skill_id, self.fast_casting_level) + else: + activation = GLOBAL_CACHE.Skill.Data.GetActivation(skill_id) + + self.aftercast = activation * 1000 + self.aftercast += GLOBAL_CACHE.Skill.Data.GetAftercast(skill_id) * 1000 + + skill_type, _ = GLOBAL_CACHE.Skill.GetType(skill_id) + if skill_type == SkillType.Attack.value: + self.aftercast += self.GetWeaponAttackAftercast() + + self.aftercast += self.ping_handler.GetCurrentPing() + self.aftercast_timer.Reset() + + # Use the interrupt skill on the casting enemy + GLOBAL_CACHE.SkillBar.UseSkill(self.skill_order[interrupt_slot]+1, casting_enemy) + + # Reset skill pointer to continue normal rotation + self.ResetSkillPointer() + return True slot = self.skill_pointer skill_id = self.skills[slot].skill_id diff --git a/HeroAI/settings.py b/HeroAI/settings.py index d79e0d957..73d23cb24 100644 --- a/HeroAI/settings.py +++ b/HeroAI/settings.py @@ -113,6 +113,9 @@ def __init__(self): self.ArcaneMimicryTargetAgentID = 0 # Which ally to target with Arcane Mimicry self.ArcaneMimicryEliteSkillID = 0 # Which elite skill to copy with Arcane Mimicry + # Combat behavior settings + self.AggressiveInterrupts = False # When enabled, immediately interrupt any casting enemy + base_path = Console.get_projects_path() self.ini_path = os.path.join(base_path, "Widgets", "Config", "HeroAI.ini") @@ -197,6 +200,9 @@ def write_settings(self): self.account_ini_handler.write_key("MesmerSkills", "ArcaneMimicrySkillSlot", str(self.ArcaneMimicrySkillSlot)) self.account_ini_handler.write_key("MesmerSkills", "ArcaneMimicryTargetAgentID", str(self.ArcaneMimicryTargetAgentID)) self.account_ini_handler.write_key("MesmerSkills", "ArcaneMimicryEliteSkillID", str(self.ArcaneMimicryEliteSkillID)) + + # Combat behavior settings + self.account_ini_handler.write_key("Combat", "AggressiveInterrupts", str(self.AggressiveInterrupts)) for hero_email, (x, y, w, h, collapsed) in self.HeroPanelPositions.items(): self.account_ini_handler.write_key("HeroPanelPositions", hero_email, f"{x},{y},{w},{h},{collapsed}") @@ -235,6 +241,9 @@ def load_settings(self): self.ArcaneMimicrySkillSlot = self.account_ini_handler.read_int("MesmerSkills", "ArcaneMimicrySkillSlot", 0) self.ArcaneMimicryTargetAgentID = self.account_ini_handler.read_int("MesmerSkills", "ArcaneMimicryTargetAgentID", 0) self.ArcaneMimicryEliteSkillID = self.account_ini_handler.read_int("MesmerSkills", "ArcaneMimicryEliteSkillID", 0) + + # Combat behavior settings + self.AggressiveInterrupts = self.account_ini_handler.read_bool("Combat", "AggressiveInterrupts", False) self.HeroPanelPositions.clear() self.import_hero_panel_positions(self.account_ini_handler) diff --git a/HeroAI/windows.py b/HeroAI/windows.py index cd935a7c2..e12711336 100644 --- a/HeroAI/windows.py +++ b/HeroAI/windows.py @@ -1214,6 +1214,42 @@ def DrawCustomSkillsWindow(cached_data: CacheData): PyImGui.text_colored("No allies found in party", Utils.RGBToNormal(255, 165, 0, 255)) else: PyImGui.text_colored("Arcane Mimicry not found in skillbar", Utils.RGBToNormal(255, 0, 0, 255)) + + PyImGui.spacing() + PyImGui.separator() + PyImGui.spacing() + + # Aggressive Interrupts configuration + if PyImGui.collapsing_header("Aggressive Interrupts", PyImGui.TreeNodeFlags.DefaultOpen): + PyImGui.text_colored( + "When enabled, HeroAI will become very aggressive with interrupts.", + Utils.RGBToNormal(180, 180, 180, 255) + ) + PyImGui.text_colored( + "The bot will find any enemy in range that is casting and immediately", + Utils.RGBToNormal(180, 180, 180, 255) + ) + PyImGui.text_colored( + "interrupt it, canceling other actions if necessary.", + Utils.RGBToNormal(180, 180, 180, 255) + ) + PyImGui.spacing() + + aggressive_interrupts = ImGui.checkbox("Enable Aggressive Interrupts", settings.AggressiveInterrupts) + if aggressive_interrupts != settings.AggressiveInterrupts: + settings.AggressiveInterrupts = aggressive_interrupts + settings.save_settings() + + if settings.AggressiveInterrupts: + PyImGui.text_colored( + "Aggressive Interrupts: ENABLED - Bot will immediately interrupt casting enemies", + Utils.RGBToNormal(0, 255, 0, 255) + ) + else: + PyImGui.text_colored( + "Aggressive Interrupts: DISABLED - Bot will use normal interrupt priority", + Utils.RGBToNormal(180, 180, 180, 255) + )