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,192 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join, relative } from "node:path";
|
||||
|
||||
const RAIZ = process.cwd();
|
||||
const PASTA = join(RAIZ, "public/jogo");
|
||||
const BLOCO = join(RAIZ, "public/3d/bloco.glb");
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
function arquivos(pasta: string): string[] {
|
||||
if (!existsSync(pasta)) return [];
|
||||
return readdirSync(pasta, { withFileTypes: true }).flatMap((e) =>
|
||||
e.isDirectory() ? arquivos(join(pasta, e.name)) : [join(pasta, e.name)],
|
||||
);
|
||||
}
|
||||
|
||||
const nome = (caminho: string) => relative(RAIZ, caminho);
|
||||
const doJogo = arquivos(PASTA).filter((a) => !a.endsWith("CREDITOS.json"));
|
||||
const glbs = [...doJogo, BLOCO].filter((a) => a.endsWith(".glb"));
|
||||
|
||||
/** Maior lado de um JPEG, PNG ou WebP, lido do cabeçalho; `null` se não for nenhum deles. */
|
||||
function lado(b: Buffer): number | null {
|
||||
if (
|
||||
b.length > 30 &&
|
||||
b.toString("latin1", 0, 4) === "RIFF" &&
|
||||
b.toString("latin1", 8, 15) === "WEBPVP8"
|
||||
) {
|
||||
const tipo = b.toString("latin1", 15, 16);
|
||||
if (tipo === "X")
|
||||
return Math.max(b.readUIntLE(24, 3), b.readUIntLE(27, 3)) + 1;
|
||||
if (tipo === " ")
|
||||
return Math.max(b.readUInt16LE(26) & 0x3fff, b.readUInt16LE(28) & 0x3fff);
|
||||
if (tipo === "L") {
|
||||
const v = b.readUInt32LE(21);
|
||||
return Math.max((v & 0x3fff) + 1, ((v >> 14) & 0x3fff) + 1);
|
||||
}
|
||||
}
|
||||
if (b.length > 24 && b.readUInt32BE(0) === 0x89504e47)
|
||||
return Math.max(b.readUInt32BE(16), b.readUInt32BE(20));
|
||||
if (b[0] === 0xff && b[1] === 0xd8) {
|
||||
let i = 2;
|
||||
while (i + 9 < b.length) {
|
||||
if (b[i] !== 0xff) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
const marcador = b[i + 1];
|
||||
if (marcador === 0xff) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (marcador === 0xd8 || (marcador >= 0xd0 && marcador <= 0xd7)) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const sof =
|
||||
marcador >= 0xc0 &&
|
||||
marcador <= 0xcf &&
|
||||
marcador !== 0xc4 &&
|
||||
marcador !== 0xc8 &&
|
||||
marcador !== 0xcc;
|
||||
if (sof) return Math.max(b.readUInt16BE(i + 5), b.readUInt16BE(i + 7));
|
||||
i += 2 + b.readUInt16BE(i + 2);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type Gltf = {
|
||||
accessors?: Array<{ count: number }>;
|
||||
meshes?: Array<{ primitives: Array<{ indices?: number }> }>;
|
||||
images?: Array<{ bufferView?: number; uri?: string }>;
|
||||
bufferViews?: Array<{ byteOffset?: number; byteLength: number }>;
|
||||
};
|
||||
|
||||
function lerGlb(caminho: string) {
|
||||
const b = readFileSync(caminho);
|
||||
const tamanhoJson = b.readUInt32LE(12);
|
||||
const json = JSON.parse(
|
||||
b.subarray(20, 20 + tamanhoJson).toString("utf8"),
|
||||
) as Gltf;
|
||||
const fimJson = 20 + tamanhoJson;
|
||||
const bin =
|
||||
fimJson + 8 <= b.length
|
||||
? b.subarray(fimJson + 8, fimJson + 8 + b.readUInt32LE(fimJson))
|
||||
: Buffer.alloc(0);
|
||||
return { json, bin };
|
||||
}
|
||||
|
||||
describe("orçamento de peso do jogo", () => {
|
||||
it("public/jogo + bloco.glb somam até 6 MB", () => {
|
||||
const total = [...doJogo, BLOCO].reduce((s, a) => s + statSync(a).size, 0);
|
||||
expect({
|
||||
total: `${(total / MB).toFixed(2)} MB`,
|
||||
ok: total <= 6 * MB,
|
||||
}).toEqual({ total: `${(total / MB).toFixed(2)} MB`, ok: true });
|
||||
});
|
||||
|
||||
it.each(glbs.map((g) => [nome(g), g]))("%s tem até 1,5 MB", (n, g) => {
|
||||
const tamanho = statSync(g).size;
|
||||
expect({
|
||||
arquivo: n,
|
||||
MB: +(tamanho / MB).toFixed(2),
|
||||
ok: tamanho <= 1.5 * MB,
|
||||
}).toEqual({ arquivo: n, MB: +(tamanho / MB).toFixed(2), ok: true });
|
||||
});
|
||||
|
||||
it("zumbi.glb tem até 12.000 triângulos", () => {
|
||||
const { json } = lerGlb(join(PASTA, "zumbi.glb"));
|
||||
let triangulos = 0;
|
||||
for (const malha of json.meshes ?? [])
|
||||
for (const p of malha.primitives)
|
||||
if (p.indices !== undefined)
|
||||
triangulos += (json.accessors?.[p.indices]?.count ?? 0) / 3;
|
||||
expect({ arquivo: "public/jogo/zumbi.glb", triangulos }).toEqual({
|
||||
arquivo: "public/jogo/zumbi.glb",
|
||||
triangulos: Math.min(triangulos, 12000),
|
||||
});
|
||||
});
|
||||
|
||||
it("imagens de public/jogo têm lado até 1024 px", () => {
|
||||
for (const a of doJogo.filter(
|
||||
(f) => /\.(jpe?g|png|webp)$/i.test(f) && !f.endsWith("capa.jpg"),
|
||||
)) {
|
||||
const l = lado(readFileSync(a));
|
||||
expect({ arquivo: nome(a), lado: l }).toEqual({
|
||||
arquivo: nome(a),
|
||||
lado: Math.min(l ?? 0, 1024),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("itens.webp tem 512 px de lado e até 120 KB", () => {
|
||||
const a = join(PASTA, "itens.webp");
|
||||
const kb = statSync(a).size / 1024;
|
||||
expect({
|
||||
arquivo: "public/jogo/itens.webp",
|
||||
lado: lado(readFileSync(a)),
|
||||
ok: kb <= 120,
|
||||
}).toEqual({ arquivo: "public/jogo/itens.webp", lado: 512, ok: true });
|
||||
});
|
||||
|
||||
it("capa.jpg tem lado até 1600 px e até 220 KB", () => {
|
||||
const a = join(PASTA, "capa.jpg");
|
||||
const l = lado(readFileSync(a));
|
||||
const kb = statSync(a).size / 1024;
|
||||
expect({
|
||||
arquivo: "public/jogo/capa.jpg",
|
||||
lado: l,
|
||||
KB: Math.round(kb),
|
||||
}).toEqual({
|
||||
arquivo: "public/jogo/capa.jpg",
|
||||
lado: Math.min(l ?? 0, 1600),
|
||||
KB: Math.min(Math.round(kb), 220),
|
||||
});
|
||||
});
|
||||
|
||||
it("imagens embutidas nos GLB têm lado até 1024 px", () => {
|
||||
for (const g of glbs) {
|
||||
const { json, bin } = lerGlb(g);
|
||||
(json.images ?? []).forEach((img, i) => {
|
||||
let dados: Buffer | null = null;
|
||||
if (img.bufferView !== undefined) {
|
||||
const v = json.bufferViews?.[img.bufferView];
|
||||
if (v)
|
||||
dados = bin.subarray(
|
||||
v.byteOffset ?? 0,
|
||||
(v.byteOffset ?? 0) + v.byteLength,
|
||||
);
|
||||
} else if (img.uri?.startsWith("data:"))
|
||||
dados = Buffer.from(img.uri.split(",")[1] ?? "", "base64");
|
||||
const l = dados ? lado(dados) : null;
|
||||
const rotulo = `${nome(g)}#imagem${i}`;
|
||||
expect({ arquivo: rotulo, lado: l }).toEqual({
|
||||
arquivo: rotulo,
|
||||
lado: Math.min(l ?? 0, 1024),
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("cada .mp3 de public/jogo/sons tem até 60 KB", () => {
|
||||
for (const a of arquivos(join(PASTA, "sons")).filter((f) =>
|
||||
f.endsWith(".mp3"),
|
||||
)) {
|
||||
const kb = statSync(a).size / 1024;
|
||||
expect({ arquivo: nome(a), KB: Math.round(kb) }).toEqual({
|
||||
arquivo: nome(a),
|
||||
KB: Math.min(Math.round(kb), 60),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user