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.
This commit is contained in:
1 parent
c7d09d63d4
commit
67b32fcab3
118 files changed
+16730
No files matched your search
+127
@@ -0,0 +1,127 @@
|
||||
/** 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user