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
+121
View File
@@ -0,0 +1,121 @@
/* ============================================================
* tools/make_icons.mjs — generate PNG icons with zero deps.
* Pixel-math art: hellfire sigil on obsidian.
* node tools/make_icons.mjs
* ============================================================ */
import zlib from 'node:zlib';
import fs from 'node:fs';
import path from 'node:path';
/* ---------- minimal PNG encoder (RGBA8, filter 0) ---------- */
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;
}
function chunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, crc]);
}
function encodePNG(w, h, rgba) {
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(w, 0);
ihdr.writeUInt32BE(h, 4);
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
const stride = w * 4;
const raw = Buffer.alloc(h * (stride + 1));
for (let y = 0; y < h; y++) {
raw[y * (stride + 1)] = 0;
rgba.copy(raw, y * (stride + 1) + 1, y * stride, (y + 1) * stride);
}
return Buffer.concat([
sig,
chunk('IHDR', ihdr),
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
chunk('IEND', Buffer.alloc(0)),
]);
}
/* ---------- sigil renderer (SDF-ish per pixel) ---------- */
function drawIcon(S) {
const px = Buffer.alloc(S * S * 4);
const cx = S / 2, cy = S / 2;
const put = (x, y, r, g, b, a = 255) => {
const i = (y * S + x) * 4;
px[i] = r; px[i + 1] = g; px[i + 2] = b; px[i + 3] = a;
};
const mix = (x, y, r, g, b, alpha) => {
const i = (y * S + x) * 4;
px[i] = px[i] * (1 - alpha) + r * alpha;
px[i + 1] = px[i + 1] * (1 - alpha) + g * alpha;
px[i + 2] = px[i + 2] * (1 - alpha) + b * alpha;
px[i + 3] = 255;
};
for (let y = 0; y < S; y++) {
for (let x = 0; x < S; x++) {
const nx = (x - cx) / (S / 2), ny = (y - cy) / (S / 2);
const rad = Math.hypot(nx, ny);
/* obsidian background with vignette */
let r = 13, g = 10, b = 9;
const vg = Math.max(0, 1 - rad * 0.85);
r += 26 * vg; g += 18 * vg; b += 14 * vg;
/* outer gold ring */
const ring = Math.abs(rad - 0.86);
if (ring < 0.045) {
const edge = 1 - ring / 0.045;
mix(x, y, 200, 163, 90, 0.55 + 0.45 * edge);
}
/* hellfire pentagram star (5-point, drawn via angular falloff) */
const ang = Math.atan2(ny, nx) - Math.PI / 2;
const spikes = 5;
const starR = 0.34 + 0.30 * Math.pow(Math.abs(Math.cos(spikes * ang / 2)), 3);
const d = rad - starR;
if (d < 0) {
const glow = Math.min(1, -d * 5);
const fire = Math.pow(glow, 0.6);
mix(x, y,
120 + 120 * fire,
24 + 60 * fire * (0.6 + 0.4 * Math.sin(ang * spikes)),
18,
Math.min(1, 0.35 + glow));
/* molten core line */
if (Math.abs(d) < 0.02 && rad > 0.12) mix(x, y, 255, 214, 130, 0.75);
}
/* center ember */
const ember = Math.hypot(nx * 2.4, ny * 2.4);
if (ember < 1) {
const e = 1 - ember;
mix(x, y, 255, 190 + 40 * e, 110, e * e);
}
put(x, y, px[(y * S + x) * 4], px[(y * S + x) * 4 + 1], px[(y * S + x) * 4 + 2], 255);
}
}
return encodePNG(S, S, px);
}
const outDir = path.join(process.cwd(), 'icons');
fs.mkdirSync(outDir, { recursive: true });
for (const size of [180, 192, 512]) {
fs.writeFileSync(path.join(outDir, `icon-${size}.png`), drawIcon(size));
console.log(`wrote icons/icon-${size}.png`);
}