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.
193 lines
6.1 KiB
TypeScript
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),
|
|
});
|
|
}
|
|
});
|
|
});
|