Diablo2D — Shadows of Tristram: complete browser ARPG
- 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
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/* ============================================================
|
||||
* 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`);
|
||||
Reference in New Issue
Block a user