141 lines
5.6 KiB
TypeScript
141 lines
5.6 KiB
TypeScript
import type { CharacterStatRuleset } from "./characterStatRuleset";
|
|
import type { ActiveAura } from "./combatAuras";
|
|
import type { RomNativeDerivedStats } from "./romNativeStatsTypes";
|
|
|
|
export type RomNativeRecoveryActorKind = "player" | "party-npc";
|
|
|
|
/** Runtime remainder for the source's 2 s player / 6 s owned-NPC recovery pulses. */
|
|
export interface RomNativeRecoveryClock {
|
|
readonly ruleset: "rom";
|
|
readonly actorKind: RomNativeRecoveryActorKind;
|
|
/** Elapsed milliseconds since the last pulse, always below the actor period. */
|
|
readonly elapsedMs: number;
|
|
}
|
|
|
|
export interface RomNativeRecoveryClockAdvance {
|
|
readonly clock: RomNativeRecoveryClock | undefined;
|
|
/** Pulse locations measured from the start of this update interval. */
|
|
readonly pulseOffsetsMs: readonly number[];
|
|
}
|
|
|
|
export interface RomNativeRecoveryPulseAmounts {
|
|
readonly health: number;
|
|
readonly mana: number;
|
|
}
|
|
|
|
export const ROM_NATIVE_PLAYER_RECOVERY_PERIOD_MS = 2_000;
|
|
export const ROM_NATIVE_OWNED_NPC_RECOVERY_PERIOD_MS = 6_000;
|
|
|
|
export function romNativeRecoveryPeriodMs(actorKind: RomNativeRecoveryActorKind): number {
|
|
return actorKind === "player"
|
|
? ROM_NATIVE_PLAYER_RECOVERY_PERIOD_MS
|
|
: ROM_NATIVE_OWNED_NPC_RECOVERY_PERIOD_MS;
|
|
}
|
|
|
|
/**
|
|
* Advances the source pulse clock without borrowing time across ruleset or actor changes.
|
|
* An uninitialized clock starts accumulating immediately; a mismatched clock is reset
|
|
* and deliberately does not credit the interval that crossed the mode boundary.
|
|
*/
|
|
export function advanceRomNativeRecoveryClock(
|
|
previous: RomNativeRecoveryClock | undefined,
|
|
statRuleset: CharacterStatRuleset,
|
|
actorKind: RomNativeRecoveryActorKind,
|
|
deltaMs: number,
|
|
): RomNativeRecoveryClockAdvance {
|
|
if (statRuleset !== "rom") return { clock: undefined, pulseOffsetsMs: [] };
|
|
|
|
const period = romNativeRecoveryPeriodMs(actorKind);
|
|
const compatible = previous?.ruleset === "rom" && previous.actorKind === actorKind;
|
|
if (previous && !compatible) {
|
|
return {
|
|
clock: Object.freeze({ ruleset: "rom", actorKind, elapsedMs: 0 }),
|
|
pulseOffsetsMs: [],
|
|
};
|
|
}
|
|
|
|
const elapsedBefore = compatible && Number.isFinite(previous.elapsedMs)
|
|
? Math.max(0, Math.min(period - Number.EPSILON, previous.elapsedMs))
|
|
: 0;
|
|
const safeDelta = Number.isFinite(deltaMs) ? Math.max(0, deltaMs) : 0;
|
|
const total = elapsedBefore + safeDelta;
|
|
const pulseCount = Math.floor(total / period);
|
|
const pulseOffsetsMs = Array.from({ length: pulseCount }, (_, index) => (
|
|
Math.max(0, period - elapsedBefore) + index * period
|
|
));
|
|
return {
|
|
clock: Object.freeze({ ruleset: "rom", actorKind, elapsedMs: total % period }),
|
|
pulseOffsetsMs,
|
|
};
|
|
}
|
|
|
|
/** One native HP/MP pulse; point values intentionally remain fractional. */
|
|
export function romNativeRecoveryPulseAmounts(
|
|
stats: Pick<RomNativeDerivedStats, "healthRecovery" | "manaRecovery" | "usesMana">,
|
|
actorKind: RomNativeRecoveryActorKind,
|
|
inCombat: boolean,
|
|
sitting = false,
|
|
): RomNativeRecoveryPulseAmounts {
|
|
const sourceHealthRecovery = Number.isFinite(stats.healthRecovery) ? Math.max(0, stats.healthRecovery) : 0;
|
|
const sourceManaRecovery = stats.usesMana && Number.isFinite(stats.manaRecovery)
|
|
? Math.max(0, stats.manaRecovery)
|
|
: 0;
|
|
if (inCombat) return Object.freeze({ health: 0, mana: sourceManaRecovery / 24 });
|
|
const idleFactor = actorKind === "party-npc" || sitting ? 0.66 : 1 / 3;
|
|
return Object.freeze({
|
|
health: sourceHealthRecovery * idleFactor,
|
|
mana: sourceManaRecovery * idleFactor,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Simulator representation of explicit source RoM resource-over-time bindings.
|
|
* These are kept separate from the character's source ReMP pulse calculation.
|
|
* Unmarked/shared WoW regeneration auras do not pass this source-ID gate.
|
|
*/
|
|
export function romNativePeriodicManaEffectPerSecond(
|
|
auras: readonly ActiveAura[],
|
|
maximumMana: number,
|
|
): number {
|
|
let flatPointsPerSecond = 0;
|
|
let percentOfMaximumPerSecond = 0;
|
|
for (const aura of auras) {
|
|
if (!aura.definition.id.startsWith("rom-resource-over-time-")) continue;
|
|
for (const modifier of aura.definition.modifiers ?? []) {
|
|
const amount = modifier.value * (modifier.perStack === false ? 1 : aura.stacks);
|
|
if (modifier.kind === "resource"
|
|
&& modifier.resource === "mana"
|
|
&& modifier.property === "regeneration"
|
|
&& modifier.operation === "flat") {
|
|
flatPointsPerSecond += amount;
|
|
} else if (modifier.kind === "stat"
|
|
&& modifier.stat === "mana-regeneration-max-percent-per-second"
|
|
&& modifier.operation === "flat") {
|
|
percentOfMaximumPerSecond += amount;
|
|
}
|
|
}
|
|
}
|
|
const safeMaximum = Number.isFinite(maximumMana) ? Math.max(0, maximumMana) : 0;
|
|
return flatPointsPerSecond + safeMaximum * percentOfMaximumPerSecond;
|
|
}
|
|
|
|
/** Integrates explicit RoM periodic Mana effects (including drains) over their live interval. */
|
|
export function romNativePeriodicManaEffectDuring(
|
|
auras: readonly ActiveAura[],
|
|
intervalStartAt: number,
|
|
intervalEndAt: number,
|
|
maximumMana: number,
|
|
): number {
|
|
if (!Number.isFinite(intervalStartAt) || !Number.isFinite(intervalEndAt) || intervalEndAt <= intervalStartAt) return 0;
|
|
let total = 0;
|
|
for (const aura of auras) {
|
|
if (!aura.definition.id.startsWith("rom-resource-over-time-")) continue;
|
|
const activeStart = Math.max(intervalStartAt, aura.appliedAt);
|
|
const activeEnd = Math.min(intervalEndAt, aura.expiresAt ?? intervalEndAt);
|
|
if (activeEnd <= activeStart) continue;
|
|
const perSecond = romNativePeriodicManaEffectPerSecond([aura], maximumMana);
|
|
total += perSecond * (activeEnd - activeStart) / 1_000;
|
|
}
|
|
return total;
|
|
}
|