Release Healer Man 0.1.9

This commit is contained in:
phenom committed 2026-08-30 14:16:56 -04:00
1 parent c245a199b6
commit d060296ade
74 files changed
+5901 -334

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
import type {
CombatPresentationEvent,
CombatVfxQuality,
} from "../game/combatPresentation";
export interface ActiveCombatEffect {
readonly key: string;
readonly event: CombatPresentationEvent;
}
export const COMBAT_EFFECT_SYSTEM_LIMIT: Readonly<Record<CombatVfxQuality, number>> = {
off: 0,
low: 24,
high: 32,
};
export interface CombatEffectFrameState {
readonly elapsedMs: number;
readonly motionDeltaSeconds: number;
readonly progress: number;
readonly done: boolean;
}
export function advanceCombatEffectFrame(
elapsedMs: number,
rawDeltaSeconds: number,
durationMs: number,
): CombatEffectFrameState {
const safeElapsedMs = Number.isFinite(elapsedMs) ? Math.max(0, elapsedMs) : 0;
const deltaSeconds = Number.isFinite(rawDeltaSeconds) ? Math.max(0, rawDeltaSeconds) : 0;
const safeDurationMs = Number.isFinite(durationMs) ? Math.max(1, durationMs) : 1;
// Age follows wall time so a slow frame expires work instead of extending the
// overload. Motion remains bounded to avoid a single-frame visual jump.
const nextElapsedMs = safeElapsedMs + deltaSeconds * 1_000;
const progress = Math.min(1, nextElapsedMs / safeDurationMs);
return {
elapsedMs: nextElapsedMs,
motionDeltaSeconds: Math.min(0.05, deltaSeconds),
progress,
done: progress >= 1,
};
}
function actorPriority(event: CombatPresentationEvent): number {
const groupPriority = event.actorGroup === "player" ? 30 : event.actorGroup === "party" ? 20 : 10;
// Preserve readable combat beats before actor ownership. An enemy cast or
// impact is more useful than another periodic tick from any actor group.
const phasePriority = event.phase === "cast-start"
? 300
: event.phase === "release" || event.phase === "impact"
? 200
: event.phase === "tick"
? 0
: 100;
return groupPriority + phasePriority;
}
function areaBurstKey(event: CombatPresentationEvent): string | null {
if (event.delivery !== "area" || event.phase === "cast-start" || event.phase === "cast-cancel") {
return null;
}
return `${event.sessionId}:${event.sourceActorId}:${event.abilityId}:${event.occurredAt}`;
}
function effectKey(event: CombatPresentationEvent): string {
return `${event.sessionId}:${event.id}`;
}
function trimCombatEffects(
effects: readonly ActiveCombatEffect[],
limit: number,
): readonly ActiveCombatEffect[] {
if (effects.length <= limit) return effects;
const next = [...effects];
while (next.length > limit) {
let removalIndex = 0;
let removalPriority = actorPriority(next[0].event);
for (let index = 1; index < next.length; index += 1) {
const priority = actorPriority(next[index].event);
if (priority < removalPriority) {
removalIndex = index;
removalPriority = priority;
}
}
next.splice(removalIndex, 1);
}
return next;
}
export function capCombatEffects(
effects: readonly ActiveCombatEffect[],
quality: CombatVfxQuality,
): readonly ActiveCombatEffect[] {
const limit = COMBAT_EFFECT_SYSTEM_LIMIT[quality];
if (limit === 0) return effects.length === 0 ? effects : [];
return trimCombatEffects(effects, limit);
}
export function updateCombatEffectQueue(
current: readonly ActiveCombatEffect[],
event: CombatPresentationEvent,
quality: CombatVfxQuality,
): readonly ActiveCombatEffect[] {
const withoutCast = event.phase === "release" || event.phase === "cast-cancel"
? current.filter((effect) => !(
effect.event.phase === "cast-start"
&& effect.event.sourceActorId === event.sourceActorId
&& effect.event.abilityId === event.abilityId
))
: current;
if (quality === "off") return current.length === 0 ? current : [];
if (event.phase === "cast-cancel") {
return withoutCast.length === current.length ? current : withoutCast;
}
const burstKey = areaBurstKey(event);
if (burstKey) {
const existingIndex = withoutCast.findIndex((effect) => areaBurstKey(effect.event) === burstKey);
if (existingIndex >= 0) {
const existing = withoutCast[existingIndex];
if (existing.event.phase === event.phase) return withoutCast;
const next = [...withoutCast];
next[existingIndex] = { key: effectKey(event), event };
return capCombatEffects(next, quality);
}
}
const next = capCombatEffects(
[...withoutCast, { key: effectKey(event), event }],
quality,
);
if (
next.length === current.length
&& next.every((effect, index) => effect.key === current[index]?.key)
) return current;
return next;
}