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)
91 lines
5.3 KiB
JavaScript
91 lines
5.3 KiB
JavaScript
'use strict';
|
|
/* ============================================================
|
|
NEON SURVIVORS — achievements.js
|
|
Declarative achievement list + evaluator. Names are embedded
|
|
bilingual strings (kept out of i18n dicts on purpose).
|
|
cond(save, G) -> boolean against persisted progress only.
|
|
============================================================ */
|
|
|
|
const ACHS = [
|
|
// ---- combat milestones ----
|
|
{ id: 'kill100', icon: '⚔️', vi: 'Thợ Săn Mới', en: 'Rookie Hunter',
|
|
dvi: 'Diệt 100 quái', den: 'Kill 100 enemies', cond: s => s.totals.kills >= 100 },
|
|
{ id: 'kill500', icon: '🗡️', vi: 'Kẻ Trừng Phạt', en: 'Executioner',
|
|
dvi: 'Diệt 500 quái', den: 'Kill 500 enemies', cond: s => s.totals.kills >= 500 },
|
|
{ id: 'kill2500', icon: '💀', vi: 'Tử Thần Nhỏ', en: 'Petite Reaper',
|
|
dvi: 'Diệt 2.500 quái', den: 'Kill 2,500 enemies', cond: s => s.totals.kills >= 2500 },
|
|
{ id: 'kill10k', icon: '☠️', vi: 'Tai Họa Đi Bộ', en: 'Walking Calamity',
|
|
dvi: 'Diệt 10.000 quái', den: 'Kill 10,000 enemies', cond: s => s.totals.kills >= 10000 },
|
|
|
|
// ---- wealth ----
|
|
{ id: 'gold1k', icon: '🪙', vi: 'Giàu Có', en: 'Coin Purse',
|
|
dvi: 'Kiếm 1.000 vàng lifetime', den: 'Earn 1,000 gold lifetime', cond: s => s.totals.gold >= 1000 },
|
|
{ id: 'gold10k', icon: '💰', vi: 'Đại Gia Vàng', en: 'Gold Baron',
|
|
dvi: 'Kiếm 10.000 vàng lifetime', den: 'Earn 10,000 gold lifetime', cond: s => s.totals.gold >= 10000 },
|
|
|
|
// ---- survival time ----
|
|
{ id: 'time5', icon: '⏱️', vi: 'Làm quen', en: 'Warmed Up',
|
|
dvi: 'Sống sót 5 phút', den: 'Survive 5 minutes', cond: s => s.totals.best >= 300 },
|
|
{ id: 'time10', icon: '⏳', vi: 'Bền Bỉ', en: 'Tenacious',
|
|
dvi: 'Sống sót 10 phút', den: 'Survive 10 minutes', cond: s => s.totals.best >= 600 },
|
|
{ id: 'time15', icon: '🌙', vi: 'Dòng Chảy', en: 'In The Zone',
|
|
dvi: 'Sống sót 15 phút', den: 'Survive 15 minutes', cond: s => s.totals.best >= 900 },
|
|
{ id: 'time20', icon: '🌅', vi: 'Bình Minh', en: 'Daybreak',
|
|
dvi: 'Sống sót đủ 20 phút (thắng)', den: 'Survive the full 20 minutes (win)', cond: s => s.totals.best >= 1200 || s.totals.wins >= 1 },
|
|
|
|
// ---- victories & difficulty ----
|
|
{ id: 'win1', icon: '👑', vi: 'Kết Thúc Cuộc Ngủ', en: 'Wake The Dead',
|
|
dvi: 'Hạ Tử Thần — thắng 1 ván', den: 'Slay Death — win a run', cond: s => s.totals.wins >= 1 },
|
|
{ id: 'win5', icon: '🏆', vi: 'Vua Sống Sót', en: 'Survivor King',
|
|
dvi: 'Thắng 5 ván', den: 'Win 5 runs', cond: s => s.totals.wins >= 5 },
|
|
{ id: 'grade3', icon: '💀', vi: 'Vô Cầu Vị', en: 'No Mercy',
|
|
dvi: 'Thắng ở Độ khó 💀 (III+)', den: 'Win on Grade 💀 (III+)', cond: s => (s.progress.gradeWins[2] || 0) + (s.progress.gradeWins[3] || 0) >= 1 },
|
|
|
|
// ---- stages & endless ----
|
|
{ id: 'st_frost', icon: '❄️', vi: 'Phá Băng', en: 'Ice Breaker',
|
|
dvi: 'Thắng Hang Băng', den: 'Win Frozen Hollow', cond: s => !!s.progress.stageWins.frostcave },
|
|
{ id: 'st_magma', icon: '🌋', vi: 'Chúa Tể Núi Lửa', en: 'Volcano Lord',
|
|
dvi: 'Thắng Vực Magma', den: 'Win Magma Rift', cond: s => !!s.progress.stageWins.magmalands },
|
|
{ id: 'endless25', icon: '∞', vi: 'Không Điểm Dừng', en: 'No Stop Sign',
|
|
dvi: 'Endless đạt 25 phút', den: 'Reach minute 25 in Endless', cond: s => (s.progress.endlessBest || 0) >= 1500 },
|
|
|
|
// ---- collection ----
|
|
{ id: 'evo1', icon: '🌟', vi: 'Tiến Hóa!', en: 'Evolve!',
|
|
dvi: 'Tiến hóa 1 vũ khí', den: 'Evolve a weapon', cond: s => (s.totals.evos || 0) >= 1 },
|
|
{ id: 'evo6', icon: '✨', vi: 'Nhà Sưu Tầm', en: 'Collector',
|
|
dvi: 'Tiến hóa 6 lần (tổng mọi ván)', den: 'Evolve 6 times (across runs)', cond: s => (s.totals.evos || 0) >= 6 },
|
|
{ id: 'chars_all', icon: '🎭', vi: 'Full Dàn Sao', en: 'Full Roster',
|
|
dvi: 'Mở khóa tất cả nhân vật', den: 'Unlock every character', cond: s => (s.chars || []).length >= Object.keys(CHARS).length },
|
|
{ id: 'meta10', icon: '🔧', vi: 'Nâng Cấp Giỏi', en: 'Well Oiled',
|
|
dvi: 'Mua tổng cộng 10 cấp cửa hàng', den: 'Buy 10 total meta-shop ranks', cond: s => Object.values(s.meta || {}).reduce((a, b) => a + b, 0) >= 10 },
|
|
|
|
// ---- misc ----
|
|
{ id: 'props50', icon: '💎', vi: 'Phá Phách', en: 'Smasher',
|
|
dvi: 'Phá 50 đèn pha lê', den: 'Break 50 crystal lamps', cond: s => (s.totals.props || 0) >= 50 },
|
|
{ id: 'daily1', icon: '⚡', vi: 'Người Của Hôm Nay', en: 'Daily Daredevil',
|
|
dvi: 'Hoàn thành 1 Thử thách hôm nay', den: 'Finish a Daily Challenge', cond: s => (s.daily.best || 0) > 0 }
|
|
];
|
|
|
|
/** Evaluate all; unlock new ones into save.ach and return their defs. */
|
|
function checkAchievements() {
|
|
const save = Store.data;
|
|
const newly = [];
|
|
for (const a of ACHS) {
|
|
if (!save.ach[a.id] && _safe(a.cond, save)) {
|
|
save.ach[a.id] = Date.now();
|
|
newly.push(a);
|
|
}
|
|
}
|
|
if (newly.length) Store.save();
|
|
return newly;
|
|
}
|
|
|
|
function _safe(fn, save) { try { return !!fn(save); } catch (e) { return false; } }
|
|
|
|
/** Achievement count helper for menu/codex badges. */
|
|
function achProgress() {
|
|
let n = 0;
|
|
for (const a of ACHS) if (Store.data.ach[a.id]) n++;
|
|
return { n, total: ACHS.length };
|
|
}
|