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:
2026-08-23 06:59:36 +00:00
commit fc1fa2d51e
42 changed files with 11784 additions and 0 deletions
+76
View File
@@ -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`);