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,268 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user