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

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