Update Healer Man server

This commit is contained in:
phenom committed 2026-09-24 16:42:43 -04:00
1 parent a64b2cb575
commit 71246e5b75
502 files changed
+4245 -19

No files matched your search

+88 -6
View File
@@ -14,15 +14,24 @@ const MAX_COMBAT_VOICES = 32;
interface ActiveVoice {
readonly group: CombatActorGroup;
readonly castLoop?: ActiveCastLoop;
readonly stop: () => void;
}
interface ActiveCastLoop {
readonly group: CombatActorGroup;
readonly timeout: ReturnType<typeof setTimeout>;
cancelled: boolean;
stop?: () => void;
}
let context: AudioContext | null = null;
let masterGain: GainNode | null = null;
let groupGains: Record<CombatActorGroup, GainNode> | null = null;
let configuredSettings: CombatPresentationSettings | null = null;
let configuredMasterVolume = 0;
const activeVoices: ActiveVoice[] = [];
const activeCastLoops = new Map<string, ActiveCastLoop>();
const activeMasterVoices = new Set<() => void>();
const decodedBuffers = new Map<string, Promise<AudioBuffer | null>>();
@@ -146,10 +155,54 @@ function registerVoice(voice: ActiveVoice): () => void {
};
}
function castLoopKey(event: CombatPresentationEvent): string {
return `${event.sessionId}:${event.sourceActorId}:${event.abilityId}`;
}
function finishCastLoop(key: string, loop: ActiveCastLoop): void {
if (activeCastLoops.get(key) !== loop) return;
loop.cancelled = true;
clearTimeout(loop.timeout);
activeCastLoops.delete(key);
}
function stopCastLoop(key: string): void {
const loop = activeCastLoops.get(key);
if (!loop) return;
finishCastLoop(key, loop);
loop.stop?.();
}
function beginCastLoop(event: CombatPresentationEvent): ActiveCastLoop {
const key = castLoopKey(event);
stopCastLoop(key);
const durationMs = Number.isFinite(event.durationMs)
? Math.max(0, event.durationMs ?? 0)
: 4_500;
const loop: ActiveCastLoop = {
group: event.actorGroup,
cancelled: false,
timeout: setTimeout(() => stopCastLoop(key), durationMs),
};
activeCastLoops.set(key, loop);
return loop;
}
export function selectCombatSoundAssets(
sounds: readonly CombatEffectSoundAsset[],
event: Pick<CombatPresentationEvent, "phase" | "instantCast">,
): readonly CombatEffectSoundAsset[] {
const exact = sounds.filter((sound) => sound.url && (!sound.phase || sound.phase === event.phase));
if (exact.length) return exact;
return event.phase === "release" && event.instantCast
? sounds.filter((sound) => sound.url && sound.phase === "cast-start")
: [];
}
function weightedSound(event: CombatPresentationEvent): CombatEffectSoundAsset | null {
if (!configuredSettings || !authenticCombatAudioEnabled(configuredSettings)) return null;
const sounds = combatEffectManifestEntry(event.sourceSpellId, event.source)?.sounds ?? [];
const playable = sounds.filter((sound) => sound.url);
const playable = selectCombatSoundAssets(sounds, event);
if (!playable.length) return null;
const total = playable.reduce((sum, sound) => sum + Math.max(1, sound.weight ?? 1), 0);
let cursor = (event.id * 0.61803398875 % 1) * total;
@@ -212,7 +265,11 @@ export async function playMasterAudioCue(
return stop;
}
async function playBufferedCombatCue(event: CombatPresentationEvent, sound: CombatEffectSoundAsset): Promise<boolean> {
async function playBufferedCombatCue(
event: CombatPresentationEvent,
sound: CombatEffectSoundAsset,
castLoop?: ActiveCastLoop,
): Promise<boolean> {
if (!sound.url) return false;
if (
!configuredSettings
@@ -226,12 +283,14 @@ async function playBufferedCombatCue(event: CombatPresentationEvent, sound: Comb
|| !audioContext
|| !buffer
|| !groupGains
|| castLoop?.cancelled
) return false;
void audioContext.resume().catch(() => undefined);
const source = audioContext.createBufferSource();
const envelope = audioContext.createGain();
const panner = positionPanner(audioContext, event);
source.buffer = buffer;
if (castLoop) source.loop = true;
envelope.gain.value = Math.max(0, Math.min(1, sound.volume ?? 1));
source.connect(envelope);
envelope.connect(panner);
@@ -239,6 +298,7 @@ async function playBufferedCombatCue(event: CombatPresentationEvent, sound: Comb
let stopped = false;
const voice: ActiveVoice = {
group: event.actorGroup,
castLoop,
stop: () => {
if (stopped) return;
stopped = true;
@@ -246,8 +306,10 @@ async function playBufferedCombatCue(event: CombatPresentationEvent, sound: Comb
},
};
const unregister = registerVoice(voice);
if (castLoop) castLoop.stop = voice.stop;
source.onended = () => {
unregister();
if (castLoop) finishCastLoop(castLoopKey(event), castLoop);
source.disconnect();
envelope.disconnect();
panner.disconnect();
@@ -313,8 +375,15 @@ function playGeneratedCombatCue(event: CombatPresentationEvent): boolean {
}
export function playCombatAudio(event: CombatPresentationEvent): void {
if (event.phase === "release" || event.phase === "cast-cancel") {
stopCastLoop(castLoopKey(event));
}
if (event.phase === "cast-cancel") {
stopCombatAudioGroup(event.actorGroup);
// A cancelled cast still cuts off legacy one-shot voices for this group.
// Other actors' active cast loops keep playing until their own terminal event.
for (const voice of [...activeVoices]) {
if (voice.group === event.actorGroup && !voice.castLoop) voice.stop();
}
return;
}
if (
@@ -323,12 +392,21 @@ export function playCombatAudio(event: CombatPresentationEvent): void {
) return;
const sound = weightedSound(event);
if (sound) {
void playBufferedCombatCue(event, sound)
const castLoop = event.phase === "cast-start" && sound.loopDuringCast
? beginCastLoop(event)
: undefined;
void playBufferedCombatCue(event, sound, castLoop)
.then((played) => {
if (!played) playGeneratedCombatCue(event);
if (!played && !castLoop?.cancelled) {
if (castLoop) stopCastLoop(castLoopKey(event));
playGeneratedCombatCue(event);
}
})
.catch(() => {
playGeneratedCombatCue(event);
if (!castLoop?.cancelled) {
if (castLoop) stopCastLoop(castLoopKey(event));
playGeneratedCombatCue(event);
}
});
} else {
playGeneratedCombatCue(event);
@@ -336,12 +414,16 @@ export function playCombatAudio(event: CombatPresentationEvent): void {
}
export function stopCombatAudioGroup(group: CombatActorGroup): void {
for (const [key, loop] of activeCastLoops) {
if (loop.group === group) stopCastLoop(key);
}
for (const voice of [...activeVoices]) {
if (voice.group === group) voice.stop();
}
}
export function resetCombatAudio(): void {
for (const key of activeCastLoops.keys()) stopCastLoop(key);
for (const voice of [...activeVoices]) voice.stop();
for (const stop of [...activeMasterVoices]) stop();
activeVoices.length = 0;