126 lines
5.1 KiB
TypeScript
126 lines
5.1 KiB
TypeScript
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;
|
|
}
|