Files
KateLegend2_proj/scripts/gen_placeholder_assets.js
2026-05-06 08:17:32 +08:00

222 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.`);