diff --git a/Py4GWCoreLib/BuildMgr.py b/Py4GWCoreLib/BuildMgr.py index d64dd5d0e..965e7ef8b 100644 --- a/Py4GWCoreLib/BuildMgr.py +++ b/Py4GWCoreLib/BuildMgr.py @@ -419,6 +419,7 @@ def ResolveAllyTarget( TargetLowestAllyMartial, TargetLowestAllyMelee, TargetLowestAllyRanged, + TargetSplinterWeapon, TargetAllyNonEnchanted, TargetAllyNonWeaponSpelled, TargetMinionNonEnchanted, @@ -455,6 +456,8 @@ def ResolveAllyTarget( return TargetAllyNonEnchanted(**range_kwargs) if target_allegiance == Skilltarget.NonWeaponSpelledAlly.value: return Player.GetAgentID() if TargetAllyNonWeaponSpelled(**range_kwargs) else 0 + if target_allegiance == Skilltarget.SplinterWeapon.value: + return TargetSplinterWeapon(distance=ally_distance) if target_allegiance in ( Skilltarget.Ally.value, diff --git a/Py4GWCoreLib/HeroAI/combat.py b/Py4GWCoreLib/HeroAI/combat.py index bc1e40876..a2de241f8 100644 --- a/Py4GWCoreLib/HeroAI/combat.py +++ b/Py4GWCoreLib/HeroAI/combat.py @@ -10,7 +10,7 @@ from Py4GWCoreLib.GlobalCache.HexRemovalPriority import get_hexed_ally_for_removal from Py4GWCoreLib.EnemyBlacklist import EnemyBlacklist from .custom_skill import CustomSkillClass -from .targeting import TargetLowestAlly, TargetLowestAllyEnergy, TargetClusteredEnemy, TargetLowestAllyCaster, TargetLowestAllyMartial, TargetLowestAllyMelee, TargetLowestAllyRanged, GetAllAlliesArray, TargetAllyWeaponSpell, TargetMinionOrAllyNonEnchanted, TargetMinionNonEnchanted, TargetAllyNonEnchanted, TargetAllyNonWeaponSpelled, TargetDeadPartyMember, IsResurrectablePartyMember +from .targeting import TargetLowestAlly, TargetLowestAllyEnergy, TargetClusteredEnemy, TargetLowestAllyCaster, TargetLowestAllyMartial, TargetLowestAllyMelee, TargetLowestAllyRanged, TargetSplinterWeapon, GetAllAlliesArray, TargetAllyWeaponSpell, TargetMinionOrAllyNonEnchanted, TargetMinionNonEnchanted, TargetAllyNonEnchanted, TargetAllyNonWeaponSpelled, TargetDeadPartyMember, IsResurrectablePartyMember from .targeting import GetEnemyAttacking, GetEnemyCasting, GetEnemyCastingSpell, GetEnemyCastingSpellOrChant, GetEnemyInjured, GetEnemyConditioned, GetEnemyHealthy from .targeting import GetEnemyHexed, GetEnemyDegenHexed, GetEnemyEnchanted, GetEnemyMoving, GetEnemyKnockedDown from .targeting import GetEnemyBleeding, GetEnemyPoisoned, GetEnemyCrippled @@ -937,6 +937,8 @@ def get_lowest_ally() -> int: v_target = TargetLowestAllyRanged(filter_skill_id=self.skills[slot].skill_id) if v_target == 0 and not targeting_strict: v_target = get_lowest_ally() + elif target_allegiance == Skilltarget.SplinterWeapon: + v_target = TargetSplinterWeapon() elif target_allegiance == Skilltarget.OtherAlly: if self.skills[slot].custom_skill_data.Nature == SkillNature.EnergyBuff.value: v_target = TargetLowestAllyEnergy(other_ally=True, filter_skill_id=self.skills[slot].skill_id, less_energy=self.skills[slot].custom_skill_data.Conditions.LessEnergy) diff --git a/Py4GWCoreLib/HeroAI/custom_skill_src/ritualist.py b/Py4GWCoreLib/HeroAI/custom_skill_src/ritualist.py index b58be9859..7108d214d 100644 --- a/Py4GWCoreLib/HeroAI/custom_skill_src/ritualist.py +++ b/Py4GWCoreLib/HeroAI/custom_skill_src/ritualist.py @@ -390,7 +390,7 @@ def __init__(self, skill_data): skill = CustomSkill() skill.SkillID = GLOBAL_CACHE.Skill.GetID("Splinter_Weapon") skill.SkillType = SkillType.WeaponSpell.value - skill.TargetAllegiance = Skilltarget.AllyMartial.value + skill.TargetAllegiance = Skilltarget.SplinterWeapon.value skill.Nature = SkillNature.Buff.value skill_data[skill.SkillID] = skill diff --git a/Py4GWCoreLib/HeroAI/targeting.py b/Py4GWCoreLib/HeroAI/targeting.py index 87f1eb0e3..d4de25d78 100644 --- a/Py4GWCoreLib/HeroAI/targeting.py +++ b/Py4GWCoreLib/HeroAI/targeting.py @@ -109,6 +109,71 @@ def TargetLowestAlly(other_ally=False, filter_skill_id=0, distance=Range.Spellca return Utils.GetFirstFromArray(ally_array) +def TargetSplinterWeapon(distance=Range.Spellcast.value): + """Resolve Splinter Weapon's ordered martial/summon target policy. + + Party members deliberately outrank temporary allies, matching the game's + current AI priority. EVAS is the first non-party fallback because its + dagger chain reliably spends Splinter's limited attack charges; minions + are the final fallback. Candidates carrying any weapon spell are skipped + here so selection can continue into the next priority tier. + """ + ebon_vanguard_assassin_model_id = 5903 + player_id = Player.GetAgentID() + player_xy = Player.GetXY() + + def is_eligible(agent_id: int) -> bool: + return bool( + agent_id + and Agent.IsValid(agent_id) + and Agent.IsTargettable(agent_id) + and Agent.IsAlive(agent_id) + and Utils.Distance(Agent.GetXY(agent_id), player_xy) <= distance + and not Routines.Checks.Agents.IsWeaponSpelled(agent_id) + ) + + if is_eligible(player_id) and Routines.Checks.Agents.IsMartial(player_id): + return player_id + + party_candidates = [ + agent_id + for agent_id in GetAllAlliesArray(distance, ordered=True) or [] + if agent_id != player_id + and is_eligible(agent_id) + and Routines.Party.IsPartyMember(agent_id) + and Routines.Checks.Agents.IsMartial(agent_id) + ] + if party_candidates: + return party_candidates[0] + + allied_creatures = set(AgentArray.GetAllyArray() or []) + allied_creatures.update(AgentArray.GetNPCMinipetArray() or []) + evas_candidates = [ + agent_id + for agent_id in allied_creatures + if is_eligible(agent_id) + and Agent.GetModelID(agent_id) == ebon_vanguard_assassin_model_id + ] + if evas_candidates: + return min( + evas_candidates, + key=lambda agent_id: (Utils.Distance(Agent.GetXY(agent_id), player_xy), agent_id), + ) + + minion_candidates = [ + agent_id + for agent_id in AgentArray.GetMinionArray() or [] + if is_eligible(agent_id) + ] + if minion_candidates: + return min( + minion_candidates, + key=lambda agent_id: (Utils.Distance(Agent.GetXY(agent_id), player_xy), agent_id), + ) + + return 0 + + def TargetMinionOrAllyNonEnchanted(filter_skill_id=0, distance=Range.Spellcast.value): minion_array = AgentArray.GetMinionArray() minion_array = AgentArray.Filter.ByDistance(minion_array, Player.GetXY(), distance) diff --git a/Py4GWCoreLib/HeroAI/types.py b/Py4GWCoreLib/HeroAI/types.py index a6b8f6574..4f7a63912 100644 --- a/Py4GWCoreLib/HeroAI/types.py +++ b/Py4GWCoreLib/HeroAI/types.py @@ -143,6 +143,7 @@ class Skilltarget (IntEnum): ResurrectionAlly = 40 HexedAlly = 41 EnemyNotNearby = 42 + SplinterWeapon = 44 diff --git a/Py4GWCoreLib/SkillManager.py b/Py4GWCoreLib/SkillManager.py index 661c9d19f..75f0d9f2b 100644 --- a/Py4GWCoreLib/SkillManager.py +++ b/Py4GWCoreLib/SkillManager.py @@ -1,7 +1,7 @@ from Py4GWCoreLib.HeroAI.custom_skill import CustomSkillClass from Py4GWCoreLib.HeroAI.types import SkillType,SkillNature, Skilltarget -from Py4GWCoreLib.HeroAI.targeting import TargetLowestAlly, TargetLowestAllyEnergy, TargetClusteredEnemy, TargetLowestAllyCaster, TargetLowestAllyMartial, TargetLowestAllyMelee, TargetLowestAllyRanged, GetAllAlliesArray +from Py4GWCoreLib.HeroAI.targeting import TargetLowestAlly, TargetLowestAllyEnergy, TargetClusteredEnemy, TargetLowestAllyCaster, TargetLowestAllyMartial, TargetLowestAllyMelee, TargetLowestAllyRanged, TargetSplinterWeapon, GetAllAlliesArray from Py4GWCoreLib.HeroAI.targeting import TargetMinionOrAllyNonEnchanted, TargetMinionNonEnchanted, TargetAllyNonEnchanted, TargetAllyNonWeaponSpelled from Py4GWCoreLib.HeroAI.targeting import GetEnemyAttacking, GetEnemyCasting, GetEnemyCastingSpell, GetEnemyInjured, GetEnemyConditioned, GetEnemyHealthy from Py4GWCoreLib.HeroAI.targeting import GetEnemyHexed, GetEnemyDegenHexed, GetEnemyEnchanted, GetEnemyMoving, GetEnemyKnockedDown @@ -457,6 +457,8 @@ def _TargetLowestAlly(other_ally=False, filter_skill_id=0): v_target = _TargetLowestAllyRanged(filter_skill_id=skills[slot].skill_id) if v_target == 0 and not targeting_strict: v_target = lowest_ally + elif target_allegiance == Skilltarget.SplinterWeapon: + v_target = TargetSplinterWeapon() elif target_allegiance == Skilltarget.OtherAlly: if skills[slot].custom_skill_data.Nature == SkillNature.EnergyBuff.value: v_target = _TargetLowestAllyEnergy(other_ally=True, filter_skill_id=skills[slot].skill_id) @@ -1563,6 +1565,8 @@ def get_lowest_ally(): v_target = Routines.Targeting.TargetLowestAllyRanged(filter_skill_id=self.skills[slot].skill_id) if v_target == 0 and not targeting_strict: v_target = get_lowest_ally() + elif target_allegiance == Skilltarget.SplinterWeapon: + v_target = TargetSplinterWeapon() elif target_allegiance == Skilltarget.OtherAlly: if self.skills[slot].custom_skill_data.Nature == SkillNature.EnergyBuff.value: v_target = Routines.Targeting.TargetLowestAllyEnergy(other_ally=True, filter_skill_id=self.skills[slot].skill_id)