import type { AbilityDefinition, ResourceType } from "./abilityCatalog"; import type { ActiveAura } from "./combatAuras"; export const ROM_SCOUT_BLOOD_ARROW_ABILITY_ID = "rom-scout-general-4"; export const ROM_SCOUT_BLOOD_ARROW_SOURCE_SKILL_ID = 490434; export const ROM_SCOUT_BLOOD_ARROW_AURA_TAG = "rom-self-health-drain"; export const ROM_SCOUT_BLOOD_ARROW_DEACTIVATION_COOLDOWN_MS = 10_000; export interface RomBloodArrowOwnerBuild { readonly classId: string; readonly secondaryClassId?: string | null; } export function isRomScoutBloodArrowAbility(ability: AbilityDefinition): boolean { return ability.source === "runewaker" && ability.id === ROM_SCOUT_BLOOD_ARROW_ABILITY_ID && ability.classId === "rom-scout" && ability.secondaryClassId === undefined && ability.dbcSpellId === ROM_SCOUT_BLOOD_ARROW_SOURCE_SKILL_ID; } export function isRomKnightScoutBloodArrowOwner(owner: RomBloodArrowOwnerBuild): boolean { return owner.classId === "rom-knight" && owner.secondaryClassId === "rom-scout"; } export function isRomScoutRogueBloodArrowOwner(owner: RomBloodArrowOwnerBuild): boolean { return owner.classId === "rom-scout" && owner.secondaryClassId === "rom-rogue"; } export function isRomBloodArrowOwner(owner: RomBloodArrowOwnerBuild): boolean { return isRomKnightScoutBloodArrowOwner(owner) || isRomScoutRogueBloodArrowOwner(owner); } function bloodArrowDescriptionForOwner(owner: RomBloodArrowOwnerBuild): string { if (isRomKnightScoutBloodArrowOwner(owner)) { return "Activates Blood Arrow and increases ranged weapon damage. Each offensive ranged weapon skill costs 2.0% HP and 1.0% MP. Right-click the buff icon or use the skill again to deactivate; the skill enters a 10-second cooldown after deactivation."; } if (isRomScoutRogueBloodArrowOwner(owner)) { return "Activates Blood Arrow and increases physical ranged weapon damage. Ranged attacks cost 2.0% HP each. Right-click the buff icon or use the skill again to deactivate; the skill enters a 10-second cooldown after deactivation."; } return "Activates Blood Arrow and increases physical ranged weapon damage. Right-click the buff icon or use the skill again to deactivate; the skill enters a 10-second cooldown after deactivation."; } function indefiniteBloodArrowEffects(effects: AbilityDefinition["effects"]): AbilityDefinition["effects"] { const auraEffect = effects.find((effect) => effect.kind === "apply-aura" && effect.recipient === "caster"); if (auraEffect?.kind !== "apply-aura") return effects; return [{ ...auraEffect, aura: { ...auraEffect.aura, durationMs: null, utilityTags: auraEffect.aura.utilityTags?.includes(ROM_SCOUT_BLOOD_ARROW_AURA_TAG) ? auraEffect.aura.utilityTags : [...(auraEffect.aura.utilityTags ?? []), ROM_SCOUT_BLOOD_ARROW_AURA_TAG], }, }]; } export function romBloodArrowAbilityForOwner( ability: AbilityDefinition, owner: RomBloodArrowOwnerBuild, ): AbilityDefinition { if (!isRomScoutBloodArrowAbility(ability) || !isRomBloodArrowOwner(owner)) return ability; const description = bloodArrowDescriptionForOwner(owner); return { ...ability, description, cooldownMs: 0, effects: indefiniteBloodArrowEffects(ability.effects), ranks: ability.ranks?.map((rank) => ({ ...rank, description, cooldownMs: 0, effects: indefiniteBloodArrowEffects(rank.effects), })), }; } export function isRomKnightScoutOffensiveRangedWeaponSkill( ability: AbilityDefinition, owner: RomBloodArrowOwnerBuild, ): boolean { if (!isRomKnightScoutBloodArrowOwner(owner) || ability.source !== "runewaker" || ability.passive || ability.dbcSpellId === ROM_SCOUT_BLOOD_ARROW_SOURCE_SKILL_ID || ability.dbcSpellId === 492589 // Autoshot is an ongoing mode, not a cast trigger. || ability.sourceAttackKind === "melee" || !ability.effects.some((effect) => effect.kind === "damage" || effect.kind === "dot")) return false; const scoutSkill = ability.classId === "rom-scout" && ability.secondaryClassId === undefined && ability.romSkillGroup === "general"; const knightScoutElite = ability.classId === "rom-knight" && ability.secondaryClassId === "rom-scout"; return (scoutSkill || knightScoutElite) && (/ranged weapon DPS/i.test(ability.description) || ability.dbcSpellId === 490420); } export function isRomScoutRogueRangedAttackSkill( ability: AbilityDefinition, owner: RomBloodArrowOwnerBuild, ): boolean { if (!isRomScoutRogueBloodArrowOwner(owner) || ability.source !== "runewaker" || ability.passive || ability.dbcSpellId === ROM_SCOUT_BLOOD_ARROW_SOURCE_SKILL_ID || ability.dbcSpellId === 492589 // Autoshot is an ongoing mode, not a cast trigger. || ability.sourceAttackKind === "melee" || !ability.effects.some((effect) => effect.kind === "damage" || effect.kind === "dot")) return false; const scoutGeneralSkill = ability.classId === "rom-scout" && ability.secondaryClassId === undefined && (ability.romSkillGroup === "general" || ability.romSkillGroup === "primary"); return scoutGeneralSkill && (/ranged weapon DPS/i.test(ability.description) || ability.dbcSpellId === 490420); } export function romBloodArrowCastCosts( ability: AbilityDefinition, owner: RomBloodArrowOwnerBuild, auras: readonly ActiveAura[], ownerId: string, maxHealth: number, maxMana: number, now?: number, ): readonly { readonly resource: ResourceType; readonly amount: number }[] { if (!isRomBloodArrowOwner(owner) || !auras.some((aura) => aura.sourceId === ownerId && aura.targetId === ownerId && (now === undefined || aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes(ROM_SCOUT_BLOOD_ARROW_AURA_TAG))) return []; if (isRomKnightScoutBloodArrowOwner(owner) && isRomKnightScoutOffensiveRangedWeaponSkill(ability, owner)) { return [ { resource: "health", amount: Math.max(0, Math.round(maxHealth * 0.02)) }, { resource: "mana", amount: Math.max(0, Math.round(maxMana * 0.01)) }, ]; } if (isRomScoutRogueBloodArrowOwner(owner) && isRomScoutRogueRangedAttackSkill(ability, owner)) { return [{ resource: "health", amount: Math.max(0, Math.round(maxHealth * 0.02)) }]; } return []; } export function romScoutRogueBloodArrowRangedAutoAttackCost( owner: RomBloodArrowOwnerBuild, auras: readonly ActiveAura[], ownerId: string, maxHealth: number, now: number, ): number { return isRomScoutRogueBloodArrowOwner(owner) && hasOwnedRomScoutBloodArrow(auras, ownerId, now) ? Math.max(0, Math.round(maxHealth * 0.02)) : 0; } export function hasOwnedRomScoutBloodArrow( auras: readonly ActiveAura[], ownerId: string, now: number, ): boolean { return auras.some((aura) => aura.sourceId === ownerId && aura.targetId === ownerId && (aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes(ROM_SCOUT_BLOOD_ARROW_AURA_TAG)); } export function isOwnedRomScoutBloodArrowAura(aura: ActiveAura, ownerId: string): boolean { return aura.sourceId === ownerId && aura.targetId === ownerId && aura.definition.utilityTags?.includes(ROM_SCOUT_BLOOD_ARROW_AURA_TAG) === true; } export function deactivateRomBloodArrowFromAuraContextMenu( aura: ActiveAura, ownerId: string, deactivate: (ownerId: string, now?: number) => boolean, now?: number, ): boolean { return isOwnedRomScoutBloodArrowAura(aura, ownerId) && deactivate(ownerId, now); }