111 lines
4.7 KiB
TypeScript
111 lines
4.7 KiB
TypeScript
import type { AbilityEffect } from "./abilityCatalog";
|
|
import type { ActiveAura, AuraDefinition } from "./combatAuras";
|
|
|
|
/** Current CoA Mage/Druid Green Guardian stack behavior (skill 494331). */
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_SKILL_ID = 494331;
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_MAX_STACKS = 20;
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_GAIN_INTERVAL_MS = 1_000;
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_MOVEMENT_SPEED_PERCENT_PER_STACK = 1;
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_AURA_TAG = "rom-mage-druid-green-guardian";
|
|
export const ROM_MAGE_DRUID_GREEN_GUARDIAN_DESCRIPTION = "While standing still, gain one Green Guardian stack each second, up to 20. Each stack grants 1% movement speed. Moving pauses stack generation. Current CoA also describes an MP restore when a stack is lost, but the loss trigger and percentage basis are unspecified, so that clause is not implemented.";
|
|
|
|
export function romMageDruidGreenGuardianAuraDefinition(): AuraDefinition {
|
|
return {
|
|
id: "rom:mage-druid:green-guardian",
|
|
name: "Green Guardian",
|
|
description: ROM_MAGE_DRUID_GREEN_GUARDIAN_DESCRIPTION,
|
|
disposition: "buff",
|
|
durationMs: null,
|
|
maxStacks: ROM_MAGE_DRUID_GREEN_GUARDIAN_MAX_STACKS,
|
|
stackBehavior: "replace",
|
|
utilityTags: [ROM_MAGE_DRUID_GREEN_GUARDIAN_AURA_TAG],
|
|
modifiers: [{
|
|
kind: "stat",
|
|
stat: "movement-speed",
|
|
operation: "percent",
|
|
value: ROM_MAGE_DRUID_GREEN_GUARDIAN_MOVEMENT_SPEED_PERCENT_PER_STACK / 100,
|
|
perStack: true,
|
|
}],
|
|
};
|
|
}
|
|
|
|
export function romMageDruidGreenGuardianAuraEffect(): Extract<AbilityEffect, { kind: "apply-aura" }> {
|
|
return {
|
|
kind: "apply-aura",
|
|
recipient: "caster",
|
|
aura: romMageDruidGreenGuardianAuraDefinition(),
|
|
sourceEffectType: -1,
|
|
sourceAuraType: -1,
|
|
miscValue: 0,
|
|
triggerSpellId: ROM_MAGE_DRUID_GREEN_GUARDIAN_SKILL_ID,
|
|
};
|
|
}
|
|
|
|
export function isRomMageDruidPair(row: {
|
|
readonly classId?: string;
|
|
readonly secondaryClassId?: string | null;
|
|
}): boolean {
|
|
return row.classId === "rom-mage" && row.secondaryClassId === "rom-druid";
|
|
}
|
|
|
|
function boundedStacks(stacks: number): number {
|
|
if (!Number.isFinite(stacks)) return 0;
|
|
return Math.max(0, Math.min(ROM_MAGE_DRUID_GREEN_GUARDIAN_MAX_STACKS, Math.floor(stacks)));
|
|
}
|
|
|
|
/** Apply one source-described stack gain after an eligible second spent standing still. */
|
|
export function romMageDruidGreenGuardianStacksAfterStationarySecond(stacks: number): number {
|
|
return Math.min(ROM_MAGE_DRUID_GREEN_GUARDIAN_MAX_STACKS, boundedStacks(stacks) + 1);
|
|
}
|
|
|
|
/** Percentage points of movement speed granted by the current stack count. */
|
|
export function romMageDruidGreenGuardianMovementSpeedPercent(stacks: number): number {
|
|
return boundedStacks(stacks) * ROM_MAGE_DRUID_GREEN_GUARDIAN_MOVEMENT_SPEED_PERCENT_PER_STACK;
|
|
}
|
|
|
|
export function romMageDruidGreenGuardianMovementSpeedMultiplier(
|
|
auras: readonly ActiveAura[],
|
|
targetId: string,
|
|
now: number,
|
|
): number {
|
|
const stacks = auras
|
|
.filter((aura) => aura.targetId === targetId
|
|
&& aura.definition.utilityTags?.includes(ROM_MAGE_DRUID_GREEN_GUARDIAN_AURA_TAG)
|
|
&& (aura.expiresAt === null || aura.expiresAt > now))
|
|
.reduce((total, aura) => total + boundedStacks(aura.stacks), 0);
|
|
return 1 + romMageDruidGreenGuardianMovementSpeedPercent(stacks) / 100;
|
|
}
|
|
|
|
export interface RomMageDruidGreenGuardianStationaryProgress {
|
|
readonly position: readonly [number, number, number];
|
|
readonly stationaryMs: number;
|
|
}
|
|
|
|
export interface RomMageDruidGreenGuardianStationaryAdvance {
|
|
readonly progress: RomMageDruidGreenGuardianStationaryProgress;
|
|
readonly stacksGained: number;
|
|
}
|
|
|
|
/**
|
|
* Advance one simulator tick. Any observed position change resets progress;
|
|
* this is the documented one-full-stationary-second approximation for CoA's
|
|
* unspecific “brief” movement pause. It does not remove existing stacks.
|
|
*/
|
|
export function romMageDruidGreenGuardianAdvanceStationaryProgress(
|
|
previous: RomMageDruidGreenGuardianStationaryProgress | undefined,
|
|
position: readonly [number, number, number],
|
|
deltaMs: number,
|
|
): RomMageDruidGreenGuardianStationaryAdvance {
|
|
const safeDelta = Math.max(0, Number.isFinite(deltaMs) ? deltaMs : 0);
|
|
if (!previous) return { progress: { position, stationaryMs: 0 }, stacksGained: 0 };
|
|
const moved = (position[0] - previous.position[0]) ** 2
|
|
+ (position[1] - previous.position[1]) ** 2
|
|
+ (position[2] - previous.position[2]) ** 2 > 1e-8;
|
|
if (moved) return { progress: { position, stationaryMs: 0 }, stacksGained: 0 };
|
|
const total = Math.max(0, previous.stationaryMs) + safeDelta;
|
|
return {
|
|
progress: { position, stationaryMs: total % ROM_MAGE_DRUID_GREEN_GUARDIAN_GAIN_INTERVAL_MS },
|
|
stacksGained: Math.floor(total / ROM_MAGE_DRUID_GREEN_GUARDIAN_GAIN_INTERVAL_MS),
|
|
};
|
|
}
|