Files
healer-man/src/game/romMageDruidGreenGuardianRuntime.ts
T
2026-10-02 10:38:12 -04:00

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),
};
}