SOURCE / PINNED RELEASE
Made of little things.
super-platformer
- Release
- b61593abc2c6…
- Author-recorded commit
- bbd35ca510f6…
- License
- LICENSE
- Author’s source reference
- nostr://npub16uawymqtmdg4j6zcl6snt5apzfp4ek2g8lpasnqa75tspdpglcqsgpatvc/wss%3A%2F%2Fgit.napplet.soy%2F/n-01288b1ab38
Archive hash verified: d2529eb9ad1961f2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/**
* Tiny Web Audio sound effects. No assets: every sound is a short synthesized
* tone. The context is created lazily on the first user gesture (the Play
* click), which satisfies browser autoplay rules.
*/
type Osc = OscillatorType;
class Sfx {
private ctx: AudioContext | null = null;
private master: GainNode | null = null;
enabled = true;
private ensure(): AudioContext | null {
if (!this.enabled) return null;
try {
if (!this.ctx) {
const Ctor: typeof AudioContext | undefined =
window.AudioContext ??
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!Ctor) return null;
this.ctx = new Ctor();
this.master = this.ctx.createGain();
this.master.gain.value = 0.5;
this.master.connect(this.ctx.destination);
}
if (this.ctx.state === 'suspended') void this.ctx.resume();
return this.ctx;
} catch {
return null;
}
}
/** Call from a user gesture (e.g. the Play button) to unlock audio. */
resume(): void {
this.ensure();
}
setEnabled(value: boolean): void {
this.enabled = value;
if (value) this.ensure();
}
private blip(
freq: number,
dur: number,
type: Osc,
gain: number,
delay = 0,
slideTo?: number,
): void {
const ctx = this.ensure();
if (!ctx || !this.master) return;
const t0 = ctx.currentTime + delay;
const osc = ctx.createOscillator();
const amp = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, t0);
if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur);
amp.gain.setValueAtTime(0.0001, t0);
amp.gain.exponentialRampToValueAtTime(gain, t0 + 0.01);
amp.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
osc.connect(amp);
amp.connect(this.master);
osc.start(t0);
osc.stop(t0 + dur + 0.03);
}
jump(): void {
this.blip(360, 0.14, 'square', 0.045, 0, 640);
}
coin(): void {
this.blip(990, 0.07, 'sine', 0.05);
this.blip(1480, 0.09, 'sine', 0.045, 0.06);
}
stomp(): void {
this.blip(210, 0.12, 'sawtooth', 0.06, 0, 90);
}
hurt(): void {
this.blip(420, 0.35, 'triangle', 0.07, 0, 70);
}
power(): void {
[523, 659, 784, 1047].forEach((f, i) => this.blip(f, 0.1, 'square', 0.045, i * 0.07));
}
fire(): void {
this.blip(900, 0.09, 'square', 0.04, 0, 320);
}
checkpoint(): void {
this.blip(660, 0.1, 'sine', 0.055);
this.blip(880, 0.13, 'sine', 0.05, 0.09);
}
bossHit(): void {
this.blip(150, 0.18, 'sawtooth', 0.07, 0, 70);
}
bossDown(): void {
[200, 150, 110, 80].forEach((f, i) => this.blip(f, 0.2, 'sawtooth', 0.07, i * 0.12));
}
complete(): void {
[523, 659, 784, 1046].forEach((f, i) => this.blip(f, 0.14, 'triangle', 0.055, i * 0.12));
}
}
export const sfx = new Sfx();
