1
0
Fork 0
polaris-task-force/src/components/frontend/qualification/sounds.ts
Z8MB1E b905fe2b5e feat(qualification): handle civilian gameplay feedback
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-04 12:04:44 -04:00

158 lines
4.6 KiB
TypeScript

declare global {
interface Window {
webkitAudioContext?: typeof AudioContext;
}
}
let audioCtx: AudioContext | null = null;
function getCtx(): AudioContext | null {
if (typeof window === "undefined") return null;
const AudioCtor = window.AudioContext ?? window.webkitAudioContext;
if (!AudioCtor) return null;
if (!audioCtx) audioCtx = new AudioCtor();
if (audioCtx.state === "suspended") void audioCtx.resume();
return audioCtx;
}
type ToneOptions = {
readonly frequency: number;
readonly endFrequency?: number;
readonly duration: number;
readonly type?: OscillatorType;
readonly volume?: number;
readonly delay?: number;
};
function playTone(ctx: AudioContext, options: ToneOptions): void {
const { frequency, endFrequency, duration, type = "sine", volume = 0.2, delay = 0 } = options;
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
const startAt = ctx.currentTime + delay;
oscillator.type = type;
oscillator.frequency.setValueAtTime(Math.max(frequency, 1), startAt);
if (endFrequency !== undefined && endFrequency !== frequency) {
oscillator.frequency.exponentialRampToValueAtTime(
Math.max(endFrequency, 1),
startAt + duration,
);
}
gain.gain.setValueAtTime(0.0001, startAt);
gain.gain.exponentialRampToValueAtTime(volume, startAt + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, startAt + duration);
oscillator.connect(gain);
gain.connect(ctx.destination);
oscillator.start(startAt);
oscillator.stop(startAt + duration + 0.05);
}
function playNoise(
ctx: AudioContext,
options: { readonly duration: number; readonly volume?: number; readonly lowpass?: number },
): void {
const { duration, volume = 0.2, lowpass = 800 } = options;
const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration));
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let index = 0; index < bufferSize; index += 1) {
data[index] = Math.random() * 2 - 1;
}
const source = ctx.createBufferSource();
source.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.setValueAtTime(lowpass, ctx.currentTime);
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(volume, ctx.currentTime + 0.005);
gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + duration);
source.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
source.start();
}
export function playFire(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, {
frequency: 180,
endFrequency: 90,
duration: 0.055,
type: "square",
volume: 0.12,
});
}
export function playHeadshot(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, {
frequency: 720,
endFrequency: 1180,
duration: 0.12,
type: "triangle",
volume: 0.16,
});
playTone(ctx, { frequency: 1040, duration: 0.16, type: "sine", volume: 0.12, delay: 0.06 });
}
export function playTorsoHit(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, {
frequency: 360,
endFrequency: 220,
duration: 0.1,
type: "triangle",
volume: 0.15,
});
}
export function playCivilianHit(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, {
frequency: 300,
endFrequency: 140,
duration: 0.22,
type: "sawtooth",
volume: 0.16,
});
playNoise(ctx, { duration: 0.12, volume: 0.12, lowpass: 520 });
}
export function playMiss(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, {
frequency: 120,
endFrequency: 55,
duration: 0.18,
type: "sawtooth",
volume: 0.14,
});
playNoise(ctx, { duration: 0.11, volume: 0.11, lowpass: 480 });
}
export function playCountdownTick(critical = false): void {
const ctx = getCtx();
if (!ctx) return;
const frequency = critical ? 880 : 520;
playTone(ctx, { frequency, duration: critical ? 0.09 : 0.06, type: "square", volume: 0.1 });
}
export function playRoundComplete(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 420, duration: 0.12, type: "triangle", volume: 0.14 });
playTone(ctx, { frequency: 630, duration: 0.12, type: "triangle", volume: 0.14, delay: 0.1 });
playTone(ctx, { frequency: 940, duration: 0.24, type: "sine", volume: 0.16, delay: 0.2 });
}
export function playXp(): void {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 660, duration: 0.09, type: "sine", volume: 0.18 });
playTone(ctx, { frequency: 880, duration: 0.16, type: "sine", volume: 0.18, delay: 0.09 });
}