Files
neon-survivors/tools/gen-icons.js
T
neon-survivors-dev 796880375e NEON SURVIVORS v1.1 — full-featured bullet-heaven survivor game
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)
2026-08-23 07:01:22 +00:00

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');
}