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

740 lines
22 KiB
JavaScript

'use strict';
/* ============================================================
NEON SURVIVORS — game.js : Game class, loop, states, input
============================================================ */
const ST = { MENU: 'menu', RUN: 'run', PAUSE: 'pause', LEVEL: 'level', CHEST: 'chest', OVER: 'over' };
class Game {
constructor(canvas) {
this.cv = canvas;
this.ctx = canvas.getContext('2d');
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
this.state = ST.MENU;
// world
this.player = null;
this.enemies = [];
this.projs = [];
this.ebullets = [];
this.pickups = [];
this.parts = [];
this.partPool = [];
this.texts = [];
this.effects = [];
this.grid = new SpatialGrid(96);
this.cam = { x: 0, y: 0 };
this.time = 0;
this.lastDt = 0.016;
this.shake = 0;
this.hurtFlash = 0;
this.partCap = 600;
this.over = false;
this.playerHidden = false;
// flow
this.queueLevelUps = 0;
this.pendingChests = 0;
this.currentChoices = [];
this.rerollsLeft = 0;
this.goldEarned = 0;
this.charId = 'kaito';
this._screenFrom = 'scr-menu';
// developer mode (session-scoped)
this.timeScale = 1;
this.devGod = false;
this.devOneHit = false;
this.freezeT = 0; // hit-stop / slow-mo on big kills
// director state
this.spawnAcc = 0;
this.lastBurstMinute = -1;
this.eliteIdx = 0;
this.bossIdx = 0;
this.boss = null;
// input
this.keys = new Set();
this.touchVec = { x: 0, y: 0 };
this._bindInput();
this._resize();
window.addEventListener('resize', () => this._resize());
document.addEventListener('visibilitychange', () => {
if (document.hidden && this.state === ST.RUN && Store.s().autoPause) this.togglePause(true);
});
this.applyQuality();
this._last = performance.now();
const loop = (t) => {
const dt = Math.min(0.05, (t - this._last) / 1000);
this._last = t;
this.lastDt = dt;
this.frame(dt);
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
/* ================= setup ================= */
_resize() {
this.dpr = Math.min(window.devicePixelRatio || 1, 2);
this.cv.width = innerWidth * this.dpr;
this.cv.height = innerHeight * this.dpr;
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
R._vig = null;
}
applyQuality() {
const q = Store.s().particles;
this.partCap = q === 'low' ? 120 : q === 'med' ? 320 : 650;
}
applySettings() {
Snd.applyVolumes();
UI.els.fpsMeter.classList.toggle('hidden', !Store.s().fps);
this.applyQuality();
}
/* ================= run lifecycle ================= */
startRun(charId, setup) {
setup = setup || {};
this.setup = setup;
// deterministic seed for Daily Challenge; null restores Math.random
seedRng(setup.seed !== undefined ? setup.seed : null);
this.charId = charId || this._charSelDefault();
const stageId = STAGES[setup.stage] ? setup.stage : 'graveyard';
this.stage = STAGES[stageId];
this.endless = !!setup.endless;
this.grade = setup.grade | 0;
this.daily = !!setup.daily;
this.diffMul = (this.stage.hpMul || 1) * (1 + this.grade * 0.32);
this.goldMul = (this.stage.goldMul || 1) * (1 + this.grade * 0.15);
this.finalBossId = this.stage.plan[this.stage.plan.length - 1].id;
this.propAcc = 0;
this.lastSurge = -1;
// tutorial state (skipped once completed)
this._tutStep = Store.data.flags.tutDone ? -1 : (setup.daily ? -1 : 0);
this._tutMoved = 0;
this.player = new Player(this.charId);
this.enemies.length = 0;
this.projs.length = 0;
this.ebullets.length = 0;
this.pickups.length = 0;
this.effects.length = 0;
this.texts.length = 0;
this.time = 0;
this.over = false;
this.playerHidden = false;
this.spawnAcc = 0;
this.lastBurstMinute = -1;
this.eliteIdx = 0;
this.bossIdx = 0;
this.boss = null;
this.queueLevelUps = 0;
this.pendingChests = 0;
this.goldEarned = 0;
this.rerollsLeft = this.player.rerollMax;
this.cam.x = 0; this.cam.y = 0;
this.shake = 0;
this.hurtFlash = 0;
this.state = ST.RUN;
UI.showScreen(null);
UI.showHud(true);
UI.hideBossBar();
Snd.setFlavor(this.stage.music || null);
UI.toast(tr('tip_move'));
Snd.init();
}
_charSelDefault() { return UI._charSel || 'kaito'; }
frame(dt) {
// developer tools visibility (cheap toggle)
UI.els.devBtn.classList.toggle('hidden',
!(Store.s().dev && (this.state === ST.RUN || this.state === ST.PAUSE ||
this.state === ST.LEVEL || this.state === ST.CHEST)));
switch (this.state) {
case ST.RUN:
this.update(dt * (this.timeScale || 1));
R.draw(this);
UI.hudUpdate(this, dt);
break;
case ST.PAUSE:
case ST.LEVEL:
case ST.CHEST:
R.draw(this); // frozen world behind modal
break;
case ST.OVER:
R.draw(this);
break;
default:
this._renderMenuBg(dt);
}
}
update(dt) {
// hit-stop: brief slow-mo window after elite/boss kills
if (this.freezeT > 0) {
this.freezeT -= dt;
dt *= 0.12;
}
const P = this.player;
this.time += dt;
// ---- movement input ----
let mx = 0, my = 0;
const kb = Store.s().keybinds;
if (this.keys.has(kb.up) || this.keys.has('ArrowUp')) my -= 1;
if (this.keys.has(kb.down) || this.keys.has('ArrowDown')) my += 1;
if (this.keys.has(kb.left) || this.keys.has('ArrowLeft')) mx -= 1;
if (this.keys.has(kb.right) || this.keys.has('ArrowRight')) mx += 1;
// gamepad: left stick + d-pad + edge-triggered A/Start
if (typeof navigator !== 'undefined' && navigator.getGamepads) {
let gp = null;
try {
for (const p of navigator.getGamepads()) if (p && p.connected) { gp = p; break; }
} catch (e) { /* some browsers throw when no permission */ }
if (gp) {
const dz = v => (Math.abs(v) > 0.22 ? v : 0);
mx += dz(gp.axes[0] || 0);
my += dz(gp.axes[1] || 0);
const btn = i => !!(gp.buttons[i] && gp.buttons[i].pressed);
if (btn(12)) my -= 1;
if (btn(13)) my += 1;
if (btn(14)) mx -= 1;
if (btn(15)) mx += 1;
const prev = this._gpPrev || (this._gpPrev = {});
const now0 = btn(0), nowStart = btn(9) || btn(8);
if (now0 && !prev.b0) {
if (this.state === ST.LEVEL) this.chooseUpgrade(0);
else if (this.state === ST.CHEST) this.closeChest();
}
if (nowStart && !prev.start) {
if (this.state === ST.RUN || this.state === ST.PAUSE) this.togglePause();
}
prev.b0 = now0; prev.start = nowStart;
}
}
mx += this.touchVec.x; my += this.touchVec.y;
const ml = len2(mx, my);
if (ml > 1) { mx /= ml; my /= ml; }
P.x += mx * P.spd * dt;
P.y += my * P.spd * dt;
if (Math.abs(mx) + Math.abs(my) > 0.01) {
const fl = Math.max(0.001, len2(mx, my));
P.faceX = mx / fl; P.faceY = my / fl;
}
// tutorial: step 0 — learn to move; step 1 — make a level-up choice
if (this._tutStep === 0) {
UI.showTut('tut_move');
this._tutMoved += len2(mx, my) * P.spd * dt;
if (this._tutMoved > 260) { this._tutStep = 1; UI.hideTut(); }
} else if (this._tutStep === 1 && this.player.level >= 2) {
UI.showTut('tut_choose');
}
// regen / timers
if (P.regen > 0) P.hp = Math.min(P.maxhp, P.hp + P.regen * dt);
if (P.invuln > 0) P.invuln -= dt;
if (this.hurtFlash > 0) this.hurtFlash -= dt;
if (this.shake > 0) this.shake = Math.max(0, this.shake - dt * 26);
// systems
Sys.fireWeapons(this, dt);
Sys.spawnTick(this, dt);
Sys.updEnemies(this, dt);
Sys.updProjectiles(this, dt);
Sys.updEffects(this, dt);
Sys.updEBullets(this, dt);
Sys.updPickups(this, dt);
// particles (pooled)
for (let i = this.parts.length - 1; i >= 0; i--) {
const p = this.parts[i];
p.life -= dt;
if (p.life <= 0) {
removeItem(this.parts, i);
if (this.partPool.length < 500) this.partPool.push(p);
continue;
}
p.x += p.vx * dt; p.y += p.vy * dt;
p.vy += p.grav * dt;
const dr = Math.pow(p.drag, dt * 60);
p.vx *= dr; p.vy *= dr;
}
// camera
const k = 1 - Math.exp(-8 * dt);
this.cam.x += (P.x - this.cam.x) * k;
this.cam.y += (P.y - this.cam.y) * k;
// music intensity
Snd.setIntensity(Sys.musicIntensity(this));
// ---- flow: chests first, then level ups ----
if (this.state === ST.RUN) {
if (this.pendingChests > 0) {
this.pendingChests--;
this._openChestFlow();
return;
}
if (this.queueLevelUps > 0) {
this.queueLevelUps--;
this.currentChoices = Sys.makeChoices(this);
this.state = ST.LEVEL;
UI.hideModal('md-chest');
UI.openLevelUp(this.currentChoices, this.rerollsLeft);
return;
}
}
}
/* ================= flow helpers ================= */
chooseUpgrade(idx) {
if (this.state !== ST.LEVEL) return;
const c = this.currentChoices[idx];
if (!c) return;
Sys.applyChoice(this, c);
this._tutComplete();
this._afterModal();
}
/** Finish the tutorial once the player makes their first upgrade choice. */
_tutComplete() {
if (this._tutStep === 1 || !Store.data.flags.tutDone) {
Store.data.flags.tutDone = true;
Store.save();
UI.hideTut();
}
this._tutStep = -1;
}
skipLevelUp() {
if (this.state !== ST.LEVEL) return;
this.player.gold += 15;
this.goldEarned += 15;
this._afterModal();
}
rerollChoices() {
if (this.state !== ST.LEVEL || this.rerollsLeft <= 0) return;
this.rerollsLeft--;
Snd.play('click');
this.currentChoices = Sys.makeChoices(this);
UI.hideModal('md-level');
UI.openLevelUp(this.currentChoices, this.rerollsLeft);
}
_afterModal() {
UI.hideModal('md-level');
if (this.pendingChests > 0) {
this.pendingChests--;
this._openChestFlow();
return;
}
if (this.queueLevelUps > 0) {
this.queueLevelUps--;
this.currentChoices = Sys.makeChoices(this);
this.state = ST.LEVEL;
UI.openLevelUp(this.currentChoices, this.rerollsLeft);
return;
}
this.state = ST.RUN;
}
_openChestFlow() {
const data = Sys.chestReward(this);
this.state = ST.CHEST;
UI.hideModal('md-level');
UI.openChest(data);
}
closeChest() {
UI.hideModal('md-chest');
if (this.queueLevelUps > 0) {
this.queueLevelUps--;
this.currentChoices = Sys.makeChoices(this);
this.state = ST.LEVEL;
UI.openLevelUp(this.currentChoices, this.rerollsLeft);
return;
}
if (this.pendingChests > 0) {
this.pendingChests--;
this._openChestFlow();
return;
}
this.state = ST.RUN;
}
/* ================= events ================= */
onBossDead(e) {
if (e.tid === 'b_wolf' && !Store.data.flags.boss1) {
Store.data.flags.boss1 = true;
Store.save();
UI.toast(tr('toast_unlock', { c: tr('c_rook') }), 'gold');
}
if (this.boss === e) {
this.boss = null;
UI.hideBossBar();
}
if (e.tid === this.finalBossId && !this.endless) {
Snd.play('win', true);
this.gameOver(true);
} else {
if (e.tid === this.finalBossId && this.endless) {
UI.toast(tr('toast_endless_on'), 'gold');
}
this.shakeIt(10);
Snd.play('boom', true);
}
}
onPlayerDown() {
const P = this.player;
if (P.revives > 0) {
P.revives--;
P.hp = Math.round(P.maxhp * 0.6);
P.invuln = 2.2;
this.effects.push(new Effect('nova', P.x, P.y, 0.6, { r: 300, col: '#ffe066' }));
for (const e of this.enemies) {
if (!e.boss && dist2(P.x, P.y, e.x, e.y) < 280 * 280) {
Sys.damageEnemy(this, e, 9999, {});
}
}
UI.toast(tr('toast_revive'), 'gold');
Snd.play('evolve', true);
return;
}
Snd.play('death', true);
this.gameOver(false);
}
gameOver(win) {
if (this.over) return;
this.over = true;
this.state = ST.OVER;
const P = this.player;
// persist progression
Store.data.totals.kills += P.kills;
Store.data.totals.runs++;
Store.addGold(P.gold);
let newRecord = false;
if (this.time > Store.data.totals.best && this.time >= 30) {
Store.data.totals.best = Math.floor(this.time);
newRecord = true;
}
if (this.time >= 600) Store.data.flags.time10 = true;
if (this.time >= 900) Store.data.flags.time15 = true;
if (win) Store.data.totals.wins++;
// stage / mode records + run history
const sec = Math.floor(this.time);
const prog = Store.data.progress;
if (win) {
prog.stageWins[this.stage.id] = (prog.stageWins[this.stage.id] || 0) + 1;
prog.gradeWins[this.grade] = (prog.gradeWins[this.grade] || 0) + 1;
}
if (win || sec > (prog.bestPerStage[this.stage.id] || 0)) {
prog.bestPerStage[this.stage.id] = Math.max(prog.bestPerStage[this.stage.id] || 0, win ? this.stage.length : sec);
}
if (this.endless && sec > (prog.endlessBest || 0)) { prog.endlessBest = sec; newRecord = true; }
Store.data.history.unshift({
d: Date.now(), ch: this.charId, st: this.stage.id,
en: this.endless ? 1 : 0, g: this.grade,
t: sec, k: P.kills, gold: P.gold, lv: P.level, w: win ? 1 : 0
});
if (Store.data.history.length > 20) Store.data.history.length = 20;
// daily challenge record
if (this.daily) {
const today = dailyKey();
const d = Store.data.daily;
if (d.date !== today) { d.date = today; d.best = 0; }
if (sec > (d.best || 0)) {
d.best = sec;
UI.toast(tr('toast_daily_record'), 'gold');
}
}
Store.save();
UI.checkAndToastAch();
this._lbSubmit(win, sec);
setTimeout(() => {
UI.openOver(win, {
time: this.time,
kills: P.kills,
goldEarned: P.gold,
level: P.level,
best: Store.data.totals.best,
newRecord
});
}, win ? 400 : 700);
if (win) this.shakeIt(12);
}
/** Best-effort leaderboard submit (silent when no server configured). */
_lbSubmit(win, sec) {
const s = Store.s();
if (!s.lbUrl || sec < 30) return;
try {
const payload = {
name: (Store.data.lbName || 'Anon').slice(0, 16),
time: sec, kills: this.player.kills, gold: this.player.gold,
lv: this.player.level, stageId: this.stage.id, grade: this.grade,
endless: !!this.endless, daily: !!this.daily, win: !!win,
day: new Date().toISOString().slice(0, 10).replace(/-/g, '')
};
payload.sig = lbSig(payload);
fetch(s.lbUrl.replace(/\/$/, '') + '/submit', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
}).then(r => { if (r.ok) UI.toast(tr('lb_sent'), 'gold'); })
.catch(() => {});
} catch (e) { /* offline — ignore */ }
}
togglePause(forcePause) {
if (this.state === ST.RUN) {
this.state = ST.PAUSE;
UI.openPause(this);
} else if (this.state === ST.PAUSE && !forcePause) {
this.state = ST.RUN;
UI.hideModal('md-pause');
}
}
quitToMenu() {
Snd.setFlavor(null);
// abandoning a live run still banks gold/kills/time records
if (this.player && !this.over && this.time > 5) {
const P = this.player;
Store.data.totals.kills += P.kills;
Store.data.totals.runs++;
Store.addGold(P.gold);
if (this.time > Store.data.totals.best && this.time >= 30)
Store.data.totals.best = Math.floor(this.time);
if (this.time >= 600) Store.data.flags.time10 = true;
if (this.time >= 900) Store.data.flags.time15 = true;
this.over = true;
}
Store.save();
this.state = ST.MENU;
UI.hideModal('md-pause');
UI.hideModal('md-over');
UI.showHud(false);
UI.showScreen('scr-menu');
Snd.setIntensity(0.2);
}
restart() {
UI.hideModal('md-over');
this.startRun(this.charId);
}
onBackFromScreen() {
// returning from options/codex/shop/chars/stage screens
const visible = ['scr-menu', 'scr-chars', 'scr-stage', 'scr-shop', 'scr-codex', 'scr-options']
.find(id => !UI.els[id].classList.contains('hidden'));
if (this.state === ST.MENU || visible === 'scr-menu') {
UI.showScreen('scr-menu');
} else if (visible === 'scr-options' && this.state === ST.PAUSE) {
UI.showScreen(null);
UI.openPause(this);
} else {
// browsing menus pre-run
if (visible === 'scr-stage') { UI.buildChars(); UI.showScreen('scr-chars'); }
else UI.showScreen('scr-menu');
}
}
buyMeta(id) {
const def = META_SHOP.find(m => m.id === id);
if (!def) return;
const rank = Store.rank(id);
if (rank >= def.max) return;
const cost = metaCost(def, rank);
if (!Store.spendGold(cost)) return;
Store.data.meta[id] = rank + 1;
Store.save();
if (this.player) this.player.recompute();
UI.buildShop();
UI.refreshHeadGold();
UI.checkAndToastAch();
}
/* ================= developer mode ================= */
devAddGold(n) {
const P = this.player;
P.gold += n; this.goldEarned += Math.max(0, n);
Snd.play('coin');
}
devSetGold(v) {
v = Math.max(0, Math.floor(v) || 0);
this.goldEarned += Math.max(0, v - this.player.gold);
this.player.gold = v;
}
devAddLevels(n) {
const P = this.player;
for (let i = 0; i < n; i++) {
this.queueLevelUps += P.addXp(P.xpNext);
}
}
devGiveWeapon(id, maxIt) {
const P = this.player;
let w = P.weaponById(id);
if (EVOLVED[id]) {
if (!w) P.weapons.push({ id, lvl: 1, timer: 0.2 });
} else if (w) {
w.lvl = maxIt ? WEAPONS[id].max : Math.min(WEAPONS[id].max, w.lvl + 1);
} else {
P.weapons.push({ id, lvl: maxIt ? WEAPONS[id].max : 1, timer: 0.2 });
}
Snd.play('chest');
}
devGivePassive(id, maxIt) {
const P = this.player;
P.passives[id] = maxIt ? PASSIVES[id].max
: Math.min(PASSIVES[id].max, (P.passives[id] || 0) + 1);
P.recompute();
Snd.play('levelup');
}
devHeal() { this.player.hp = this.player.maxhp; Snd.play('heal'); }
devMagnetAll() {
for (const pk of this.pickups) pk.pull = true;
Snd.play('heal');
}
devNuke(radius) {
const P = this.player;
const r = radius || 900;
this.effects.push(new Effect('nova', P.x, P.y, 0.5, { r, col: '#ffd24a' }));
for (const e of this.enemies) {
if (dist2(P.x, P.y, e.x, e.y) < r * r) Sys.damageEnemy(this, e, 9999999, {});
}
Snd.play('boom');
this.shakeIt(10);
}
devSkipTime(sec) { this.time += sec; }
devSpawnElite() { Sys.spawnElite(this); }
devSpawnBoss(id) {
if (!BOSSES[id]) return;
const pos = Sys.ringPos(this, 520);
const b = new Enemy(id, pos.x, pos.y);
b.hp *= 1 + Math.floor(this.time / 60) * 0.06;
b.maxhp = b.hp;
this.enemies.push(b);
this.boss = b;
UI.showBossBar(b);
UI.toast(tr('toast_boss'), 'warn');
Snd.play('roar', true);
this.shakeIt(8);
}
devUnlockAllChars() {
for (const id in CHARS) Store.unlockChar(id);
Store.save();
UI.toast('🔓 ' + tr('dev_unlockall'), 'gold');
}
shakeIt(a) {
if (Store.s().shake) this.shake = Math.min(14, this.shake + a);
}
/* ================= input ================= */
_bindInput() {
window.addEventListener('keydown', (e) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(e.code)) e.preventDefault();
this.keys.add(e.code);
Snd.init();
// developer mode shortcuts
if (Store.s().dev && e.code === 'Backquote') {
UI.toggleDevPanel();
return;
}
if (e.code === 'Escape' && !UI.els['md-dev'].classList.contains('hidden')) {
UI.hideModal('md-dev');
return;
}
if ((e.code === 'Escape' || e.code === 'KeyP')) {
if (this.state === ST.RUN || this.state === ST.PAUSE) this.togglePause();
else if (this.state === ST.LEVEL) this.skipLevelUp();
else if (this.state === ST.CHEST) this.closeChest();
}
if (e.code === 'Digit1') this.chooseUpgrade(0);
if (e.code === 'Digit2') this.chooseUpgrade(1);
if (e.code === 'Digit3') this.chooseUpgrade(2);
if (e.code === 'Digit4') this.chooseUpgrade(3);
});
window.addEventListener('keyup', (e) => this.keys.delete(e.code));
// ---- touch joystick ----
const layer = document.getElementById('touchLayer');
const base = document.getElementById('stickBase');
const knob = document.getElementById('stickKnob');
let tid = null, bx = 0, by = 0;
const isTouch = ('ontouchstart' in window);
window.addEventListener('touchstart', (e) => {
Snd.init();
if (!isTouch) return;
layer.classList.remove('hidden');
const t = e.changedTouches[0];
tid = t.identifier;
bx = t.clientX; by = t.clientY;
base.style.display = 'block';
base.style.left = (bx - 55) + 'px';
base.style.top = (by - 55) + 'px';
knob.style.transform = 'translate(-50%,-50%)';
}, { passive: true });
window.addEventListener('touchmove', (e) => {
for (const t of e.changedTouches) {
if (t.identifier !== tid) continue;
let dx = t.clientX - bx, dy = t.clientY - by;
const d = len2(dx, dy);
const max = 46;
if (d > max) { dx = dx / d * max; dy = dy / d * max; }
knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
this.touchVec.x = dx / max;
this.touchVec.y = dy / max;
}
}, { passive: true });
const endTouch = (e) => {
for (const t of e.changedTouches) {
if (t.identifier !== tid) continue;
tid = null;
this.touchVec.x = 0; this.touchVec.y = 0;
base.style.display = 'none';
}
};
window.addEventListener('touchend', endTouch);
window.addEventListener('touchcancel', endTouch);
}
/* ================= menu backdrop ================= */
_renderMenuBg(dt) {
R.drawMenuScene(this, dt);
}
}