// Render each fox frame magnified, with the anchor line and hit box overlaid, so // we can see whether they actually sit on the fox's body. // // node tools/preview-frames.js (uses the boxes in this file) 'use strict'; const fs = require('fs'), path = require('path'), zlib = require('zlib'); const { decodePNG } = require('./pngdec.js'); function crc32(buf) { let c, t = []; for (let n = 0; n < 256; n++) { c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } let crc = 0xffffffff; for (let i = 0; i < buf.length; i++) crc = t[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; } const 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 stride = w * 4, raw = Buffer.alloc((stride + 1) * h); for (let y = 0; y < h; y++) { raw[y * (stride + 1)] = 0; rgba.copy(raw, y * (stride + 1) + 1, y * stride, y * stride + stride); } const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6; return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]); } const { SHEET, FOX_BOX } = require('./game-constants.js'); const img = decodePNG(fs.readFileSync(path.join(__dirname, '..', 'assets', 'sprites.png'))); const { bpp, px } = img; const at = (x, y) => { const i = (y * img.w + x) * bpp; return [px[i], px[i + 1], px[i + 2], px[i + 3]]; }; const S = 8, PAD = 14, COLS = 3; // only the fox frames carry hit boxes; SHEET also holds the obstacles and tiles const names = Object.keys(FOX_BOX); const cellW = Math.max(...names.map(n => SHEET[n].w)) * S + PAD * 2; const cellH = Math.max(...names.map(n => SHEET[n].h)) * S + PAD * 2; const rows = Math.ceil(names.length / COLS); const W = COLS * cellW, H = rows * cellH; const out = Buffer.alloc(W * H * 4); for (let i = 0; i < W * H; i++) { out[i * 4] = 0x08; out[i * 4 + 1] = 0x10; out[i * 4 + 2] = 0x20; out[i * 4 + 3] = 255; } const set = (x, y, r, g, b) => { if (x < 0 || y < 0 || x >= W || y >= H) return; const o = (y * W + x) * 4; out[o] = r; out[o + 1] = g; out[o + 2] = b; out[o + 3] = 255; }; names.forEach((name, idx) => { const f = SHEET[name]; const ox = (idx % COLS) * cellW + PAD; const oy = Math.floor(idx / COLS) * cellH + PAD; // sprite for (let y = 0; y < f.h; y++) for (let x = 0; x < f.w; x++) { const c = at(f.x + x, f.y + y); if (c[3] < 128) continue; for (let sy = 0; sy < S; sy++) for (let sx = 0; sx < S; sx++) set(ox + x * S + sx, oy + y * S + sy, c[0], c[1], c[2]); } // anchor line (ax) in cyan, and the ground row (ay) in pink for (let y = 0; y < f.h * S; y++) for (let t = 0; t < 2; t++) set(ox + f.ax * S + t, oy + y, 0x00, 0xf0, 0xff); for (let x = -PAD; x < f.w * S + PAD; x++) for (let t = 0; t < 2; t++) { if (oy + f.ay * S + t < H) set(ox + x, oy + f.ay * S + t, 0xff, 0x2e, 0xc7); } // hit box in yellow for (const b of FOX_BOX[name]) { for (let x = b.x * S; x < (b.x + b.w) * S; x++) { set(ox + x, oy + b.y * S, 0xff, 0xd1, 0x66); set(ox + x, oy + (b.y + b.h) * S - 1, 0xff, 0xd1, 0x66); } for (let y = b.y * S; y < (b.y + b.h) * S; y++) { set(ox + b.x * S, oy + y, 0xff, 0xd1, 0x66); set(ox + (b.x + b.w) * S - 1, oy + y, 0xff, 0xd1, 0x66); } } console.log(`${name.padEnd(5)} frame ${f.w}x${f.h} anchor ax=${f.ax} ground ay=${f.ay} box=${JSON.stringify(FOX_BOX[name][0])}`); }); fs.writeFileSync(path.join(__dirname, 'frame-check.png'), encodePNG(W, H, out)); console.log(`\nwrote tools/frame-check.png ${W}x${H}`); console.log('cyan = anchor x pink = ground row yellow = hit box');