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)
389 lines
17 KiB
JavaScript
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)); }
|