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
@@ -0,0 +1,183 @@
|
||||
import { criarSom } from "./som";
|
||||
|
||||
class GanhoFalso {
|
||||
gain = { value: 1 };
|
||||
connect = jest.fn();
|
||||
disconnect = jest.fn();
|
||||
}
|
||||
|
||||
class FonteFalsa {
|
||||
buffer: unknown = null;
|
||||
playbackRate = { value: 1 };
|
||||
onended: (() => void) | null = null;
|
||||
connect = jest.fn();
|
||||
start = jest.fn();
|
||||
stop = jest.fn();
|
||||
}
|
||||
|
||||
class ContextoFalso {
|
||||
static ultimo: ContextoFalso | null = null;
|
||||
destination = {};
|
||||
ganhos: GanhoFalso[] = [];
|
||||
fontes: FonteFalsa[] = [];
|
||||
close = jest.fn(() => Promise.resolve());
|
||||
constructor() {
|
||||
ContextoFalso.ultimo = this;
|
||||
}
|
||||
state = "running";
|
||||
resume = jest.fn(() => Promise.resolve());
|
||||
decodeAudioData() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
createGain() {
|
||||
const g = new GanhoFalso();
|
||||
this.ganhos.push(g);
|
||||
return g;
|
||||
}
|
||||
createBufferSource() {
|
||||
const f = new FonteFalsa();
|
||||
this.fontes.push(f);
|
||||
return f;
|
||||
}
|
||||
}
|
||||
|
||||
const g = globalThis as unknown as { AudioContext?: unknown; fetch?: unknown };
|
||||
|
||||
function respostaOk() {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
ContextoFalso.ultimo = null;
|
||||
g.AudioContext = ContextoFalso;
|
||||
g.fetch = jest.fn(respostaOk);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete g.AudioContext;
|
||||
delete g.fetch;
|
||||
});
|
||||
|
||||
it("tocar antes de liberar não quebra nem cria contexto", () => {
|
||||
const som = criarSom();
|
||||
expect(() => som.tocar("pistola")).not.toThrow();
|
||||
expect(ContextoFalso.ultimo).toBeNull();
|
||||
expect(g.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("liberar cria o contexto e baixa os dez sons", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
expect(ContextoFalso.ultimo).not.toBeNull();
|
||||
expect(g.fetch).toHaveBeenCalledTimes(10);
|
||||
expect(g.fetch).toHaveBeenCalledWith("/jogo/sons/gemido-1.mp3");
|
||||
som.tocar("acerto", { volume: 0.4 });
|
||||
const ctx = ContextoFalso.ultimo as ContextoFalso;
|
||||
expect(ctx.fontes).toHaveLength(1);
|
||||
expect(ctx.fontes[0].start).toHaveBeenCalled();
|
||||
expect(ctx.ganhos.at(-1)?.gain.value).toBe(0.4);
|
||||
});
|
||||
|
||||
it("liberar de novo retoma o contexto parado, sem lançar se a retomada falhar", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
const ctx = ContextoFalso.ultimo as ContextoFalso;
|
||||
ctx.resume.mockClear();
|
||||
await som.liberar();
|
||||
expect(ctx.resume).not.toHaveBeenCalled();
|
||||
for (const parado of ["suspended", "interrupted"]) {
|
||||
ctx.state = parado;
|
||||
ctx.resume.mockImplementationOnce(() => Promise.reject(new Error("não")));
|
||||
await expect(som.liberar()).resolves.toBeUndefined();
|
||||
}
|
||||
expect(ctx.resume).toHaveBeenCalledTimes(2);
|
||||
expect(ContextoFalso.ultimo).toBe(ctx);
|
||||
});
|
||||
|
||||
it("no máximo 8 vozes: a mais antiga é cortada", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
for (let i = 0; i < 10; i++) som.tocar("automatica");
|
||||
const fontes = (ContextoFalso.ultimo as ContextoFalso).fontes;
|
||||
expect(fontes).toHaveLength(10);
|
||||
expect(fontes.filter((f) => f.stop.mock.calls.length > 0)).toEqual([
|
||||
fontes[0],
|
||||
fontes[1],
|
||||
]);
|
||||
});
|
||||
|
||||
it("voz que terminou libera a vaga", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
for (let i = 0; i < 8; i++) som.tocar("pistola");
|
||||
const fontes = (ContextoFalso.ultimo as ContextoFalso).fontes;
|
||||
fontes[3].onended?.();
|
||||
som.tocar("pistola");
|
||||
expect(fontes.some((f) => f.stop.mock.calls.length > 0)).toBe(false);
|
||||
});
|
||||
|
||||
it("variar muda a velocidade em até 6%", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
for (let i = 0; i < 20; i++) som.tocar("pistola", { variar: true });
|
||||
for (const f of (ContextoFalso.ultimo as ContextoFalso).fontes) {
|
||||
expect(f.playbackRate.value).toBeGreaterThanOrEqual(0.94);
|
||||
expect(f.playbackRate.value).toBeLessThanOrEqual(1.06);
|
||||
}
|
||||
});
|
||||
|
||||
it("mudo zera o ganho geral, antes ou depois de liberar", async () => {
|
||||
const som = criarSom();
|
||||
som.definirMudo(true);
|
||||
await som.liberar();
|
||||
const geral = (ContextoFalso.ultimo as ContextoFalso).ganhos[0];
|
||||
expect(geral.gain.value).toBe(0);
|
||||
som.definirMudo(false);
|
||||
expect(geral.gain.value).toBe(1);
|
||||
som.definirMudo(true);
|
||||
expect(geral.gain.value).toBe(0);
|
||||
});
|
||||
|
||||
it("falha de download não lança erro e o som que faltou não toca", async () => {
|
||||
const erro = jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
g.fetch = jest.fn((url: string) => {
|
||||
if (url.includes("pistola")) return Promise.reject(new Error("rede"));
|
||||
if (url.includes("faca")) return Promise.resolve({ ok: false });
|
||||
return respostaOk();
|
||||
});
|
||||
const som = criarSom();
|
||||
await expect(som.liberar()).resolves.toBeUndefined();
|
||||
som.tocar("pistola");
|
||||
som.tocar("faca");
|
||||
som.tocar("golpe");
|
||||
expect((ContextoFalso.ultimo as ContextoFalso).fontes).toHaveLength(1);
|
||||
expect(erro).not.toHaveBeenCalled();
|
||||
erro.mockRestore();
|
||||
});
|
||||
|
||||
it("sem AudioContext tudo vira no-op", async () => {
|
||||
delete g.AudioContext;
|
||||
const som = criarSom();
|
||||
await expect(som.liberar()).resolves.toBeUndefined();
|
||||
expect(() => {
|
||||
som.tocar("drone");
|
||||
som.definirMudo(true);
|
||||
som.descartar();
|
||||
}).not.toThrow();
|
||||
expect(g.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("descartar fecha o contexto e cala o resto", async () => {
|
||||
const som = criarSom();
|
||||
await som.liberar();
|
||||
som.tocar("caixa");
|
||||
som.descartar();
|
||||
const ctx = ContextoFalso.ultimo as ContextoFalso;
|
||||
expect(ctx.close).toHaveBeenCalled();
|
||||
expect(ctx.fontes[0].stop).toHaveBeenCalled();
|
||||
som.tocar("caixa");
|
||||
expect(ctx.fontes).toHaveLength(1);
|
||||
});
|
||||
Reference in new issue
Block a user