Vanilla JS + Canvas, zero dependencies, offline-first PWA. Gameplay: - 11 weapons x8 levels + 11 evolutions (chest-based), incl. timed mines - 13 passives, crit system with directional hit-sparks & hit-stop - 10 characters w/ unique mods + unlock conditions, gold cosmetic skins - 3 biomes (Neon Graveyard / Frozen Hollow / Magma Rift) each with own spawn tables, boss plans and music flavor; Endless mode + surges; 4 difficulty grades; breakable crystal-lamp props - Elite random affixes (Swift/Sturdy/Volatile), 4 bosses, win flow - Achievements (23) w/ gold rewards, run history, daily seeded challenge Tech: - Cinematic canvas main-menu scene, game-feel FX suite (trails, muzzle, status tints, low-HP pulse), viewport culling + particle pooling - WebAudio synth SFX + generative per-biome soundtrack - Gamepad support, remappable keys, touch joystick, fullscreen - i18n VI/EN, localStorage saves w/ export-import codes - Cloudflare Workers leaderboard scaffold (KV) w/ signed submits - Headless integrity test-suite (node test/integrity.js)
98 lines
3.2 KiB
JavaScript
98 lines
3.2 KiB
JavaScript
// Generates PWA icons without external deps: neon diamond on dark bg.
|
|
'use strict';
|
|
const zlib = require('zlib');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
function crc32(buf) {
|
|
let table = crc32.table;
|
|
if (!table) {
|
|
table = crc32.table = 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;
|
|
table[n] = c >>> 0;
|
|
}
|
|
}
|
|
let c = 0xFFFFFFFF;
|
|
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xFF] ^ (c >>> 8);
|
|
return (c ^ 0xFFFFFFFF) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length);
|
|
const t = Buffer.from(type, 'ascii');
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(Buffer.concat([t, data])));
|
|
return Buffer.concat([len, t, data, crc]);
|
|
}
|
|
|
|
function png(w, h, rgba) {
|
|
const stride = w * 4;
|
|
const 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 + 1) * stride);
|
|
}
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4);
|
|
ihdr[8] = 8; ihdr[9] = 6;
|
|
const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
|
return Buffer.concat([
|
|
sig,
|
|
chunk('IHDR', ihdr),
|
|
chunk('IDAT', zlib.deflateSync(raw, { level: 9 })),
|
|
chunk('IEND', Buffer.alloc(0))
|
|
]);
|
|
}
|
|
|
|
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
|
|
function makeIcon(size) {
|
|
const px = Buffer.alloc(size * size * 4);
|
|
const cx = size / 2, cy = size / 2;
|
|
const maxD = Math.hypot(cx, cy);
|
|
const diaR = size * 0.34;
|
|
for (let y = 0; y < size; y++) {
|
|
for (let x = 0; x < size; x++) {
|
|
const i = (y * size + x) * 4;
|
|
const dx = x - cx, dy = y - cy;
|
|
const d = Math.hypot(dx, dy) / maxD;
|
|
|
|
// background: vertical dark gradient
|
|
let r = lerp(10, 5, y / size);
|
|
let g = lerp(10, 5, y / size);
|
|
let b = lerp(31, 12, y / size);
|
|
|
|
// soft purple glow ring
|
|
const glow = Math.max(0, 1 - d * 2.2) ** 2;
|
|
r += 124 * glow * 0.35; g += 92 * glow * 0.35; b += 255 * glow * 0.4;
|
|
|
|
// neon diamond
|
|
const ad = Math.abs(dx) + Math.abs(dy);
|
|
if (ad < diaR) {
|
|
const t = ad / diaR;
|
|
// gradient edge cyan -> center violet -> white core
|
|
if (t > 0.82) { r = 224; g = 250; b = 255; } // rim highlight
|
|
else if (t > 0.55) { r = 0; g = 229; b = 255; } // cyan edge
|
|
else if (t > 0.25) { r = 124; g = 92; b = 255; } // violet body
|
|
else { r = 233; g = 213; b = 255; } // white core
|
|
// subtle scanline shading
|
|
if (((x + y) >> 3) % 2 === 0 && t <= 0.82) { r *= 0.92; g *= 0.92; b *= 0.95; }
|
|
}
|
|
px[i] = Math.min(255, r | 0); px[i + 1] = Math.min(255, g | 0);
|
|
px[i + 2] = Math.min(255, b | 0); px[i + 3] = 255;
|
|
}
|
|
}
|
|
return png(size, size, px);
|
|
}
|
|
|
|
const outDir = path.join(__dirname, '..', 'icons');
|
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir);
|
|
for (const s of [192, 512]) {
|
|
const f = path.join(outDir, `icon-${s}.png`);
|
|
fs.writeFileSync(f, makeIcon(s));
|
|
console.log('wrote', f, fs.statSync(f).size, 'bytes');
|
|
}
|