Skip to content
Open
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
18 changes: 11 additions & 7 deletions Py4GWCoreLib/BuildMgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down
79 changes: 79 additions & 0 deletions Py4GWCoreLib/Builds/Ritualist/Rt_Any/SOS.py
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions Py4GWCoreLib/Builds/Ritualist/Rt_Any/Soul_Twisting.py
Original file line number Diff line number Diff line change
@@ -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
Empty file.
Empty file.
133 changes: 132 additions & 1 deletion Py4GWCoreLib/Builds/Skills/ritualist/ChannelingMagic.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions Py4GWCoreLib/Builds/Skills/ritualist/Communing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down