feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s

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:
evandro committed 2026-11-10 19:37:10 -03:00
1 parent c7d09d63d4
commit 67b32fcab3
118 files changed
+16730

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
import { campoDeFluxo, direcaoNoCampo, lerMapa } from "../mapa";
import { AMBIENTES, ESCRITORIO, PORTAS } from "./escritorio";
const MAPA = lerMapa(ESCRITORIO);
const { largura, altura } = MAPA;
const andavel = (bloqueio: Uint8Array, x: number, z: number) =>
x >= 0 &&
z >= 0 &&
x < largura &&
z < altura &&
bloqueio[z * largura + x] === 0;
/** Quantas células andáveis a busca em largura alcança a partir do início. */
function alcancaveis(bloqueio: Uint8Array): number {
const inicio =
Math.floor(MAPA.inicio[1]) * largura + Math.floor(MAPA.inicio[0]);
const visto = new Uint8Array(largura * altura);
const fila = [inicio];
visto[inicio] = 1;
for (let k = 0; k < fila.length; k++) {
const x = fila[k] % largura;
const z = Math.floor(fila[k] / largura);
for (const [dx, dz] of [
[1, 0],
[-1, 0],
[0, 1],
[0, -1],
]) {
const i = (z + dz) * largura + x + dx;
if (andavel(bloqueio, x + dx, z + dz) && !visto[i]) {
visto[i] = 1;
fila.push(i);
}
}
}
return fila.length;
}
const totalAndavel = MAPA.solido.filter((c) => c === 0).length;
describe("mapa do escritório", () => {
it("tem pelo menos 40 × 28 células e é lido por lerMapa", () => {
expect(largura).toBeGreaterThanOrEqual(40);
expect(altura).toBeGreaterThanOrEqual(28);
expect(() => lerMapa(ESCRITORIO)).not.toThrow();
});
it("tem os 7 ambientes com nome", () => {
const nomes = AMBIENTES.map((a) => a.nome);
for (const nome of [
"recepção",
"área de mesas",
"sala de reunião",
"copa",
"sala do servidor",
"diretoria",
"almoxarifado",
]) {
expect(nomes).toContain(nome);
}
});
it("tem 6 ou mais nascedouros, todos a 8 m ou mais do início", () => {
expect(MAPA.nascedouros.length).toBeGreaterThanOrEqual(6);
for (const [x, z] of MAPA.nascedouros) {
expect(
Math.hypot(x - MAPA.inicio[0], z - MAPA.inicio[1]),
).toBeGreaterThanOrEqual(8);
}
});
it("alcança todo o chão a partir do início", () => {
expect(alcancaveis(MAPA.solido)).toBe(totalAndavel);
});
it("continua alcançando todo o chão sem qualquer uma das portas", () => {
expect(PORTAS.length).toBeGreaterThan(0);
for (const porta of PORTAS) {
const bloqueio = MAPA.solido.slice();
for (let z = porta.z; z < porta.z + porta.altura; z++) {
for (let x = porta.x; x < porta.x + porta.largura; x++) {
expect(andavel(bloqueio, x, z)).toBe(true);
bloqueio[z * largura + x] = 1;
}
}
expect(alcancaveis(bloqueio)).toBe(
totalAndavel - porta.largura * porta.altura,
);
}
});
it("não tem passagem com menos de 2 células: toda célula andável está num 2 × 2 andável", () => {
const livre = (x: number, z: number) => andavel(MAPA.solido, x, z);
for (let z = 0; z < altura; z++) {
for (let x = 0; x < largura; x++) {
if (!livre(x, z)) continue;
const emQuadrado = [
[-1, -1],
[-1, 0],
[0, -1],
[0, 0],
].some(
([ox, oz]) =>
livre(x + ox, z + oz) &&
livre(x + ox + 1, z + oz) &&
livre(x + ox, z + oz + 1) &&
livre(x + ox + 1, z + oz + 1),
);
expect([x, z, emQuadrado]).toEqual([x, z, true]);
}
}
});
it("o campo de fluxo leva cada nascedouro ao início em menos de 400 passos", () => {
const campo = campoDeFluxo(MAPA, MAPA.inicio[0], MAPA.inicio[1]);
const alvo = [Math.floor(MAPA.inicio[0]), Math.floor(MAPA.inicio[1])];
for (const [nx, nz] of MAPA.nascedouros) {
let x = Math.floor(nx);
let z = Math.floor(nz);
let passos = 0;
while ((x !== alvo[0] || z !== alvo[1]) && passos < 400) {
const [dx, dz] = direcaoNoCampo(MAPA, campo, x + 0.5, z + 0.5);
x += Math.round(dx);
z += Math.round(dz);
passos++;
}
expect([x, z]).toEqual(alvo);
expect(passos).toBeLessThan(400);
}
});
});
+89
View File
@@ -0,0 +1,89 @@
// Gerado por `bash 3d/mapa.sh` (`python3 3d/mapa.py converter`) a partir de 3d/mapa/escritorio.tmx: não edite à mão.
/** Planta do escritório para `lerMapa`: `#` parede ou móvel alto, `M` móvel baixo, `.` chão, `J` início, `N` nascedouro. */
export const ESCRITORIO: readonly string[] = [
"############################################",
"#..MMM.........#............#MMM..........##",
"#..............#............#.............##",
"#M......MMMM...#..MMMMMMMM.........MMMM...##",
"#M.............#..MMMMMMMM.........MMMM...##",
"#M.....J.......#............#..............#",
"#M.............#............#..............#",
"####..######..####..####..####..############",
"#........#........................#........#",
"#........#........................#........#",
"#........#..#########..#####..##..#..#######",
"#M..MM......#.................##...........#",
"#M..MM......#.................##...........#",
"#M.......#..#..MMMMM...MMMMM..##..#..#######",
"#M.......#..#..MMMMM...MMMMM...#..#........#",
"#MMMM..###........................#........#",
"##########........................####..####",
"###..MMMM#..#..MMMMM...MMMMM...#..#.......##",
"###......#..#..MMMMM...MMMMM...#..#.......##",
"###.........#..................#..........##",
"###.........#..................#.....###...#",
"###...N..#..#########..#########..#........#",
"#........#........................#......N.#",
"#........#........................###......#",
"####..#######..#####..######..########..####",
"#................#......#..................#",
"#................#......#..................#",
"#.N....N.............####...............MMM#",
"#....................####...............MMM#",
"####..###........#.N.####.N................#",
"####..###..MMMM..#...####..######........N.#",
"############################################",
];
/** Salas com nome, em células (x para a direita, z para baixo). */
export const AMBIENTES: readonly {
nome: string;
x: number;
z: number;
largura: number;
altura: number;
}[] = [
{ nome: "recepção", x: 1, z: 1, largura: 14, altura: 6 },
{ nome: "sala de reunião", x: 16, z: 1, largura: 12, altura: 6 },
{ nome: "diretoria", x: 29, z: 1, largura: 14, altura: 6 },
{ nome: "copa", x: 1, z: 8, largura: 8, altura: 8 },
{ nome: "banheiros", x: 1, z: 17, largura: 8, altura: 7 },
{ nome: "área de mesas", x: 13, z: 11, largura: 18, altura: 10 },
{ nome: "sala do servidor", x: 35, z: 8, largura: 8, altura: 8 },
{ nome: "almoxarifado", x: 35, z: 17, largura: 8, altura: 7 },
{ nome: "hall dos elevadores", x: 1, z: 25, largura: 16, altura: 6 },
{ nome: "escada", x: 18, z: 25, largura: 6, altura: 6 },
{ nome: "área de serviço", x: 25, z: 25, largura: 18, altura: 6 },
];
/** Células abertas nas paredes, em células. */
export const PORTAS: readonly {
x: number;
z: number;
largura: number;
altura: number;
}[] = [
{ x: 4, z: 7, largura: 2, altura: 1 },
{ x: 12, z: 7, largura: 2, altura: 1 },
{ x: 18, z: 7, largura: 2, altura: 1 },
{ x: 24, z: 7, largura: 2, altura: 1 },
{ x: 28, z: 3, largura: 1, altura: 2 },
{ x: 30, z: 7, largura: 2, altura: 1 },
{ x: 9, z: 11, largura: 1, altura: 2 },
{ x: 9, z: 19, largura: 1, altura: 2 },
{ x: 4, z: 24, largura: 2, altura: 1 },
{ x: 13, z: 24, largura: 2, altura: 1 },
{ x: 17, z: 27, largura: 1, altura: 2 },
{ x: 20, z: 24, largura: 2, altura: 1 },
{ x: 28, z: 24, largura: 2, altura: 1 },
{ x: 38, z: 24, largura: 2, altura: 1 },
{ x: 34, z: 19, largura: 1, altura: 2 },
{ x: 38, z: 16, largura: 2, altura: 1 },
{ x: 34, z: 11, largura: 1, altura: 2 },
{ x: 21, z: 10, largura: 2, altura: 1 },
{ x: 28, z: 10, largura: 2, altura: 1 },
{ x: 21, z: 21, largura: 2, altura: 1 },
{ x: 12, z: 15, largura: 1, altura: 2 },
{ x: 31, z: 15, largura: 1, altura: 2 },
];