import { applyAura, type ActiveAura, type AuraDefinition, type CombatEntityId } from "./combatAuras"; /** Current CoA Druid/Mage Inspiration stack behavior. */ export const ROM_DRUID_MAGE_INSPIRATION_PASSIVE_ID = 494322; export const ROM_DRUID_MAGE_INSPIRATION_MAX_STACKS = 10; export const ROM_DRUID_MAGE_INSPIRATION_BONUS_PER_STACK_PERCENT = 2; export const ROM_DRUID_MAGE_INSPIRATION_DURATION_MS = 10_000; /** Current CoA skill page's `Range: 50`, converted with the project's 10:1 CoA range convention. */ export const ROM_DRUID_MAGE_INSPIRATION_RADIUS = 5; export const ROM_DRUID_MAGE_INSPIRATION_AURA_TAG = "rom-druid-mage-inspiration"; export const ROM_DRUID_MAGE_INSPIRATION_DESCRIPTION = "Passive: whenever you spend Nature's Power, nearby party members gain one Inspiration stack, increasing Physical Attack Speed and Casting Speed by 2% per stack, up to 10 stacks for 10 seconds. CoA also describes a Fireball travel-speed and Nature's Power rider, but current CoA Fireball 499943 cannot be proven identical to local Fireball 490204, so those clauses are not implemented."; export type RomDruidMageInspirationPosition = readonly [number, number, number]; export interface RomDruidMageInspirationRecipient { readonly id: CombatEntityId; readonly position: RomDruidMageInspirationPosition | null; readonly living: boolean; } /** Select modeled living party actors inside the interpreted caster-centered CoA Range 50. */ export function romDruidMageInspirationRecipientIds( sourcePosition: RomDruidMageInspirationPosition, candidates: readonly RomDruidMageInspirationRecipient[], ): readonly CombatEntityId[] { const radiusSquared = ROM_DRUID_MAGE_INSPIRATION_RADIUS ** 2; return [...new Set(candidates.filter((candidate) => { if (!candidate.living || !candidate.position) return false; const dx = candidate.position[0] - sourcePosition[0]; const dy = candidate.position[1] - sourcePosition[1]; const dz = candidate.position[2] - sourcePosition[2]; return dx * dx + dy * dy + dz * dz <= radiusSquared; }).map((candidate) => candidate.id))]; } export function romDruidMageInspirationAuraDefinition(): AuraDefinition { return { id: "rom:druid-mage:inspiration", name: "Inspiration", description: "Increases physical attack speed and casting speed by 2% per stack.", disposition: "buff", durationMs: ROM_DRUID_MAGE_INSPIRATION_DURATION_MS, maxStacks: ROM_DRUID_MAGE_INSPIRATION_MAX_STACKS, stackBehavior: "add", uniqueBySource: false, utilityTags: [ROM_DRUID_MAGE_INSPIRATION_AURA_TAG], modifiers: [ { kind: "stat", stat: "melee-haste", operation: "flat", value: ROM_DRUID_MAGE_INSPIRATION_BONUS_PER_STACK_PERCENT, perStack: true, }, { kind: "stat", stat: "ranged-haste", operation: "flat", value: ROM_DRUID_MAGE_INSPIRATION_BONUS_PER_STACK_PERCENT, perStack: true, }, { kind: "stat", stat: "spell-haste", operation: "flat", value: ROM_DRUID_MAGE_INSPIRATION_BONUS_PER_STACK_PERCENT, perStack: true, }, ], }; } /** Apply one stack to each recipient selected by the caller; the shared aura refreshes for 10 seconds. */ export function romDruidMageInspirationApplySpendStack( current: readonly ActiveAura[], sourceId: CombatEntityId, recipientIds: readonly CombatEntityId[], now: number, ): readonly ActiveAura[] { let auras = current; const definition = romDruidMageInspirationAuraDefinition(); for (const targetId of new Set(recipientIds)) { auras = applyAura(auras, { definition, sourceId, targetId, now, stacks: 1 }).auras; } return auras; } export function romDruidMageInspirationHastePercent( auras: readonly ActiveAura[], targetId: string, now: number, ): number { return auras .filter((aura) => aura.targetId === targetId && aura.definition.utilityTags?.includes(ROM_DRUID_MAGE_INSPIRATION_AURA_TAG) && (aura.expiresAt === null || aura.expiresAt > now)) .reduce((total, aura) => total + romDruidMageInspirationBonusPercent(aura.stacks), 0); } export function isRomDruidMagePair(row: { readonly classId?: string; readonly secondaryClassId?: string | null; }): boolean { return row.classId === "rom-druid" && row.secondaryClassId === "rom-mage"; } /** * CoA gives one Inspiration stack when the caster spends Nature's Power. * This resolves one positive resource-spend event, not each point spent. */ export function romDruidMageInspirationStacksAfterSpend( currentStacks: number, spentNaturePower: number, passiveLearned: boolean, ): number { const stacks = Math.max(0, Math.min(ROM_DRUID_MAGE_INSPIRATION_MAX_STACKS, Math.floor(currentStacks))); if (!passiveLearned || !(spentNaturePower > 0)) return stacks; return Math.min(ROM_DRUID_MAGE_INSPIRATION_MAX_STACKS, stacks + 1); } export function romDruidMageInspirationBonusPercent(stacks: number): number { const boundedStacks = Math.max(0, Math.min(ROM_DRUID_MAGE_INSPIRATION_MAX_STACKS, Math.floor(stacks))); return boundedStacks * ROM_DRUID_MAGE_INSPIRATION_BONUS_PER_STACK_PERCENT; }