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)
52 lines
1.3 KiB
JavaScript
52 lines
1.3 KiB
JavaScript
/* Neon Survivors — service worker (cache-first, versioned) */
|
|
'use strict';
|
|
const CACHE = 'neon-survivors-v1';
|
|
const ASSETS = [
|
|
'./',
|
|
'./index.html',
|
|
'./manifest.webmanifest',
|
|
'./css/style.css',
|
|
'./icons/icon-192.png',
|
|
'./icons/icon-512.png',
|
|
'./js/util.js',
|
|
'./js/i18n.js',
|
|
'./js/save.js',
|
|
'./js/audio.js',
|
|
'./js/data.js',
|
|
'./js/entities.js',
|
|
'./js/systems.js',
|
|
'./js/render.js',
|
|
'./js/ui.js',
|
|
'./js/game.js',
|
|
'./js/main.js'
|
|
];
|
|
|
|
self.addEventListener('install', (e) => {
|
|
e.waitUntil(
|
|
caches.open(CACHE).then(c => c.addAll(ASSETS)).then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil(
|
|
caches.keys()
|
|
.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
self.addEventListener('fetch', (e) => {
|
|
if (e.request.method !== 'GET') return;
|
|
const url = new URL(e.request.url);
|
|
if (url.origin !== location.origin) return;
|
|
e.respondWith(
|
|
caches.match(e.request, { ignoreSearch: true }).then(hit =>
|
|
hit || fetch(e.request).then(res => {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, copy));
|
|
return res;
|
|
}).catch(() => hit)
|
|
)
|
|
);
|
|
});
|