- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
77 lines
2.6 KiB
JavaScript
77 lines
2.6 KiB
JavaScript
/* ============================================================
|
|
* tools/build_zip.mjs — package the game for itch.io.
|
|
* Store-only ZIP (no deps). Excludes tools/ and dev files.
|
|
* node tools/build_zip.mjs
|
|
* ============================================================ */
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const CRC_TABLE = (() => {
|
|
const t = new Int32Array(256);
|
|
for (let n = 0; n < 256; n++) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
t[n] = c;
|
|
}
|
|
return t;
|
|
})();
|
|
function crc32(buf) {
|
|
let c = 0xffffffff;
|
|
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
const ROOT = path.join(process.cwd());
|
|
const INCLUDE = ['index.html', 'manifest.webmanifest', 'LICENSE', 'README.md', 'server.js', 'css', 'js', 'icons'];
|
|
const EXCLUDE_DIR = new Set(['tools', 'node_modules', '.git']);
|
|
|
|
function walk(rel, out) {
|
|
const full = path.join(ROOT, rel);
|
|
const st = fs.statSync(full);
|
|
if (st.isDirectory()) {
|
|
if (EXCLUDE_DIR.has(path.basename(rel))) return;
|
|
for (const name of fs.readdirSync(full)) walk(path.join(rel, name), out);
|
|
} else {
|
|
out.push({ rel: rel.split(path.sep).join('/'), full });
|
|
}
|
|
}
|
|
const files = [];
|
|
for (const f of INCLUDE) if (fs.existsSync(path.join(ROOT, f))) walk(f, files);
|
|
|
|
/* ---- assemble store-only zip ---- */
|
|
const locals = [];
|
|
const centrals = [];
|
|
let offset = 0;
|
|
const u16 = v => { const b = Buffer.alloc(2); b.writeUInt16LE(v); return b; };
|
|
const u32 = v => { const b = Buffer.alloc(4); b.writeUInt32LE(v); return b; };
|
|
|
|
for (const f of files) {
|
|
const data = fs.readFileSync(f.full);
|
|
const name = Buffer.from(f.rel, 'utf8');
|
|
const crc = crc32(data);
|
|
|
|
const lh = Buffer.concat([
|
|
u32(0x04034b50), u16(20), u16(0), u16(0), u16(0x21), u16(0),
|
|
u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), name,
|
|
]);
|
|
locals.push(lh, data);
|
|
|
|
centrals.push(Buffer.concat([
|
|
u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0x21), u16(0),
|
|
u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0), u16(0),
|
|
u16(0), u16(0), u32(0), u32(offset), name,
|
|
]));
|
|
offset += lh.length + data.length;
|
|
}
|
|
|
|
const cdBuf = Buffer.concat(centrals);
|
|
const eocd = Buffer.concat([
|
|
u32(0x06054b50), u16(0), u16(0), u16(files.length), u16(files.length),
|
|
u32(cdBuf.length), u32(offset), u16(0),
|
|
]);
|
|
|
|
const zip = Buffer.concat([...locals, cdBuf, eocd]);
|
|
const outName = 'diablo2d-itch.zip';
|
|
fs.writeFileSync(path.join(ROOT, outName), zip);
|
|
console.log(`wrote ${outName}: ${files.length} files, ${(zip.length / 1024).toFixed(1)} KB`);
|