#!/usr/bin/env node /** * Placeholder asset generator. * * Produces minimal-but-valid binary files for every entry in * `assets/resources/ASSETS.md`, so the Cocos Creator project can be opened * and navigated end-to-end before the real art/audio pass. * * • PNG : 1×1 solid-color (distinct per category for visual debugging) * • WAV : 0.1 s of silence, mono, 8 kHz, 8-bit PCM (~ 1 KB) * • MP3 : minimal silent MP3 header frame * * Safe to re-run: files are only (re)written when their content differs or * the path does not exist yet. * * Usage: * node scripts/gen_placeholder_assets.js */ const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); // --------------------------------------------------------------------- // // PNG builder (1×1 RGBA solid color, no external deps) // // --------------------------------------------------------------------- // function makeSolidPng(r, g, b, a = 255) { const crcTable = (() => { const t = new Uint32Array(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 >>> 0; } return t; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = crcTable[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function chunk(type, data) { const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0); const typeBuf = Buffer.from(type, 'ascii'); const crcInput = Buffer.concat([typeBuf, data]); const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(crcInput), 0); return Buffer.concat([len, typeBuf, data, crc]); } const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(1, 0); // width ihdr.writeUInt32BE(1, 4); // height ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type RGBA ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const raw = Buffer.from([0x00, r & 0xff, g & 0xff, b & 0xff, a & 0xff]); // filter byte + pixel const idatData = zlib.deflateSync(raw); return Buffer.concat([ signature, chunk('IHDR', ihdr), chunk('IDAT', idatData), chunk('IEND', Buffer.alloc(0)), ]); } // --------------------------------------------------------------------- // // WAV builder (0.1s silence, mono 8 kHz, 8-bit PCM) // // --------------------------------------------------------------------- // function makeSilentWav(durationSec = 0.1) { const sampleRate = 8000; const numSamples = Math.floor(sampleRate * durationSec); const dataSize = numSamples; // 1 byte per sample const fileSize = 44 + dataSize - 8; const buf = Buffer.alloc(44 + dataSize); buf.write('RIFF', 0); buf.writeUInt32LE(fileSize, 4); buf.write('WAVE', 8); buf.write('fmt ', 12); buf.writeUInt32LE(16, 16); // subchunk1 size buf.writeUInt16LE(1, 20); // PCM buf.writeUInt16LE(1, 22); // mono buf.writeUInt32LE(sampleRate, 24); buf.writeUInt32LE(sampleRate, 28); // byteRate = sampleRate * 1 * 8/8 buf.writeUInt16LE(1, 32); // blockAlign buf.writeUInt16LE(8, 34); // bits per sample buf.write('data', 36); buf.writeUInt32LE(dataSize, 40); buf.fill(128, 44); // unsigned silence = 128 return buf; } // --------------------------------------------------------------------- // // MP3 builder (single silent MPEG-1 layer 3 frame, 8 kHz mono 8 kbps) // // --------------------------------------------------------------------- // // 13-byte ID3v2 empty tag + one 24-byte frame. Any real MP3 decoder will // render it as ~0.07s of silence. function makeSilentMp3() { const id3 = Buffer.from([ 0x49, 0x44, 0x33, // "ID3" 0x03, 0x00, // v2.3 0x00, // flags 0x00, 0x00, 0x00, 0x00, // size (0) ]); // MPEG-1 Layer III, 32 kbps, 32 kHz, mono, CRC off, no padding // Frame header bytes chosen to be universally parseable. const frame = Buffer.from([ 0xff, 0xfb, 0x10, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); return Buffer.concat([id3, frame]); } // --------------------------------------------------------------------- // // Spec table // // --------------------------------------------------------------------- // const ROOT = path.resolve(__dirname, '..', 'assets', 'resources'); // [relativePath, kind, colorOrNote] const SPEC = [ // 1. Protagonist — 3 color states ['textures/characters/kage_red.png', 'png', [220, 60, 60]], ['textures/characters/kage_green.png', 'png', [ 60, 200, 100]], ['textures/characters/kage_yellow.png', 'png', [240, 210, 60]], // 2. Enemies + BOSS ['textures/enemies/qing_ren.png', 'png', [ 80, 160, 220]], ['textures/enemies/chi_ren.png', 'png', [200, 70, 70]], ['textures/enemies/hei_ren.png', 'png', [ 40, 40, 50]], ['textures/enemies/yao_fang.png', 'png', [180, 100, 200]], ['textures/bosses/shuang_huan_fang.png', 'png', [120, 40, 140]], ['textures/bosses/butterfly.png', 'png', [240, 240, 100]], // 3. Scenes — 3 themes × 4 layers ['textures/scenes/forest/far.png', 'png', [ 30, 80, 50]], ['textures/scenes/forest/mid.png', 'png', [ 50, 110, 70]], ['textures/scenes/forest/near.png', 'png', [ 70, 140, 80]], ['textures/scenes/forest/fx.png', 'png', [180, 220, 160]], ['textures/scenes/castle_wall/far.png', 'png', [ 60, 60, 80]], ['textures/scenes/castle_wall/mid.png', 'png', [100, 100, 120]], ['textures/scenes/castle_wall/near.png', 'png', [140, 140, 150]], ['textures/scenes/castle_wall/fx.png', 'png', [220, 220, 200]], ['textures/scenes/demon_castle/far.png', 'png', [ 40, 20, 50]], ['textures/scenes/demon_castle/mid.png', 'png', [ 70, 30, 80]], ['textures/scenes/demon_castle/near.png', 'png', [110, 50, 120]], ['textures/scenes/demon_castle/fx.png', 'png', [200, 120, 220]], // 4. Story illustrations ['textures/story/ch1_page1_ninja.png', 'png', [ 50, 80, 140]], ['textures/story/ch1_page2_princess.png', 'png', [220, 160, 180]], ['textures/story/ch1_page3_depart.png', 'png', [180, 180, 110]], // 5. FX textures ['textures/fx/leaf_particle.png', 'png', [110, 180, 70]], ['textures/fx/jump_dust.png', 'png', [210, 210, 200]], ['textures/fx/parry_spark.png', 'png', [255, 240, 130]], // 6. SFX WAVs ['audio/sfx/attack.wav', 'wav'], ['audio/sfx/jump.wav', 'wav'], ['audio/sfx/hurt.wav', 'wav'], ['audio/sfx/pickup.wav', 'wav'], ['audio/sfx/parry.wav', 'wav'], // 7. BGM MP3s ['audio/bgm/bgm_forest.mp3', 'mp3'], ['audio/bgm/bgm_castle.mp3', 'mp3'], ['audio/bgm/bgm_final.mp3', 'mp3'], ['audio/bgm/bgm_boss.mp3', 'mp3'], ['audio/bgm/bgm_story.mp3', 'mp3'], ]; // --------------------------------------------------------------------- // // Writer // // --------------------------------------------------------------------- // function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function writeIfDifferent(filePath, buf) { if (fs.existsSync(filePath)) { const existing = fs.readFileSync(filePath); if (existing.length === buf.length && existing.equals(buf)) { return 'unchanged'; } } ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, buf); return 'wrote'; } let wrote = 0; let skipped = 0; for (const entry of SPEC) { const rel = entry[0]; const kind = entry[1]; const abs = path.join(ROOT, rel); let buf; switch (kind) { case 'png': { const [r, g, b] = entry[2]; buf = makeSolidPng(r, g, b, 255); break; } case 'wav': buf = makeSilentWav(); break; case 'mp3': buf = makeSilentMp3(); break; default: throw new Error(`unknown kind: ${kind}`); } const action = writeIfDifferent(abs, buf); if (action === 'wrote') { wrote++; console.log(` + ${rel}`); } else { skipped++; } } console.log(`\nPlaceholder asset generation done: ${wrote} written, ${skipped} unchanged.`);