Files
neon-survivors/js/data.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

389 lines
17 KiB
JavaScript

'use strict';
/* ============================================================
NEON SURVIVORS — data.js : all game content definitions
Weapons / Evolutions / Passives / Characters / Enemies /
Bosses / Spawn timeline / Meta shop.
============================================================ */
/* ---------------- WEAPONS ----------------
Stat model: effective stat = base + sum(per[0..level-2])
dmg damage per hit cd seconds between activations
n projectile/count spd projectile speed (px/s)
pierce extra enemies pierced dur lifetime (s)
area size multiplier kb knockback impulse
*/
const WEAPONS = {
wand: {
sym: '🪄', col: '#b39dff', max: 8,
base: { dmg: 12, cd: 1.1, n: 1, spd: 430, pierce: 0, dur: 1.6, area: 1, kb: 40 },
per: [
{ dmg: 5 }, { n: 1 }, { cd: -0.15 }, { dmg: 6 },
{ n: 1, spd: 60 }, { pierce: 1, dmg: 4 }, { n: 1 }, { cd: -0.15, dmg: 8 }
],
evo: { into: 'e_wand', need: 'p_cd' }
},
whip: {
sym: '🩹', col: '#ff7a9c', max: 8,
base: { dmg: 14, cd: 1.35, n: 1, area: 1.1, kb: 140 },
per: [
{ dmg: 6 }, { area: 0.15 }, { dmg: 8 }, { n: 1 },
{ dmg: 10 }, { area: 0.2 }, { cd: -0.15 }, { dmg: 12 }
],
evo: { into: 'e_whip', need: 'p_hp' }
},
knife: {
sym: '🔪', col: '#cfe8ff', max: 8,
base: { dmg: 8, cd: 0.9, n: 2, spd: 520, pierce: 0, dur: 1.1, kb: 20 },
per: [
{ n: 1 }, { dmg: 4 }, { n: 2 }, { dmg: 4 },
{ pierce: 1 }, { n: 2 }, { dmg: 6 }, { n: 2, spd: 80 }
],
evo: { into: 'e_knife', need: 'p_spd' }
},
orb: {
sym: '🔮', col: '#7dfaff', max: 8,
base: { dmg: 10, cd: 1.0, n: 2, spd: 2.4, area: 1, hitCd: 0.5 },
per: [
{ n: 1 }, { spd: 0.4 }, { dmg: 5 }, { n: 1 },
{ area: 0.15 }, { dmg: 6 }, { n: 1, spd: 0.4 }, { dmg: 8, area: 0.15 }
],
evo: { into: 'e_orb', need: 'p_area' }
},
axe: {
sym: '🪓', col: '#ffb347', max: 8,
base: { dmg: 26, cd: 1.7, n: 1, spd: 240, pierce: 99, area: 1, kb: 200, grav: 900 },
per: [
{ dmg: 12 }, { n: 1 }, { dmg: 12 }, { n: 1 },
{ area: 0.25 }, { dmg: 16 }, { n: 1, cd: -0.2 }, { dmg: 20 }
],
evo: { into: 'e_axe', need: 'p_might' }
},
garlic: {
sym: '💡', col: '#fff3b0', max: 8,
base: { dmg: 5, cd: 0.45, area: 1, kb: 90, radius: 95 },
per: [
{ area: 0.18 }, { dmg: 3 }, { area: 0.18 }, { kb: 40 },
{ dmg: 4 }, { area: 0.22 }, { dmg: 5, cd: -0.05 }, { area: 0.25, dmg: 6 }
],
evo: { into: 'e_garlic', need: 'p_regen' }
},
light: {
sym: '⚡', col: '#9bf6ff', max: 8,
base: { dmg: 30, cd: 2.2, n: 2, area: 1, blastR: 70 },
per: [
{ n: 1 }, { dmg: 12 }, { n: 1 }, { area: 0.2 },
{ dmg: 16 }, { n: 1 }, { cd: -0.3 }, { dmg: 20, n: 1 }
],
evo: { into: 'e_light', need: 'p_dur' }
},
fire: {
sym: '🔥', col: '#ff8c42', max: 8,
base: { dmg: 20, cd: 2.0, n: 1, spd: 300, area: 1, blastR: 85, burnDps: 8, burnDur: 2, kb: 60 },
per: [
{ dmg: 8 }, { n: 1 }, { dmg: 10 }, { area: 0.25 },
{ n: 1 }, { dmg: 12 }, { cd: -0.3 }, { area: 0.3, dmg: 14 }
],
evo: { into: 'e_fire', need: 'p_luck' }
},
frost: {
sym: '❄️', col: '#a0e7ff', max: 8,
base: { dmg: 10, cd: 1.2, n: 1, spd: 480, pierce: 2, dur: 1.4, slow: 0.35, slowDur: 2, kb: 30 },
per: [
{ n: 1 }, { dmg: 5 }, { slow: 0.1 }, { pierce: 2 },
{ dmg: 6 }, { n: 1 }, { slow: 0.1, dmg: 6 }, { n: 2, dmg: 8 }
],
evo: { into: 'e_frost', need: 'p_mag' }
},
cross: {
sym: '✝️', col: '#ffffff', max: 8,
base: { dmg: 16, cd: 1.9, n: 1, spd: 420, pierce: 99, dur: 2.2, area: 1, rehit: 0.45, kb: 80 },
per: [
{ dmg: 8 }, { n: 1 }, { dmg: 8 }, { spd: 60 },
{ n: 1 }, { dmg: 10 }, { dur: 0.4, area: 0.15 }, { n: 1, dmg: 12 }
],
evo: { into: 'e_cross', need: 'p_move' }
},
mine: {
sym: '🧨', col: '#ff6b9d', max: 8,
base: { dmg: 26, cd: 2.6, n: 1, dur: 10, blastR: 85, kb: 140, pierce: 0 },
per: [
{ dmg: 10 }, { dur: 2 }, { blastR: 15 }, { n: 1 },
{ dmg: 14 }, { cd: -0.4 }, { blastR: 20, dmg: 12 }, { n: 1, dmg: 18 }
],
evo: { into: 'e_mine', need: 'p_area' }
}
};
/* ---------------- EVOLVED WEAPONS (max level 1, from chests) ---------------- */
const EVOLVED = {
e_mine: { sym: '💣', col: '#ff4d88', base: { dmg: 55, cd: 1.5, n: 3, dur: 14, blastR: 150, kb: 220 } },
e_wand: { sym: '🌟', col: '#ffe066', base: { dmg: 30, cd: 0.55, n: 6, spd: 560, pierce: 2, dur: 1.8, kb: 60 } },
e_whip: { sym: '🩸', col: '#ff4d6d', base: { dmg: 44, cd: 1.0, n: 2, area: 1.5, kb: 180, lifesteal: 0.08 } },
e_knife: { sym: '🗡️', col: '#e8fbff', base: { dmg: 18, cd: 0.45, n: 8, spd: 640, pierce: 3, dur: 1.2, kb: 20 } },
e_orb: { sym: '💫', col: '#7dfaff', base: { dmg: 28, cd: 0.35, n: 6, spd: 3.4, area: 1.5, hitCd: 0.4 } },
e_axe: { sym: '🌀', col: '#ffd166', base: { dmg: 70, cd: 1.2, n: 4, spd: 320, pierce: 999, area: 1.6, kb: 260, grav: 700 } },
e_garlic:{ sym: '😇', col: '#fffbe0', base: { dmg: 16, cd: 0.3, area: 1.9, kb: 160, radius: 175, healOnKill: 1 } },
e_light: { sym: '🌩️', col: '#d0fbff', base: { dmg: 65, cd: 1.3, n: 5, area: 1.4, blastR: 100, chain: 2, stun: 1.0 } },
e_fire: { sym: '☄️', col: '#ff5c33', base: { dmg: 50, cd: 1.4, n: 2, spd: 340, area: 1.7, blastR: 130, burnDps: 30, burnDur: 4, groundDur: 3.5, groundDps: 35, kb: 90 } },
e_frost: { sym: '💠', col: '#cdf5ff', base: { dmg: 34, cd: 0.9, n: 3, spd: 560, pierce: 99, dur: 1.6, slow: 0.6, slowDur: 2.5, freeze: 1.4, kb: 40 } },
e_cross: { sym: '⚜️', col: '#ffffff', base: { dmg: 60, cd: 1.5, n: 2, spd: 500, pierce: 999, dur: 3.2, area: 1.8, rehit: 0.4, kb: 120 } }
};
/** Unified weapon definition accessor. */
function wdef(id) { return EVOLVED[id] || WEAPONS[id]; }
/** Compute effective stats for a weapon at a given level. */
function wstats(id, lvl) {
const ev = EVOLVED[id];
if (ev) return Object.assign({}, ev.base);
const d = WEAPONS[id];
const s = Object.assign({}, d.base);
for (let i = 0; i < Math.min(lvl - 1, d.per.length); i++) {
const add = d.per[i];
for (const k in add) s[k] = (s[k] || 0) + add[k];
}
s.cd = Math.max(0.12, s.cd);
return s;
}
/* ---------------- PASSIVES ---------------- */
const PASSIVES = {
p_might: { sym: '💪', col: '#ff7a7a', max: 5, val: 0.10 },
p_armor: { sym: '🛡️', col: '#9db4ff', max: 5, val: 1 },
p_hp: { sym: '❤️', col: '#ff5f7a', max: 5, val: 0.20 },
p_regen: { sym: '🌿', col: '#7dff9e', max: 5, val: 0.6 },
p_cd: { sym: '⏱️', col: '#7dd0ff', max: 5, val: 0.08 },
p_area: { sym: '🌐', col: '#c07dff', max: 5, val: 0.12 },
p_spd: { sym: '🏹', col: '#ffd57d', max: 5, val: 0.15 },
p_dur: { sym: '⌛', col: '#ffc7ec', max: 5, val: 0.15 },
p_move: { sym: '👟', col: '#9effd0', max: 5, val: 0.10 },
p_mag: { sym: '🧲', col: '#ffa8f6', max: 5, val: 0.30 },
p_luck: { sym: '🍀', col: '#8aff6b', max: 5, val: 0.10 },
p_greed: { sym: '💰', col: '#ffd24a', max: 5, val: 0.20 },
p_growth: { sym: '📘', col: '#7db8ff', max: 5, val: 0.10 }
};
/** Passive name/desc keys derive from id: p_might -> 'p_might'/'p_might_d'. */
/* ---------------- CHARACTERS ---------------- */
const CHARS = {
kaito: { sym: '🧙', col: '#9b8cff', weapon: 'wand',
mods: {},
cond: { key: 'cond_free', fn: () => true } },
mira: { sym: '🛡️', col: '#ff9db0', weapon: 'whip',
mods: { armorFlat: 1, hpMul: 0.10 },
cond: { key: 'cond_kills500', fn: () => Store.data.totals.kills >= 500 } },
bolt: { sym: '⚡', col: '#ffe066', weapon: 'knife',
mods: { moveMul: 0.12, pspdMul: 0.15 },
cond: { key: 'cond_time10', fn: () => Store.data.totals.best >= 600 } },
luna: { sym: '🌙', col: '#b8a9ff', weapon: 'orb',
mods: { areaMul: 0.18, cdrMul: 0.10 },
cond: { key: 'cond_gold2000', fn: () => Store.data.totals.gold >= 2000 } },
rook: { sym: '🪓', col: '#ffb347', weapon: 'axe',
mods: { dmgMul: 0.20, moveMul: -0.05 },
cond: { key: 'cond_boss1', fn: () => !!Store.data.flags.boss1 } },
nyx: { sym: '🧛', col: '#ff6b81', weapon: 'garlic',
mods: { regenFlat: 0.4, magMul: 0.40 },
cond: { key: 'cond_time15', fn: () => Store.data.totals.best >= 900 } },
sera: { sym: '🌩️', col: '#7dfaff', weapon: 'light',
mods: { cdrMul: 0.12, luckFlat: 0.05 },
cond: { key: 'cond_win', fn: () => Store.data.totals.wins >= 1 } },
vane: { sym: '✝️', col: '#e8e8ff', weapon: 'cross',
mods: { luckFlat: 0.15, greedMul: 0.10 },
cond: { key: 'cond_kills5000', fn: () => Store.data.totals.kills >= 5000 } },
hana: { sym: '🏹', col: '#7dff9e', weapon: 'mine',
mods: { critC: 0.15, critM: 0.3 },
cond: { key: 'cond_time15', fn: () => Store.data.totals.best >= 900 } },
bram: { sym: '🧱', col: '#c96f4a', weapon: 'fire',
mods: { areaMul: 0.20, moveMul: -0.05, hpMul: 0.10 },
cond: { key: 'cond_gold2000', fn: () => Store.data.totals.gold >= 2000 } }
};
/* ---------------- COSMETIC SKINS ----------------
* Deterministic per-character palettes; purchasable with gold.
*/
const SKIN_POOL = [
{ key: 'ember', col: '#ff8c42', vi: 'Lửa', en: 'Ember', price: 300 },
{ key: 'frost', col: '#7fdcff', vi: 'Băng', en: 'Frost', price: 300 },
{ key: 'toxic', col: '#a4f94d', vi: 'Độc', en: 'Toxic', price: 450 },
{ key: 'rose', col: '#ff9db0', vi: 'Hồng', en: 'Rose', price: 450 },
{ key: 'gold', col: '#ffd24a', vi: 'Vàng', en: 'Gold', price: 800 }
];
function _strHash(s) {
let h = 2166136261;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h * 16777619) >>> 0; }
return h;
}
/** Two extra skins per character, chosen deterministically from the pool. */
function skinsFor(charId) {
const h = _strHash(charId);
const a = SKIN_POOL[h % SKIN_POOL.length];
let b = SKIN_POOL[(h + 2) % SKIN_POOL.length];
if (b.key === a.key) b = SKIN_POOL[(h + 4) % SKIN_POOL.length];
return [a, b];
}
/* ---------------- ENEMIES ---------------- */
const ENEMIES = {
bat: { nk: 'en_bat', hp: 8, spd: 100, dmg: 6, xp: 1, r: 10, col: '#b06bff', shape: 'bat', ai: 'chase', wiggle: 1.6 },
zombie: { nk: 'en_zombie', hp: 18, spd: 68, dmg: 10, xp: 1, r: 13, col: '#6bd66b', shape: 'zombie', ai: 'chase' },
imp: { nk: 'en_imp', hp: 11, spd: 118, dmg: 8, xp: 1, r: 10, col: '#ff8c42', shape: 'imp', ai: 'chase', wiggle: 2.6 },
arch: { nk: 'en_arch', hp: 24, spd: 76, dmg: 10, xp: 2, r: 13, col: '#e8e8e8', shape: 'archer', ai: 'shooter', range: 270, bspd: 235, bcd: 2.3 },
brute: { nk: 'en_brute', hp: 135, spd: 46, dmg: 22, xp: 5, r: 24, col: '#c96f4a', shape: 'brute', ai: 'chase', kbRes: 0.75 },
ghost: { nk: 'en_ghost', hp: 32, spd: 82, dmg: 12, xp: 2, r: 13, col: '#9ad5ff', shape: 'ghost', ai: 'ghost' },
spit: { nk: 'en_spit', hp: 38, spd: 62, dmg: 12, xp: 3, r: 15, col: '#a4f94d', shape: 'spit', ai: 'shooter', range: 250, bspd: 210, bcd: 2.8, spread: 3 },
garg: { nk: 'en_garg', hp: 95, spd: 60, dmg: 18, xp: 4, r: 17, col: '#8fa3b8', shape: 'garg', ai: 'charger', chargeSpd: 340 },
/* breakable prop — no contact damage, drops coins when smashed */
lamp: { nk: 'en_lamp', hp: 26, spd: 0, dmg: 0, xp: 0, r: 15, col: '#8fd0ff', shape: 'prop', ai: 'prop', prop: true }
};
/* Difficulty multipliers by elapsed minute. */
function diffScale(minute) {
return {
hp: 1 + minute * 0.5 + minute * minute * 0.035,
dmg: 1 + minute * 0.08,
spd: Math.min(1 + minute * 0.012, 1.3)
};
}
/* Which enemy types can appear at a given minute (weighted evenly). */
const SPAWN_TABLE = [
/*00*/ ['bat'],
/*01*/ ['bat', 'zombie'],
/*02*/ ['zombie', 'bat', 'imp'],
/*03*/ ['zombie', 'imp', 'bat'],
/*04*/ ['zombie', 'imp', 'arch'],
/*05*/ ['imp', 'arch', 'zombie', 'bat'],
/*06*/ ['arch', 'imp', 'ghost'],
/*07*/ ['ghost', 'arch', 'brute'],
/*08*/ ['brute', 'ghost', 'imp', 'arch'],
/*09*/ ['brute', 'spit', 'ghost'],
/*10*/ ['spit', 'brute', 'garg', 'imp'],
/*11*/ ['garg', 'spit', 'ghost', 'brute'],
/*12*/ ['garg', 'brute', 'spit', 'bat'],
/*13*/ ['garg', 'spit', 'brute', 'ghost'],
/*14*/ ['garg', 'brute', 'spit', 'garg'],
/*15*/ ['garg', 'brute', 'spit', 'ghost'],
/*16*/ ['garg', 'brute', 'spit', 'imp'],
/*17*/ ['garg', 'brute', 'garg', 'spit'],
/*18*/ ['garg', 'brute', 'spit', 'garg'],
/*19*/ ['garg', 'brute', 'spit', 'garg']
];
/* Elite spawn schedule (seconds). Elites drop treasure chests. */
const ELITE_TIMES = [70, 145, 220, 295, 370, 445, 520, 595, 670, 745, 820, 895, 970, 1045, 1120];
/* Random elite modifiers — one may roll on spawn (65%). Bilingual inline. */
const ELITE_AFFIXES = {
haste: { icon: '⚡', vi: 'Tốc Độ', en: 'Swift', col: '#ffe066',
apply(e) { e.spd *= 1.45; } },
tank: { icon: '🛡️', vi: 'Cứng Cáp', en: 'Sturdy', col: '#9db4ff',
apply(e) { e.hp *= 1.6; e.maxhp = e.hp; e.dmg *= 1.15;
e.kbRes = Math.min(0.92, (e.def.kbRes || 0) + 0.55); } },
blast: { icon: '💥', vi: 'Nổ Tung', en: 'Volatile', col: '#ff8c42',
apply(e) { e.blastOnDeath = true; } }
};
/* Boss plan — killing the final boss wins the run. */
const RUN_LENGTH = 1200;
const BOSS_PLAN = [
{ t: 300, id: 'b_wolf' },
{ t: 600, id: 'b_lich' },
{ t: 900, id: 'b_behemoth' },
{ t: 1140, id: 'b_death' }
];
const BOSSES = {
b_wolf: { nk: 'bn_wolf', hp: 850, spd: 165, dmg: 25, r: 30, col: '#8a5cff', shape: 'wolf', ai: 'boss_wolf', xp: 60 },
b_lich: { nk: 'bn_lich', hp: 2400, spd: 58, dmg: 28, r: 30, col: '#c9f0ff', shape: 'lich', ai: 'boss_lich', xp: 100 },
b_behemoth: { nk: 'bn_behemoth', hp: 6500, spd: 42, dmg: 34, r: 44, col: '#ff5c5c', shape: 'behemoth', ai: 'boss_beho', xp: 160 },
b_death: { nk: 'bn_death', hp: 15000, spd: 195, dmg: 45, r: 34, col: '#ddddff', shape: 'death', ai: 'boss_death', xp: 400 }
};
/* ---------------- STAGES ----------------
* Each biome swaps the spawn table / boss plan / palette and applies
* hp/gold multipliers. Killing the plan's final boss wins (unless Endless).
*/
const STAGES = {
graveyard: {
id: 'graveyard', icon: '🪦', unlock: () => true,
music: { shift: 0, tempo: 1 },
bg: { grid: 'rgba(90,90,160,0.10)', mote: ['#5a4d9c', '#3f7fb0'] },
table: SPAWN_TABLE,
plan: [
{ t: 300, id: 'b_wolf' },
{ t: 600, id: 'b_lich' },
{ t: 900, id: 'b_behemoth' },
{ t: 1140, id: 'b_death', final: true }
],
length: RUN_LENGTH,
hpMul: 1, goldMul: 1
},
frostcave: {
id: 'frostcave', icon: '❄️', unlock: s => s.flags.time10,
music: { shift: -3, tempo: 0.92 }, // darker, slower
lockKey: 'st_lock_frost',
bg: { grid: 'rgba(120,190,255,0.10)', mote: ['#7fc8ff', '#b0e8ff'] },
table: (() => {
// colder mix: ghosts early, brutes mid, gargs sooner
const t = SPAWN_TABLE.map(r => r.slice());
t[2] = ['ghost', 'zombie', 'bat'];
t[6] = ['ghost', 'arch', 'imp'];
t[9] = ['brute', 'garg', 'spit'];
t[12] = ['garg', 'garg', 'spit', 'brute'];
return t;
})(),
plan: [
{ t: 240, id: 'b_lich' },
{ t: 540, id: 'b_wolf' },
{ t: 870, id: 'b_behemoth' },
{ t: 1140, id: 'b_death', final: true }
],
length: RUN_LENGTH,
hpMul: 1.12, goldMul: 1.25
},
magmalands: {
id: 'magmalands', icon: '🌋', unlock: s => s.totals.wins >= 1,
music: { shift: 2, tempo: 1.08 }, // hotter, faster
lockKey: 'st_lock_magma',
bg: { grid: 'rgba(255,120,60,0.09)', mote: ['#ff8c42', '#ffd166'] },
table: (() => {
// violent mix: imps early, spitters sooner, heavy late
const t = SPAWN_TABLE.map(r => r.slice());
t[1] = ['bat', 'imp'];
t[4] = ['imp', 'spit', 'zombie'];
t[7] = ['spit', 'brute', 'garg'];
t[10] = ['garg', 'brute', 'brute', 'spit'];
t[16] = ['garg', 'garg', 'brute', 'brute'];
return t;
})(),
plan: [
{ t: 210, id: 'b_behemoth' },
{ t: 480, id: 'b_lich' },
{ t: 780, id: 'b_wolf' },
{ t: 1080, id: 'b_death', final: true }
],
length: RUN_LENGTH,
hpMul: 1.28, goldMul: 1.5
}
};
/* ---------------- META SHOP ---------------- */
const META_SHOP = [
{ id: 'might', sym: '💪', base: 100, max: 5 },
{ id: 'hp', sym: '❤️', base: 100, max: 5 },
{ id: 'armor', sym: '🛡️', base: 120, max: 5 },
{ id: 'regen', sym: '🌿', base: 150, max: 5 },
{ id: 'spd', sym: '👟', base: 120, max: 5 },
{ id: 'cd', sym: '⏱️', base: 180, max: 5 },
{ id: 'growth', sym: '📘', base: 130, max: 5 },
{ id: 'greed', sym: '💰', base: 130, max: 5 },
{ id: 'luck', sym: '🍀', base: 140, max: 5 },
{ id: 'magnet', sym: '🧲', base: 110, max: 5 },
{ id: 'reroll', sym: '🎲', base: 150, max: 3 },
{ id: 'revive', sym: '🕊️', base: 350, max: 2 }
];
function metaCost(def, rank) { return Math.round(def.base * Math.pow(1.7, rank)); }