Files
portifolio-evandrodevbr/app/jogo/atores.test.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

269 lines
8.3 KiB
TypeScript

import {
BoxGeometry,
Group,
Mesh,
MeshStandardMaterial,
type Object3D,
Scene,
Sprite,
Texture,
} from "three";
import { criarPartida } from "@/lib/jogo/partida";
import type { Partida, TipoZumbi, Zumbi } from "@/lib/jogo/tipos";
import { criarAtores } from "./atores";
import type { ModeloDoZumbi } from "./zumbis3d";
// O zumbis3d de verdade importa o GLTFLoader (ESM, que o Jest não transforma); aqui o clone é um `clone()` simples.
const mockTocados: string[] = [];
jest.mock("./zumbis3d", () => ({
criarZumbi: (modelo: ModeloDoZumbi) => {
const raiz = modelo.cena.clone();
return {
raiz,
tocar: (clipe: string, opcoes?: { umaVez?: boolean }) => {
mockTocados.push(opcoes?.umaVez ? `${clipe} uma vez` : clipe);
},
avancar: () => {},
descartar: () => raiz.removeFromParent(),
};
},
}));
/** Zumbi mínimo com as três peças do modelo. */
function modeloFalso(): ModeloDoZumbi {
const cena = new Group();
cena.name = "zumbi";
for (const nome of ["corpo", "roupa", "olhos"]) {
cena.add(
new Mesh(new BoxGeometry(), new MeshStandardMaterial({ name: nome })),
);
}
return { cena, clipes: [] };
}
function zumbi(id: number, x: number, z: number, tipo: TipoZumbi = "comum") {
const novo: Zumbi = {
id,
tipo,
problemaId: "site-lento",
x,
z,
rumo: 0,
vida: 100,
estado: "andando",
tempo: 0,
};
return novo;
}
function partidaCom(zumbis: Zumbi[], passo: number): Partida {
const p = criarPartida({
semente: 1,
dificuldade: "facil",
problemaId: "site-lento",
});
p.zumbis = zumbis;
p.passo = passo;
return p;
}
const ehZumbi = (o: Object3D) => o.name === "zumbi";
function zumbisNaCena(cena: Scene) {
const todos = cena.children.filter(ehZumbi);
return { total: todos.length, visiveis: todos.filter((o) => o.visible) };
}
beforeEach(() => {
jest.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
});
afterEach(() => {
jest.restoreAllMocks();
});
function montar() {
const cena = new Scene();
const atores = criarAtores(cena, {
zumbi: modeloFalso(),
bloco: new BoxGeometry(),
roupas: [new Texture(), new Texture()],
});
return { cena, atores };
}
test("cria, esconde e reaproveita zumbis sem a reserva passar do maior grupo", () => {
const { cena, atores } = montar();
const cinco = [1, 2, 3, 4, 5].map((id) => zumbi(id, 2 + id, 2));
atores.sincronizar(partidaCom(cinco, 1), 1, 0.016);
expect(zumbisNaCena(cena).total).toBe(5);
expect(zumbisNaCena(cena).visiveis).toHaveLength(5);
atores.sincronizar(partidaCom(cinco.slice(0, 2), 2), 1, 0.016);
expect(zumbisNaCena(cena).total).toBe(5);
expect(zumbisNaCena(cena).visiveis).toHaveLength(2);
const seis = [1, 2, 6, 7, 8, 9].map((id) => zumbi(id, 2 + id, 3));
atores.sincronizar(partidaCom(seis, 3), 1, 0.016);
const { total, visiveis } = zumbisNaCena(cena);
expect(total).toBe(6);
expect(visiveis).toHaveLength(6);
expect(visiveis.map((o) => o.position.x).sort((a, b) => a - b)).toEqual([
3, 4, 8, 9, 10, 11,
]);
});
test("toca o clipe do estado e veste roupa e escala do tipo", () => {
const { cena, atores } = montar();
mockTocados.length = 0;
const rapido = zumbi(1, 3, 3, "rapido");
const comum = zumbi(2, 4, 3);
const blindado = zumbi(3, 5, 3, "blindado");
atores.sincronizar(partidaCom([rapido, comum, blindado], 1), 1, 0);
expect(mockTocados).toEqual(["correr", "andar", "andar"]);
atores.sincronizar(
partidaCom(
[
{ ...rapido, estado: "atacando" },
{ ...comum, estado: "morrendo" },
blindado,
],
2,
),
1,
0,
);
expect(mockTocados.slice(3)).toEqual(["atacar", "morrer uma vez"]);
const [r, c, b] = cena.children.filter(ehZumbi);
const roupa = (o: Object3D) =>
(o.children[1] as Mesh).material as MeshStandardMaterial;
expect(r.scale.x).toBeCloseTo(0.95);
expect(b.scale.x).toBeCloseTo(1.08);
// ids 1, 2 e 3: roupa B, roupa C e a original escurecida e metálica
expect(new Set([roupa(r), roupa(c), roupa(b)]).size).toBe(3);
expect(roupa(b).metalness).toBeGreaterThan(0.5);
});
test("põe o zumbi no lugar e no rumo da lógica, interpolando entre passos", () => {
const { cena, atores } = montar();
const z = zumbi(1, 4, 4);
atores.sincronizar(partidaCom([z], 1), 1, 0);
const p2 = partidaCom([{ ...z, x: 5, rumo: 0.5 }], 2);
atores.guardarPasso(p2);
atores.sincronizar(p2, 0.5, 0);
const raiz = cena.children.find(ehZumbi);
expect(raiz?.position.x).toBeCloseTo(4.5);
expect(raiz?.position.z).toBeCloseTo(4);
expect(raiz?.rotation.y).toBeCloseTo(0.25);
});
test("dois passos no mesmo quadro: interpola entre o penúltimo e o último", () => {
const { cena, atores } = montar();
const z = zumbi(1, 4, 4);
atores.sincronizar(partidaCom([z], 1), 1, 0);
atores.guardarPasso(partidaCom([{ ...z, x: 5 }], 2));
const p3 = partidaCom([{ ...z, x: 6 }], 3);
atores.guardarPasso(p3);
atores.sincronizar(p3, 0.5, 0);
expect(cena.children.find(ehZumbi)?.position.x).toBeCloseTo(5.5);
});
test("com a partida parada, antes vira agora e nada treme", () => {
const { cena, atores } = montar();
const z = zumbi(1, 4, 4);
const drone = {
id: 9,
problemaId: "site-lento",
x: 13,
z: 9,
rumo: 0,
recarga: 0,
eliminacoes: 0,
};
const p1 = partidaCom([z], 1);
p1.drones = [drone];
atores.sincronizar(p1, 1, 0);
const p2 = partidaCom([{ ...z, x: 5 }], 2);
p2.drones = [{ ...drone, x: 14 }];
atores.guardarPasso(p2);
// terminada, a lógica não avança `passo`, mas a cena segue chamando a cada passo
atores.guardarPasso(p2);
for (const alfa of [0, 0.3, 0.9]) {
atores.sincronizar(p2, alfa, 0);
expect(cena.children.find(ehZumbi)?.position.x).toBeCloseTo(5);
const doDrone = cena.children.find(
(o) => o instanceof Sprite && o.name === "Site rápido",
);
expect(doDrone?.position.x).toBeCloseTo(14);
}
});
test("o rótulo do zumbi é o nome do problema e só aparece a até 10 m, menos no chefão", () => {
const { cena, atores } = montar();
// o jogador começa no centro do escritório (13,5; 8,5)
const perto = zumbi(1, 13.5, 5);
const longe = zumbi(2, 1.5, 1.5);
const chefao = zumbi(3, 26.5, 15.5, "chefao");
atores.sincronizar(partidaCom([perto, longe, chefao], 1), 1, 0);
const rotulos = cena.children.filter(
(o): o is Sprite => o instanceof Sprite && o.name !== "",
);
const em = (x: number) => rotulos.find((s) => s.position.x === x);
expect(em(13.5)?.name).toBe("Site lento");
expect(em(13.5)?.visible).toBe(true);
expect(em(1.5)?.visible).toBe(false);
expect(em(26.5)?.visible).toBe(true);
expect(em(26.5)?.material.depthTest).toBe(true);
});
test("só os 3 zumbis mais perto e o chefão levam rótulo, do mesmo tamanho na tela a qualquer distância", () => {
const { cena, atores } = montar();
const zumbis = [2, 3, 4, 5, 6].map((d, i) => zumbi(i + 1, 13.5, 8.5 + d));
zumbis.push(zumbi(9, 26.5, 15.5, "chefao"));
atores.sincronizar(partidaCom(zumbis, 1), 1, 0);
const rotulos = cena.children.filter(
(o): o is Sprite => o instanceof Sprite && o.visible,
);
expect(rotulos.map((s) => s.position.z).sort((a, b) => a - b)).toEqual([
10.5, 11.5, 12.5, 15.5,
]);
for (const s of rotulos) {
expect(s.material.sizeAttenuation).toBe(false);
expect(s.material.depthTest).toBe(true);
}
expect(rotulos[0].scale.y).toBeCloseTo(rotulos[1].scale.y, 10);
});
test("o drone leva o nome da solução, acima da cabeça e em rótulo menor que o do zumbi", () => {
const { cena, atores } = montar();
const p = partidaCom([zumbi(1, 13.5, 5)], 1);
p.drones = [
{
id: 9,
problemaId: "site-lento",
x: 13,
z: 9,
rumo: 0,
recarga: 0,
eliminacoes: 0,
},
];
atores.sincronizar(p, 1, 0);
const visivel = (nome: string) =>
cena.children.find(
(o): o is Sprite => o instanceof Sprite && o.visible && o.name === nome,
);
const doDrone = visivel("Site rápido");
expect(doDrone?.position.y).toBeGreaterThan(2.3);
expect(doDrone?.scale.y).toBeLessThan(visivel("Site lento")?.scale.y ?? 0);
expect(doDrone?.material.depthTest).toBe(false);
});
test("descartar tira tudo da cena", () => {
const { cena, atores } = montar();
atores.sincronizar(partidaCom([zumbi(1, 3, 3)], 1), 1, 0);
atores.descartar();
expect(cena.children).toHaveLength(0);
});