diff --git a/Py4GWCoreLib/BuildMgr.py b/Py4GWCoreLib/BuildMgr.py index ada30ede1..0ab5fcc17 100644 --- a/Py4GWCoreLib/BuildMgr.py +++ b/Py4GWCoreLib/BuildMgr.py @@ -1250,13 +1250,14 @@ def CastSkillID( GLOBAL_CACHE.SkillBar.UseSkill(slot, target_agent_id=target_agent_id, aftercast_delay=aftercast_delay) self._mark_local_cast_pending(aftercast_delay) - if self._is_spirit_skill(skill_id): - yield from Routines.Yield.wait(self._get_spirit_cast_wait_ms(skill_id, aftercast_delay)) - yield from self._wait_for_spirit_spawn_and_step_away(skill_id) if log: ConsoleLog("CastSkillID", f"Cast {Skill.GetName(skill_id)}, slot: {slot}", Console.MessageType.Info, log=log) self.SetTickSuccess() + if self._is_spirit_skill(skill_id): + yield from Routines.Yield.wait(self._get_spirit_cast_wait_ms(skill_id, aftercast_delay)) + yield from self._wait_for_spirit_spawn_and_step_away(skill_id) + return True def CastSkillIDAndRestoreTarget( @@ -1308,7 +1309,9 @@ def CastSpiritSkillID( aftercast_delay=aftercast_delay, )) - yield from self._move_for_spirit_cast() + if not self.CanCastSkillID(skill_id, extra_condition=extra_condition): + return False + return (yield from self.CastSkillID( skill_id=skill_id, extra_condition=extra_condition, @@ -1338,13 +1341,14 @@ def CastSkillSlot( GLOBAL_CACHE.SkillBar.UseSkill(slot, target_agent_id=target_agent_id, aftercast_delay=aftercast_delay) self._mark_local_cast_pending(aftercast_delay) - if self._is_spirit_skill(skill_id): - yield from Routines.Yield.wait(self._get_spirit_cast_wait_ms(skill_id, aftercast_delay)) - yield from self._wait_for_spirit_spawn_and_step_away(skill_id) if log: ConsoleLog("CastSkillSlot", f"Cast {GLOBAL_CACHE.Skill.GetName(skill_id)}, slot: {slot}", Console.MessageType.Info, log=log) self.SetTickSuccess() + if self._is_spirit_skill(skill_id): + yield from Routines.Yield.wait(self._get_spirit_cast_wait_ms(skill_id, aftercast_delay)) + yield from self._wait_for_spirit_spawn_and_step_away(skill_id) + return True diff --git a/Py4GWCoreLib/Builds/Ritualist/Rt_Any/SOS.py b/Py4GWCoreLib/Builds/Ritualist/Rt_Any/SOS.py new file mode 100644 index 000000000..6db2623cb --- /dev/null +++ b/Py4GWCoreLib/Builds/Ritualist/Rt_Any/SOS.py @@ -0,0 +1,79 @@ +from Py4GWCoreLib import Profession, Routines +from Py4GWCoreLib.Builds.Any.HeroAI import HeroAI_Build +from Py4GWCoreLib import BuildMgr +from Py4GWCoreLib.Skill import Skill +from Py4GWCoreLib.Builds.Skills import SkillsTemplate + + +Signet_of_Spirits_ID = Skill.GetID("Signet_of_Spirits") +Bloodsong_ID = Skill.GetID("Bloodsong") +Vampirism_ID = Skill.GetID("Vampirism") +Gaze_of_Fury_ID = Skill.GetID("Gaze_of_Fury") +Painful_Bond_ID = Skill.GetID("Painful_Bond") +Armor_of_Unfeeling_ID = Skill.GetID("Armor_of_Unfeeling") +Summon_Spirits_Kurzick_ID = Skill.GetID("Summon_Spirits_kurzick") +Summon_Spirits_Luxon_ID = Skill.GetID("Summon_Spirits_luxon") + + +class SOS(BuildMgr): + def __init__(self, match_only: bool = False): + super().__init__( + name="Signet of Spirits", + required_primary=Profession.Ritualist, + template_code="OACiIykMdNVO5DOACAAAAAAAAA", # placeholder — needs in-game verification + required_skills=[ + Signet_of_Spirits_ID, + Bloodsong_ID, + Vampirism_ID, + ], + optional_skills=[ + Gaze_of_Fury_ID, + Painful_Bond_ID, + Armor_of_Unfeeling_ID, + Summon_Spirits_Kurzick_ID, + Summon_Spirits_Luxon_ID, + ], + ) + if match_only: + return + + self.SetFallback("HeroAI", HeroAI_Build(standalone_fallback=True)) + self.SetSkillCastingFn(self._run_local_skill_logic) + self.skills: SkillsTemplate = SkillsTemplate(self) + + def _run_local_skill_logic(self): + if not Routines.Checks.Skills.CanCast(): + return False + + # Summon spirits to regroup (highest priority) + if self.IsSkillEquipped(Summon_Spirits_Kurzick_ID) and (yield from self.skills.Ritualist.ChannelingMagic.Summon_Spirits()): + return True + if self.IsSkillEquipped(Summon_Spirits_Luxon_ID) and (yield from self.skills.Ritualist.ChannelingMagic.Summon_Spirits()): + return True + + # Core spirits + if (yield from self.skills.Ritualist.ChannelingMagic.Signet_of_Spirits()): + return True + if (yield from self.skills.Ritualist.ChannelingMagic.Vampirism()): + return True + if (yield from self.skills.Ritualist.ChannelingMagic.Bloodsong()): + return True + + if not Routines.Checks.Agents.InAggro(): + return False + + # Combat optional + if (yield from self.skills.Ritualist.ChannelingMagic.Gaze_of_Fury()): + return True + if (yield from self.skills.Ritualist.ChannelingMagic.Painful_Bond()): + return True + + # Defensive + if (yield from self.skills.Ritualist.Communing.Armor_of_Unfeeling()): + return True + + # Common PvE + if (yield from self.skills.Any.PvE.Ebon_Vanguard_Assassin_Support()): + return True + + return False diff --git a/Py4GWCoreLib/Builds/Ritualist/Rt_Any/Soul_Twisting.py b/Py4GWCoreLib/Builds/Ritualist/Rt_Any/Soul_Twisting.py new file mode 100644 index 000000000..67c341e8f --- /dev/null +++ b/Py4GWCoreLib/Builds/Ritualist/Rt_Any/Soul_Twisting.py @@ -0,0 +1,96 @@ +from Py4GWCoreLib import Profession, Routines +from Py4GWCoreLib.Builds.Any.HeroAI import HeroAI_Build +from Py4GWCoreLib import BuildMgr +from Py4GWCoreLib.Skill import Skill +from Py4GWCoreLib.Builds.Skills import SkillsTemplate + + +Soul_Twisting_ID = Skill.GetID("Soul_Twisting") +Boon_of_Creation_ID = Skill.GetID("Boon_of_Creation") +Shelter_ID = Skill.GetID("Shelter") +Union_ID = Skill.GetID("Union") +Displacement_ID = Skill.GetID("Displacement") +Armor_of_Unfeeling_ID = Skill.GetID("Armor_of_Unfeeling") +Spirits_Gift_ID = Skill.GetID("Spirits_Gift") +Summon_Spirits_Kurzick_ID = Skill.GetID("Summon_Spirits_kurzick") +Summon_Spirits_Luxon_ID = Skill.GetID("Summon_Spirits_luxon") + + +class Soul_Twisting(BuildMgr): + def __init__(self, match_only: bool = False): + super().__init__( + name="Soul Twisting", + required_primary=Profession.Ritualist, + template_code="OACiAyk8gNtePOAAAAAAAAAA", + required_skills=[ + Soul_Twisting_ID, + Shelter_ID, + Union_ID, + ], + optional_skills=[ + Displacement_ID, + Boon_of_Creation_ID, + Armor_of_Unfeeling_ID, + Spirits_Gift_ID, + Summon_Spirits_Kurzick_ID, + Summon_Spirits_Luxon_ID, + ], + ) + if match_only: + return + + self.SetFallback("HeroAI", HeroAI_Build(standalone_fallback=True)) + self.SetOOCFn(self._run_ooc) + self.SetCombatFn(self._run_combat) + self.skills: SkillsTemplate = SkillsTemplate(self) + + def _run_ooc(self): + """Out of combat: maintain self-buffs only.""" + if not Routines.Checks.Skills.CanCast(): + return False + + if self.IsSkillEquipped(Soul_Twisting_ID) and (yield from self.skills.Ritualist.SpawningPower.Soul_Twisting()): + return True + if self.IsSkillEquipped(Boon_of_Creation_ID) and (yield from self.skills.Ritualist.SpawningPower.Boon_of_Creation()): + return True + if self.IsSkillEquipped(Spirits_Gift_ID) and (yield from self.skills.Ritualist.SpawningPower.Spirits_Gift()): + return True + + return False + + def _run_combat(self): + """In combat: full rotation — buffs, spirits, PvE skills.""" + if not Routines.Checks.Skills.CanCast(): + return False + + # Maintain self buffs (highest priority) + if (yield from self.skills.Ritualist.SpawningPower.Soul_Twisting()): + return True + if (yield from self.skills.Ritualist.SpawningPower.Boon_of_Creation()): + return True + if (yield from self.skills.Ritualist.SpawningPower.Spirits_Gift()): + return True + + # Summon spirits to regroup + if self.IsSkillEquipped(Summon_Spirits_Kurzick_ID) and (yield from self.skills.Ritualist.ChannelingMagic.Summon_Spirits()): + return True + if self.IsSkillEquipped(Summon_Spirits_Luxon_ID) and (yield from self.skills.Ritualist.ChannelingMagic.Summon_Spirits()): + return True + + # Protective spirits (Soul Twisting must be active — gated inside Communing) + if (yield from self.skills.Ritualist.Communing.Shelter()): + return True + if (yield from self.skills.Ritualist.Communing.Union()): + return True + if (yield from self.skills.Ritualist.Communing.Displacement()): + return True + + # Armor spirits + if (yield from self.skills.Ritualist.Communing.Armor_of_Unfeeling()): + return True + + # Common PvE + if (yield from self.skills.Any.PvE.Ebon_Vanguard_Assassin_Support()): + return True + + return False diff --git a/Py4GWCoreLib/Builds/Ritualist/Rt_Any/__init__.py b/Py4GWCoreLib/Builds/Ritualist/Rt_Any/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Py4GWCoreLib/Builds/Ritualist/__init__.py b/Py4GWCoreLib/Builds/Ritualist/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Py4GWCoreLib/Builds/Skills/ritualist/ChannelingMagic.py b/Py4GWCoreLib/Builds/Skills/ritualist/ChannelingMagic.py index 53d0378cb..22204e224 100644 --- a/Py4GWCoreLib/Builds/Skills/ritualist/ChannelingMagic.py +++ b/Py4GWCoreLib/Builds/Skills/ritualist/ChannelingMagic.py @@ -1,11 +1,142 @@ -from __future__ import annotations +from __future__ import annotations from typing import TYPE_CHECKING +from Py4GWCoreLib.BuildMgr import BuildCoroutine +from Py4GWCoreLib import AgentArray, Range, Routines +from Py4GWCoreLib.Agent import Agent +from Py4GWCoreLib.Player import Player +from Py4GWCoreLib.Skill import Skill + if TYPE_CHECKING: from Py4GWCoreLib.BuildMgr import BuildMgr +__all__ = ["ChannelingMagic"] + + class ChannelingMagic: def __init__(self, build: BuildMgr) -> None: self.build: BuildMgr = build + #region S + def Signet_of_Spirits(self) -> BuildCoroutine: + signet_of_spirits_id: int = Skill.GetID("Signet_of_Spirits") + + if not self.build.IsSkillEquipped(signet_of_spirits_id): + return False + + return (yield from self.build.CastSpiritSkillID( + skill_id=signet_of_spirits_id, + log=False, + aftercast_delay=250, + )) + + def Summon_Spirits(self) -> BuildCoroutine: + """Cast Summon Spirits (kurzick or luxon variant). Relocates owned spirits to player position.""" + summon_k_id: int = Skill.GetID("Summon_Spirits_kurzick") + summon_l_id: int = Skill.GetID("Summon_Spirits_luxon") + + skill_id = summon_k_id if self.build.IsSkillEquipped(summon_k_id) else summon_l_id + if not self.build.IsSkillEquipped(skill_id): + return False + + spirits = AgentArray.GetSpiritPetArray() + player_pos = Player.GetXY() + far_spirits = AgentArray.Filter.ByCondition( + spirits, + lambda s: Agent.IsAlive(s) and Agent.IsSpawned(s), + ) + far_spirits = [s for s in far_spirits if not AgentArray.Filter.ByDistance([s], player_pos, Range.Earshot.value)] + if not far_spirits: + return False + + return (yield from self.build.CastSkillID( + skill_id=skill_id, + log=False, + aftercast_delay=250, + )) + #endregion + + #region B + def Bloodsong(self) -> BuildCoroutine: + bloodsong_id: int = Skill.GetID("Bloodsong") + + if not self.build.IsSkillEquipped(bloodsong_id): + return False + + return (yield from self.build.CastSpiritSkillID( + skill_id=bloodsong_id, + log=False, + aftercast_delay=250, + )) + #endregion + + #region V + def Vampirism(self) -> BuildCoroutine: + vampirism_id: int = Skill.GetID("Vampirism") + + if not self.build.IsSkillEquipped(vampirism_id): + return False + + return (yield from self.build.CastSpiritSkillID( + skill_id=vampirism_id, + log=False, + aftercast_delay=250, + )) + #endregion + + #region G + def Gaze_of_Fury(self) -> BuildCoroutine: + """Destroy target enemy spirit and create a spirit of Gaze of Fury.""" + gaze_of_fury_id: int = Skill.GetID("Gaze_of_Fury") + + if not self.build.IsSkillEquipped(gaze_of_fury_id): + return False + + spirits = AgentArray.GetSpiritPetArray() + spirits = AgentArray.Filter.ByDistance(spirits, Player.GetXY(), Range.Spellcast.value) + enemy_spirits = AgentArray.Filter.ByCondition( + spirits, + lambda s: Agent.IsAlive(s) and not Agent.GetIsAlly(s), + ) + if not enemy_spirits: + return (yield from self.build.CastSpiritSkillID( + skill_id=gaze_of_fury_id, + log=False, + aftercast_delay=250, + )) + + target_id = enemy_spirits[0] + return (yield from self.build.CastSkillID( + skill_id=gaze_of_fury_id, + target_agent_id=target_id, + log=False, + aftercast_delay=250, + )) + #endregion + + #region P + def Painful_Bond(self) -> BuildCoroutine: + """Hex target foe. Only effective if spirits are nearby.""" + painful_bond_id: int = Skill.GetID("Painful_Bond") + + if not self.build.IsSkillEquipped(painful_bond_id): + return False + + spirits = AgentArray.GetSpiritPetArray() + spirits = AgentArray.Filter.ByDistance(spirits, Player.GetXY(), Range.Earshot.value) + spirits = AgentArray.Filter.ByCondition(spirits, lambda s: Agent.IsAlive(s)) + if len(spirits) < 2: + return False + + enemies = Routines.Agents.GetFilteredEnemyArray(*Player.GetXY(), Range.Spellcast.value) + if not enemies: + return False + + return (yield from self.build.CastSkillID( + skill_id=painful_bond_id, + target_agent_id=enemies[0], + log=False, + aftercast_delay=250, + )) + #endregion diff --git a/Py4GWCoreLib/Builds/Skills/ritualist/Communing.py b/Py4GWCoreLib/Builds/Skills/ritualist/Communing.py index 8dd3ef5f5..7780f1307 100644 --- a/Py4GWCoreLib/Builds/Skills/ritualist/Communing.py +++ b/Py4GWCoreLib/Builds/Skills/ritualist/Communing.py @@ -81,6 +81,8 @@ def _resolve_armor_of_unfeeling_target(self) -> int: def _cast_protective_spirit(self, skill_id: int) -> BuildCoroutine: if not self.build.IsSkillEquipped(skill_id): return False + if self.build.SpiritBuffExists(skill_id): + return False if not self._is_soul_twisting_ready(): return False