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,225 @@
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { PROBLEMAS } from "@/lib/jogo/problemas";
|
||||
import type { Cena, OpcoesDaCena } from "./cena";
|
||||
import type { Resultado, Retrato } from "./contrato";
|
||||
import Jogo from "./Jogo";
|
||||
|
||||
// a cena e o som são escritos em paralelo: virtual, para os dublês valerem mesmo antes de os arquivos existirem
|
||||
jest.mock("./cena", () => ({ criarCena: jest.fn() }), { virtual: true });
|
||||
jest.mock(
|
||||
"./som",
|
||||
() => ({
|
||||
criarSom: jest.fn(() => ({
|
||||
liberar: jest.fn(() => Promise.resolve()),
|
||||
tocar: jest.fn(),
|
||||
definirMudo: jest.fn(),
|
||||
descartar: jest.fn(),
|
||||
})),
|
||||
}),
|
||||
{ virtual: true },
|
||||
);
|
||||
|
||||
const { criarCena } = jest.requireMock("./cena") as {
|
||||
criarCena: jest.Mock<Promise<Cena>, [OpcoesDaCena]>;
|
||||
};
|
||||
const { criarSom } = jest.requireMock("./som") as { criarSom: jest.Mock };
|
||||
|
||||
const problema = PROBLEMAS[0];
|
||||
|
||||
const retrato: Retrato = {
|
||||
fase: "horda",
|
||||
horda: 0,
|
||||
problemaAtual: problema.id,
|
||||
restantes: 12,
|
||||
vida: 100,
|
||||
arma: 0,
|
||||
municao: 0,
|
||||
placar: { jogador: 3, drones: 1 },
|
||||
feed: [],
|
||||
drones: [],
|
||||
pausado: false,
|
||||
};
|
||||
|
||||
const resultado: Resultado = {
|
||||
vitoria: true,
|
||||
problemaId: problema.id,
|
||||
dificuldade: "facil",
|
||||
resolvidos: [problema.id],
|
||||
placar: { jogador: 40, drones: 9 },
|
||||
drones: [],
|
||||
segundos: 300,
|
||||
};
|
||||
|
||||
let cenas: { pausar: jest.Mock; continuar: jest.Mock; descartar: jest.Mock }[];
|
||||
let midias: Record<string, boolean>;
|
||||
|
||||
function opcoes(i = 0) {
|
||||
return criarCena.mock.calls[i][0];
|
||||
}
|
||||
|
||||
function som() {
|
||||
return criarSom.mock.results[0].value;
|
||||
}
|
||||
|
||||
async function comecar() {
|
||||
const usuario = userEvent.setup();
|
||||
await usuario.click(screen.getByRole("radio", { name: problema.nome }));
|
||||
await usuario.click(screen.getByRole("button", { name: "Começar" }));
|
||||
await waitFor(() => expect(criarCena).toHaveBeenCalled());
|
||||
return usuario;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cenas = [];
|
||||
midias = {};
|
||||
criarCena.mockReset().mockImplementation(async () => {
|
||||
const cena = {
|
||||
pausar: jest.fn(),
|
||||
continuar: jest.fn(),
|
||||
descartar: jest.fn(),
|
||||
};
|
||||
cenas.push(cena);
|
||||
return cena;
|
||||
});
|
||||
criarSom.mockClear();
|
||||
window.matchMedia = jest.fn((query: string) => ({
|
||||
matches: midias[query] ?? false,
|
||||
media: query,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
})) as unknown as typeof window.matchMedia;
|
||||
jest
|
||||
.spyOn(HTMLCanvasElement.prototype, "getContext")
|
||||
.mockReturnValue({ getExtension: () => null } as never);
|
||||
localStorage.clear();
|
||||
window.history.replaceState(null, "", "/jogo");
|
||||
});
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
describe("Jogo", () => {
|
||||
it("começa pela tela inicial, sem carregar a cena", () => {
|
||||
render(<Jogo />);
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: "Expediente dos Mortos" }),
|
||||
).toBeInTheDocument();
|
||||
expect(criarCena).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ao começar libera o som e cria a cena com a escolha", async () => {
|
||||
render(<Jogo />);
|
||||
await comecar();
|
||||
expect(som().liberar).toHaveBeenCalled();
|
||||
expect(opcoes()).toMatchObject({
|
||||
escolha: { problemaId: problema.id, dificuldade: "facil" },
|
||||
piloto: false,
|
||||
movimentoReduzido: false,
|
||||
});
|
||||
expect(opcoes().canvas).toBeInstanceOf(HTMLCanvasElement);
|
||||
expect(document.documentElement.style.overflow).toBe("hidden");
|
||||
});
|
||||
|
||||
it("mostra no HUD o retrato que a cena manda", async () => {
|
||||
render(<Jogo />);
|
||||
await comecar();
|
||||
act(() => opcoes().aoRetrato(retrato));
|
||||
expect(await screen.findByText("Você 3 · Drones 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("ao terminar mostra o relatório, descarta a cena e destrava a rolagem", async () => {
|
||||
render(<Jogo />);
|
||||
await comecar();
|
||||
await waitFor(() => expect(cenas).toHaveLength(1));
|
||||
act(() => opcoes().aoTerminar(resultado));
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: "Problemas resolvidos" }),
|
||||
).toBeInTheDocument();
|
||||
expect(cenas[0].descartar).toHaveBeenCalled();
|
||||
expect(document.documentElement.style.overflow).toBe("");
|
||||
});
|
||||
|
||||
it("jogar de novo cria outra cena com a mesma escolha e outra semente", async () => {
|
||||
render(<Jogo />);
|
||||
const usuario = await comecar();
|
||||
await waitFor(() => expect(cenas).toHaveLength(1));
|
||||
act(() => opcoes().aoTerminar(resultado));
|
||||
await usuario.click(screen.getByRole("button", { name: "Jogar de novo" }));
|
||||
await waitFor(() => expect(criarCena).toHaveBeenCalledTimes(2));
|
||||
expect(opcoes(1).escolha).toEqual(opcoes(0).escolha);
|
||||
expect(opcoes(1).semente).not.toBe(opcoes(0).semente);
|
||||
});
|
||||
|
||||
it("ao desmontar descarta a cena e o som", async () => {
|
||||
const { unmount } = render(<Jogo />);
|
||||
await comecar();
|
||||
await waitFor(() => expect(cenas).toHaveLength(1));
|
||||
unmount();
|
||||
expect(cenas[0].descartar).toHaveBeenCalled();
|
||||
expect(som().descartar).toHaveBeenCalled();
|
||||
expect(document.documentElement.style.overflow).toBe("");
|
||||
});
|
||||
|
||||
it("se a cena falhar, volta ao início com a mensagem", async () => {
|
||||
criarCena.mockRejectedValueOnce(new Error("sem webgl2"));
|
||||
render(<Jogo />);
|
||||
await comecar();
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent(
|
||||
"Não deu para abrir o jogo neste navegador.",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("heading", { level: 1, name: "Expediente dos Mortos" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("com piloto=1 e um problema válido pula o início", async () => {
|
||||
window.history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`/jogo?piloto=1&semente=7&problema=${problema.id}&dificuldade=normal&vel=8`,
|
||||
);
|
||||
render(<Jogo />);
|
||||
await waitFor(() => expect(criarCena).toHaveBeenCalled());
|
||||
expect(opcoes()).toMatchObject({
|
||||
escolha: { problemaId: problema.id, dificuldade: "normal" },
|
||||
semente: 7,
|
||||
piloto: true,
|
||||
velocidade: 8,
|
||||
});
|
||||
expect(
|
||||
screen.queryByRole("heading", { name: "Expediente dos Mortos" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("no toque, em retrato, pede para virar o aparelho e pausa", async () => {
|
||||
midias["(pointer: coarse)"] = true;
|
||||
midias["(orientation: portrait)"] = true;
|
||||
render(<Jogo />);
|
||||
await comecar();
|
||||
expect(
|
||||
await screen.findByText("Vire o aparelho para jogar"),
|
||||
).toBeInTheDocument();
|
||||
expect(cenas[0].pausar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continuar depois da pausa libera o som de novo, no mesmo clique", async () => {
|
||||
render(<Jogo />);
|
||||
const usuario = await comecar();
|
||||
await waitFor(() => expect(cenas).toHaveLength(1));
|
||||
act(() => opcoes().aoRetrato({ ...retrato, pausado: true }));
|
||||
som().liberar.mockClear();
|
||||
await usuario.click(
|
||||
await screen.findByRole("button", { name: "Continuar" }),
|
||||
);
|
||||
expect(som().liberar).toHaveBeenCalledTimes(1);
|
||||
expect(cenas[0].continuar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a cena sabe que foi cancelada quando o jogador sai durante a carga", async () => {
|
||||
const { unmount } = render(<Jogo />);
|
||||
await comecar();
|
||||
expect(opcoes().cancelada()).toBe(false);
|
||||
unmount();
|
||||
expect(opcoes().cancelada()).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user