import { deflateSync } from "node:zlib" // Procedurally draw the placeholder textures and write them to /assets as PNGs. // One-shot dev tool: `bun run assets`. Swap the output files for real art // anytime; the filenames are the contract the game loads. const CRC_TABLE = buildCrcTable() function buildCrcTable(): Uint32Array { const table = new Uint32Array(256) for (let n = 0; n < 256; n++) { let c = n for (let k = 0; k < 8; k++) { c = (c & 1) === 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1 } table[n] = c >>> 0 } return table } function crc32(bytes: Uint8Array): number { let c = 0xFFFFFFFF for (let i = 0; i < bytes.length; i++) { c = CRC_TABLE[(c ^ bytes[i]) & 0xFF] ^ (c >>> 8) } return (c ^ 0xFFFFFFFF) >>> 0 } function chunk(type: string, data: Uint8Array): Uint8Array { const body = new Uint8Array(4 + data.length) for (let i = 0; i < 4; i++) { body[i] = type.charCodeAt(i) } body.set(data, 4) const out = new Uint8Array(8 + body.length) const view = new DataView(out.buffer) view.setUint32(0, data.length) out.set(body, 4) view.setUint32(4 + body.length, crc32(body)) return out } function concat(parts: Uint8Array[]): Uint8Array { const total = parts.reduce((n, p) => n + p.length, 0) const out = new Uint8Array(total) let offset = 0 for (const part of parts) { out.set(part, offset) offset += part.length } return out } /** Encode 8-bit RGBA pixels as a PNG byte stream. */ function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array { const signature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]) const ihdr = new Uint8Array(13) const view = new DataView(ihdr.buffer) view.setUint32(0, width) view.setUint32(4, height) ihdr[8] = 8 // bit depth ihdr[9] = 6 // color type: RGBA const stride = width * 4 const raw = new Uint8Array((stride + 1) * height) for (let y = 0; y < height; y++) { raw[y * (stride + 1)] = 0 // filter type: none raw.set(rgba.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1) } const idat = new Uint8Array(deflateSync(raw)) return concat([signature, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", new Uint8Array(0))]) } type Shade = (x: number, y: number) => [number, number, number, number] function draw(width: number, height: number, shade: Shade): Uint8Array { const rgba = new Uint8Array(width * height * 4) for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const [r, g, b, a] = shade(x, y) const i = (y * width + x) * 4 rgba[i] = clamp(r) rgba[i + 1] = clamp(g) rgba[i + 2] = clamp(b) rgba[i + 3] = clamp(a) } } return rgba } function clamp(v: number): number { return Math.max(0, Math.min(255, Math.round(v))) } /** Deterministic value noise in roughly [-1, 1] from integer coords. */ function noise(x: number, y: number): number { const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453 return (n - Math.floor(n)) * 2 - 1 } // --- Textures ------------------------------------------------------------ const floor: Shade = (x, y) => { const grout = x % 32 < 2 || y % 32 < 2 if (grout) { return [58, 58, 70, 255] } const n = noise(x, y) * 14 return [132 + n, 130 + n, 120 + n, 255] } // Outdoor ground. Green, busy, with faux vertical blades and soft patches, kept // free of strong low-frequency features so it tiles across the terrain without an // obvious repeating grid. const grass: Shade = (x, y) => { const n = noise(x, y) * 18 const blade = noise(x, y * 3) * 8 const patch = noise(Math.floor(x / 8), Math.floor(y / 8)) * 14 return [56 + n * 0.7 + patch * 0.5, 116 + n + blade + patch, 50 + n * 0.5 + patch * 0.4, 255] } const wall: Shade = (x, y) => { const row = Math.floor(y / 16) const bx = (x + (row % 2) * 16) % 32 const mortar = y % 16 < 2 || bx < 2 if (mortar) { return [48, 44, 44, 255] } const n = noise(x, y) * 12 return [150 + n, 72 + n, 56 + n, 255] } const crate: Shade = (x, y) => { const edge = x < 3 || x > 60 || y < 3 || y > 60 const band = (x > 29 && x < 35) || (y > 29 && y < 35) const bolt = (x - 8) ** 2 + (y - 8) ** 2 < 6 || (x - 55) ** 2 + (y - 55) ** 2 < 6 const n = noise(x, y) * 10 + Math.sin(y * 0.5) * 8 if (bolt) { return [60, 46, 26, 255] } if (edge || band) { return [96, 62, 30, 255] } return [140 + n, 96 + n, 46 + n, 255] } // 48x64, transparent background, a simple round-topped figure with eyes. const npc: Shade = (x, y) => { const dx = (x - 24) / 17 const dy = (y - 34) / 24 const d = dx * dx + dy * dy if (d > 1) { return [0, 0, 0, 0] } const outline = d > 0.82 const eye = (x - 17) ** 2 + (y - 28) ** 2 < 9 || (x - 31) ** 2 + (y - 28) ** 2 < 9 const pupil = (x - 17) ** 2 + (y - 29) ** 2 < 2 || (x - 31) ** 2 + (y - 29) ** 2 < 2 if (pupil) { return [20, 20, 30, 255] } if (eye) { return [235, 235, 240, 255] } if (outline) { return [30, 70, 50, 255] } const n = noise(x, y) * 12 return [70 + n, 165 + n, 110 + n, 255] } // --- Write --------------------------------------------------------------- const assets: Array<[string, number, number, Shade]> = [ ["floor", 64, 64, floor], ["grass", 64, 64, grass], ["wall", 64, 64, wall], ["crate", 64, 64, crate], ["npc", 48, 64, npc], ] for (const [name, w, h, shade] of assets) { const png = encodePng(w, h, draw(w, h, shade)) await Bun.write(`assets/${name}.png`, png) console.log(`assets/${name}.png (${w}x${h}, ${png.length} bytes)`) }