Files
portifolio-evandrodevbr/app/jogo/som.ts
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
2026-11-10 19:37:10 -03:00

128 lines
3.4 KiB
TypeScript

/** Sons do jogo com Web Audio. Sem `AudioContext` no navegador, tudo vira no-op. */
export type NomeDoSom =
| "pistola"
| "automatica"
| "faca"
| "acerto"
| "gemido-1"
| "gemido-2"
| "golpe"
| "drone"
| "resolvido"
| "caixa";
export type Som = {
/** Cria o contexto e baixa os sons; depois, retoma o contexto parado. Chamar a cada toque ou clique que começa ou volta ao jogo. */
liberar(): Promise<void>;
tocar(nome: NomeDoSom, opcoes?: { volume?: number; variar?: boolean }): void;
definirMudo(mudo: boolean): void;
descartar(): void;
};
const NOMES: readonly NomeDoSom[] = [
"pistola",
"automatica",
"faca",
"acerto",
"gemido-1",
"gemido-2",
"golpe",
"drone",
"resolvido",
"caixa",
];
const MAXIMO_DE_VOZES = 8;
/** `variar` muda a velocidade em até 6% para cima ou para baixo. */
const VARIACAO = 0.06;
type Voz = { fonte: AudioBufferSourceNode; ganho: GainNode };
export function criarSom(): Som {
let contexto: AudioContext | null = null;
let geral: GainNode | null = null;
let carga: Promise<void> | null = null;
let mudo = false;
let descartado = false;
const buffers = new Map<NomeDoSom, AudioBuffer>();
const vozes: Voz[] = [];
async function baixar(ctx: AudioContext, nome: NomeDoSom): Promise<void> {
try {
const resposta = await fetch(`/jogo/sons/${nome}.mp3`);
if (!resposta.ok) return;
const buffer = await ctx.decodeAudioData(await resposta.arrayBuffer());
buffers.set(nome, buffer);
} catch {
// Som que não carregou simplesmente não toca.
}
}
function soltar(voz: Voz): void {
const i = vozes.indexOf(voz);
if (i >= 0) vozes.splice(i, 1);
voz.ganho.disconnect();
}
return {
liberar() {
const Contexto = globalThis.AudioContext;
if (descartado || typeof Contexto !== "function")
return Promise.resolve();
if (carga) {
// no iOS o contexto fica `interrupted` ou `suspended` ao sair da aba
if (contexto && contexto.state !== "running")
contexto.resume().catch(() => {});
return carga;
}
const ctx = new Contexto();
contexto = ctx;
geral = ctx.createGain();
geral.gain.value = mudo ? 0 : 1;
geral.connect(ctx.destination);
carga = Promise.all([
ctx.resume().catch(() => {}),
...NOMES.map((nome) => baixar(ctx, nome)),
]).then(() => {});
return carga;
},
tocar(nome, opcoes) {
const buffer = buffers.get(nome);
if (!contexto || !geral || !buffer) return;
if (vozes.length >= MAXIMO_DE_VOZES) {
const antiga = vozes[0];
antiga.fonte.stop();
soltar(antiga);
}
const fonte = contexto.createBufferSource();
fonte.buffer = buffer;
if (opcoes?.variar)
fonte.playbackRate.value = 1 + (Math.random() * 2 - 1) * VARIACAO;
const ganho = contexto.createGain();
ganho.gain.value = opcoes?.volume ?? 1;
fonte.connect(ganho);
ganho.connect(geral);
const voz = { fonte, ganho };
vozes.push(voz);
fonte.onended = () => soltar(voz);
fonte.start();
},
definirMudo(valor) {
mudo = valor;
if (geral) geral.gain.value = mudo ? 0 : 1;
},
descartar() {
descartado = true;
for (const voz of vozes.splice(0)) voz.fonte.stop();
buffers.clear();
contexto?.close().catch(() => {});
contexto = null;
geral = null;
},
};
}