Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
158 lines
4.6 KiB
TypeScript
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 });
|
|
}
|