Files
portifolio-evandrodevbr/lib/jogo/orcamento.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

193 lines
6.1 KiB
TypeScript

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),
});
}
});
});