Release Healer Man 0.1.9
This commit is contained in:
1 parent
c245a199b6
commit
d060296ade
74 files changed
+5901
-334
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user