Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions HeroAI/combat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions HeroAI/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions HeroAI/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)