222 lines
8.7 KiB
JavaScript
222 lines
8.7 KiB
JavaScript
#!/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.`);
|