neon-rooftops / tools /preview-frames.js
kingabzpro's picture
Anchor the fox by its snout and extend hit boxes to the nose
eb7f61a verified
Raw History Blame Contribute Delete
3.99 kB
// 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');