Files
MeatSlicer/js/main.js

582 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---- bootstrap, game loop, mode dispatch ----
let canvas, ctx, lastT = 0;
const PRESSURE_SLIDER_RECT = { x: 300, y: 470, w: 360, h: 46 };
const MENU_EXIT_BUTTON = { x: W - 208, y: H - 48, w: 180, h: 28 };
const PAUSE_MENU_BUTTON = { x: W / 2 - 75, y: 526, w: 150, h: 28 };
const PAUSE_EXIT_BUTTON = { x: W / 2 + 87, y: 526, w: 150, h: 28 };
const CONFIRM_WINDOW = 3;
function pauseBars() {
const x = W / 2 - 55, w = 225;
return [
{ rect: { x, y: 418, w, h: 10 }, set: v => Sfx.setVolume(v) },
{ rect: { x, y: 446, w, h: 10 }, set: v => Music.setVolume(v) },
{ rect: { x, y: 474, w, h: 10 }, set: v => setHudAlpha(0.25 + v * 0.75) },
];
}
function init() {
canvas = document.getElementById('game');
ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
G.devMode = debugEnabled();
initInput(canvas);
Sprites.load();
fitCanvas();
buildAtmosphereLayer();
window.addEventListener('resize', fitCanvas);
if (document.addEventListener) {
document.addEventListener('visibilitychange', () => {
lastT = performance.now();
if (document.hidden) { Input.keys = {}; Input.mdown = false; }
});
}
try { G.best = parseInt(localStorage.getItem('meatslicer_best') || '0', 10) || 0; }
catch (e) { G.best = 0; }
try {
const sv = parseFloat(localStorage.getItem('meatslicer_sfx_volume'));
const mv = parseFloat(localStorage.getItem('meatslicer_music_volume'));
if (Number.isFinite(sv)) G.sfxVol = clamp(sv, 0, 1);
if (Number.isFinite(mv)) G.musicVol = clamp(mv, 0, 1);
G.autoPerk = localStorage.getItem('meatslicer_autoperk') === '1';
const pd = parseFloat(localStorage.getItem('meatslicer_pressure_dial'));
if (Number.isFinite(pd)) G.pressureDial = clamp(Math.round(pd), PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX);
const ha = parseFloat(localStorage.getItem('meatslicer_hud_alpha'));
if (Number.isFinite(ha)) G.hudAlpha = clamp(ha, 0.25, 1);
} catch (e) {}
requestAnimationFrame(loop);
}
function setPressureDial(n) {
G.pressureDial = clamp(Math.round(n), PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX);
try { localStorage.setItem('meatslicer_pressure_dial', String(G.pressureDial)); } catch (e) {}
Sfx.menu();
}
function setHudAlpha(v) {
G.hudAlpha = clamp(Math.round(v * 100) / 100, 0.25, 1);
try { localStorage.setItem('meatslicer_hud_alpha', String(G.hudAlpha)); } catch (e) {}
}
function setAutoPerk(enabled) {
G.autoPerk = !!enabled;
try { localStorage.setItem('meatslicer_autoperk', G.autoPerk ? '1' : '0'); } catch (e) {}
addToast('AUTO-DRAFT ' + (G.autoPerk ? 'ON' : 'OFF'), G.autoPerk ? 'level-ups now choose randomly' : 'manual mutation drafts restored');
Sfx.menu();
}
function buildAtmosphereLayer() {
if (!document.createElement) return;
const layer = document.createElement('canvas');
layer.width = W; layer.height = H;
const c = layer.getContext('2d');
const vignette = c.createRadialGradient(W / 2, H / 2, 170, W / 2, H / 2, 590);
vignette.addColorStop(0, 'rgba(0,0,0,0)');
vignette.addColorStop(0.72, 'rgba(12,2,5,0.12)');
vignette.addColorStop(1, 'rgba(4,1,2,0.64)');
c.fillStyle = vignette; c.fillRect(0, 0, W, H);
// Fixed grain gives the room texture without per-frame random allocations.
for (let i = 0; i < 380; i++) {
const x = hashTile(i * 17, 11) % W, y = hashTile(i * 29, 23) % H;
c.fillStyle = i % 3 ? 'rgba(255,220,205,0.018)' : 'rgba(0,0,0,0.035)';
c.fillRect(x, y, 1, 1);
}
G.atmosphereLayer = layer;
}
function fitCanvas() {
const k = Math.min(window.innerWidth / W, window.innerHeight / H) * 0.97;
canvas.style.width = Math.floor(W * k) + 'px';
canvas.style.height = Math.floor(H * k) + 'px';
}
function resetRun() {
Sfx.init(); Sfx.resume();
Sfx.stopAllLoops(); Sfx.menu();
G.floor = 1; G.score = 0; G.kills = 0; G.time = 0;
G.parts = []; G.corpses = []; G.toasts = []; G.pendingLevelups = 0; G.perkChoices = null;
G.pauseHelp = false; G.helpPage = 0;
G.confirmAction = null; G.confirmT = 0;
G.deathLockT = 0;
G.pressure = 1; G.streak = 0; G.roomDamaged = false; G.roomEnterT = 0; G.recentHits = [];
G.shake = 0; G.flash = 0;
G.debugUsed = false; G.debugFlags = {}; G.debugTimescale = 1; G.debugFrameStep = false;
initPlayer();
genFloor(1);
enterRoom(0, 0);
G.player.shieldHp = G.player.stats.shieldPerk || 0;
}
function startRun() {
resetRun();
G.mode = 'play';
Music.requestFloorMusic();
addToast('FLOOR 1', 'clear the rooms. find the stairs. feed.');
}
function returnToMenu() {
resetRun();
G.mode = 'menu';
G.toasts = [];
Music.playMenu();
}
function canQuitDesktop() {
return !!(window.MSDesktop && typeof window.MSDesktop.quit === 'function');
}
function quitToDesktop() {
if (canQuitDesktop()) window.MSDesktop.quit();
}
function confirmAction(name, action) {
if (G.confirmAction === name && G.confirmT > 0) {
G.confirmAction = null; G.confirmT = 0;
action();
return;
}
G.confirmAction = name;
G.confirmT = CONFIRM_WINDOW;
Sfx.menu();
}
function gameOver() {
G.mode = 'gameover';
G.deathLockT = DEATH_LOCK;
spawnPlayerGore(G.player.x, G.player.y);
Sfx.playerDeath();
if (!G.debugUsed && G.score > (G.best || 0)) {
G.best = G.score;
try { localStorage.setItem('meatslicer_best', String(G.best)); } catch (e) {}
}
Music.playMenu();
}
function loop(now) {
let dt = Math.min((now - lastT) / 1000 || 0.016, 0.05);
lastT = now;
dt *= (G.debugTimescale || 1);
if (G.debugFlags.freeze) {
if (G.debugFrameStep) { G.debugFrameStep = false; dt = 1 / 60; }
else dt = 0;
}
update(dt);
draw();
clearInputEdges();
requestAnimationFrame(loop);
}
function update(dt) {
Music.update(dt);
if (G.deathLockT > 0) G.deathLockT = Math.max(0, G.deathLockT - dt);
if (G.confirmT > 0) {
G.confirmT = Math.max(0, G.confirmT - dt);
if (G.confirmT <= 0) G.confirmAction = null;
}
// global keys
if (keyPressed('m')) {
G.muted = !G.muted;
Music.setMuted(G.muted);
Sfx.setMuted(G.muted);
}
switch (G.mode) {
case 'menu': {
const sl = PRESSURE_SLIDER_RECT;
const exitClick = Input.mpressed && inRect(Input.mx, Input.my, MENU_EXIT_BUTTON);
if (canQuitDesktop() && (keyPressed('x') || exitClick)) {
confirmAction('desktop', quitToDesktop);
break;
}
if (keyPressed('arrowleft', 'a', '-', '_')) setPressureDial(G.pressureDial - 1);
if (keyPressed('arrowright', 'd', '=', '+')) setPressureDial(G.pressureDial + 1);
if (Input.mpressed && inRect(Input.mx, Input.my, sl)) G.menuDialDrag = true;
if (G.menuDialDrag && Input.mdown) {
setPressureDial(Math.round(((Input.mx - sl.x) / sl.w) * 20) - 10);
}
if (!Input.mdown) G.menuDialDrag = false;
if (keyPressed('enter', ' ') || (Input.mpressed && !inRect(Input.mx, Input.my, sl) && !exitClick)) startRun();
break;
}
case 'play':
G.time += dt;
if (G.devMode && keyPressed('`', '~')) { G.debugReturn = 'play'; G.mode = 'debug'; Sfx.menu(); break; }
if (keyPressed('p', 'escape')) { G.mode = 'pause'; G.confirmAction = null; G.confirmT = 0; Sfx.stopAllLoops(); Sfx.menu(); break; }
if (keyPressed('n')) Music.cycle(1);
if (keyPressed('r')) swapWeapon();
if (keyPressed(' ')) useActive();
if (keyPressed('t')) setAutoPerk(!G.autoPerk);
updatePlayer(dt);
updateRoom(dt);
updateEnemies(dt);
updateTelegraphs(dt);
updateBullets(dt);
updateHazards(dt);
updatePickups(dt);
updateParticles(dt);
drawToastsUpdate(dt);
break;
case 'levelup':
updateLevelup(dt);
updateParticles(dt);
break;
case 'debug':
updateDebug();
// pinned mode lets the world keep running behind the panel
if (G.debugPin) {
G.time += dt;
// mask trigger input so clicking panel buttons doesn't fire the weapon
const md = Input.mdown, mp = Input.mpressed, mr = Input.mreleased;
Input.mdown = Input.mpressed = Input.mreleased = false;
updatePlayer(dt);
Input.mdown = md; Input.mpressed = mp; Input.mreleased = mr;
updateRoom(dt); updateEnemies(dt); updateTelegraphs(dt);
updateBullets(dt); updateHazards(dt); updatePickups(dt); updateParticles(dt);
drawToastsUpdate(dt);
} else {
updateParticles(dt);
}
break;
case 'pause': {
if (G.pauseHelp) { updatePauseHelp(); break; }
if (G.devMode && keyPressed('`', '~')) { G.debugReturn = 'pause'; G.mode = 'debug'; Sfx.menu(); break; }
const menuClick = Input.mpressed && inRect(Input.mx, Input.my, PAUSE_MENU_BUTTON);
const exitClick = Input.mpressed && inRect(Input.mx, Input.my, PAUSE_EXIT_BUTTON);
if (keyPressed('q') || menuClick) { confirmAction('menu', returnToMenu); break; }
if (canQuitDesktop() && (keyPressed('x') || exitClick)) { confirmAction('desktop', quitToDesktop); break; }
if (keyPressed('p', 'escape')) { G.mode = 'play'; G.confirmAction = null; G.confirmT = 0; Sfx.menu(); break; }
if (keyPressed('h') || (Input.mpressed && inRect(Input.mx, Input.my, HELP_BUTTON))) {
G.pauseHelp = true; G.helpPage = 0; G.confirmAction = null; G.confirmT = 0; Sfx.menu(); break;
}
if (keyPressed('r')) swapWeapon(true);
if (keyPressed('t')) setAutoPerk(!G.autoPerk);
if (keyPressed('-', '_')) { Sfx.setVolume(G.sfxVol - 0.1); Sfx.menu(); }
if (keyPressed('=', '+')) { Sfx.setVolume(G.sfxVol + 0.1); Sfx.menu(); }
if (keyPressed(',', '<')) { Music.setVolume(G.musicVol - 0.1); Sfx.menu(); }
if (keyPressed('.', '>')) { Music.setVolume(G.musicVol + 0.1); Sfx.menu(); }
if (keyPressed(';', ':')) { setHudAlpha(G.hudAlpha - 0.1); Sfx.menu(); }
if (keyPressed("'", '"')) { setHudAlpha(G.hudAlpha + 0.1); Sfx.menu(); }
// click-and-drag on the three pause sliders
{
const bars = pauseBars();
if (Input.mpressed) {
for (let i = 0; i < bars.length; i++) if (inRect(Input.mx, Input.my, bars[i].rect)) { G.pauseDrag = i; break; }
}
if (G.pauseDrag !== null && G.pauseDrag !== undefined && Input.mdown) {
const b = bars[G.pauseDrag];
const v = clamp((Input.mx - b.rect.x) / b.rect.w, 0, 1);
b.set(v);
}
if (!Input.mdown) G.pauseDrag = null;
}
if (keyPressed('arrowleft', '[')) Music.cycle(-1);
else if (keyPressed('arrowright', ']')) Music.cycle(1);
else if (Input.mpressed) {
// jukebox arrows
if (dist(Input.mx, Input.my, W / 2 - 200, H / 2 + 60) < 22) Music.cycle(-1);
else if (dist(Input.mx, Input.my, W / 2 + 200, H / 2 + 60) < 22) Music.cycle(1);
}
break;
}
case 'gameover':
updateParticles(dt);
if (G.deathLockT <= 0 && (keyPressed('r', 'enter', ' ') || Input.mpressed)) startRun();
break;
}
if (G.shake > 0) G.shake = Math.max(0, G.shake - dt * 30);
if (G.flash > 0) G.flash = Math.max(0, G.flash - dt);
if (G.transition > 0) G.transition = Math.max(0, G.transition - dt);
}
function draw() {
ctx.fillStyle = '#0a0506';
ctx.fillRect(0, 0, W, H);
if (G.mode === 'menu') { drawMenu(ctx); return; }
// world (with screen shake)
ctx.save();
if (G.shake > 0) ctx.translate(rand(-G.shake, G.shake) * 0.5, rand(-G.shake, G.shake) * 0.5);
drawRoom(ctx);
drawCorpses(ctx);
drawPickups(ctx);
drawEnemies(ctx);
drawPlayer(ctx);
drawBullets(ctx);
drawParticles(ctx);
drawTelegraphs(ctx);
ctx.restore();
if (G.atmosphereLayer) ctx.drawImage(G.atmosphereLayer, 0, 0);
// Fine inner frame and corner cuts finish the playfield presentation.
ctx.strokeStyle = 'rgba(104,36,43,0.55)'; ctx.lineWidth = 2;
ctx.strokeRect(2, 2, W - 4, H - 4);
ctx.strokeStyle = 'rgba(224,177,102,0.22)'; ctx.lineWidth = 1;
ctx.beginPath();
for (const [x, y, sx, sy] of [[8, 8, 1, 1], [W - 8, 8, -1, 1], [8, H - 8, 1, -1], [W - 8, H - 8, -1, -1]]) {
ctx.moveTo(x, y + sy * 20); ctx.lineTo(x, y); ctx.lineTo(x + sx * 20, y);
}
ctx.stroke();
drawHUD(ctx);
// hurt vignette
if (G.flash > 0) {
ctx.fillStyle = 'rgba(180,10,25,' + (G.flash * 0.55) + ')';
ctx.fillRect(0, 0, W, H);
}
// room transition fade
if (G.transition > 0) {
ctx.fillStyle = 'rgba(5,2,3,' + clamp(G.transition * 3, 0, 1) + ')';
ctx.fillRect(0, 0, W, H);
}
if (G.mode === 'levelup') drawLevelup(ctx);
else if (G.mode === 'pause') { drawPause(ctx); if (G.pauseHelp) drawPauseHelp(ctx); }
else if (G.mode === 'gameover') drawGameOver(ctx);
else if (G.mode === 'debug') drawDebug(ctx);
if (G.devMode) { drawDebugOverlays(ctx); if (G.debugPin && G.mode !== 'debug') drawDebugPin(ctx); }
}
function drawMenu(ctx) {
const t = performance.now() / 1000;
const bg = ctx.createRadialGradient && ctx.createRadialGradient(W / 2, H * 0.4, 30, W / 2, H * 0.45, 590);
if (bg && bg.addColorStop) {
bg.addColorStop(0, '#260b11'); bg.addColorStop(0.5, '#100609'); bg.addColorStop(1, '#050204');
ctx.fillStyle = bg;
} else ctx.fillStyle = '#100609';
ctx.fillRect(0, 0, W, H);
// Low-contrast industrial grid replaces decorative circles with a cleaner,
// modern system-screen backdrop while preserving hard pixel geometry.
ctx.lineWidth = 1;
for (let x = 0; x <= W; x += 48) {
ctx.strokeStyle = x % 192 === 0 ? 'rgba(121,35,48,0.11)' : 'rgba(121,35,48,0.045)';
ctx.beginPath(); ctx.moveTo(x + 0.5, 0); ctx.lineTo(x + 0.5, H); ctx.stroke();
}
for (let y = 0; y <= H; y += 48) {
ctx.strokeStyle = y % 192 === 0 ? 'rgba(121,35,48,0.11)' : 'rgba(121,35,48,0.045)';
ctx.beginPath(); ctx.moveTo(0, y + 0.5); ctx.lineTo(W, y + 0.5); ctx.stroke();
}
ctx.globalAlpha = 0.13;
Sprites.draw(ctx, 'bullet_cleaver', 168, 274, -0.2, 150);
Sprites.draw(ctx, 'bullet_cleaver', W - 168, 274, Math.PI + 0.2, 150);
ctx.globalAlpha = 1;
drawPixelTag(ctx, 'SLAUGHTER PROTOCOL // 01', 28, 24, {
width: 188, height: 24, color: '#a99089', accent: '#b6243a',
});
drawPixelTag(ctx, 'LOCAL RUNNER // ONLINE', W - 196, 24, {
width: 168, height: 24, color: '#74aeb5', accent: '#398b99',
});
ctx.textAlign = 'center';
ctx.font = '900 70px monospace';
ctx.fillStyle = '#39070d'; ctx.fillText('MEATSLICER', W / 2 + 4, 248);
ctx.fillStyle = '#f02b48'; ctx.fillText('MEATSLICER', W / 2, 243);
ctx.fillStyle = '#ff9a8a'; ctx.globalAlpha = 0.35; ctx.fillText('MEATSLICER', W / 2, 240); ctx.globalAlpha = 1;
drawBloodTrim(ctx, W / 2 - 230, 255, 460, 11, 0.65);
ctx.fillStyle = '#d4ad3d'; ctx.font = 'bold 12px monospace';
ctx.fillText('A BUTCHER\'S DESCENT // CLEAR · HARVEST · DESCEND', W / 2, 281);
const pulse = 0.75 + Math.sin(t * 3) * 0.18;
drawPixelPanel(ctx, W / 2 - 180, 320, 360, 64, {
accent: '#e22b46', border: 'rgba(218,40,62,' + pulse + ')', lineWidth: 2,
fill: 'rgba(12,4,7,0.9)', blood: true, seed: 4, bloodAlpha: 0.35,
});
ctx.fillStyle = '#7f6062'; ctx.font = 'bold 9px monospace';
ctx.fillText('INITIATE DESCENT', W / 2, 341);
ctx.fillStyle = '#f5e9d6'; ctx.font = 'bold 14px monospace';
ctx.fillText('[ ENTER ] OR CLICK TO START', W / 2, 365);
const controls = ['WASD MOVE', 'MOUSE AIM', 'LMB FIRE', 'R SWAP', 'P PAUSE', 'T AUTO', 'M MUTE', 'N MUSIC'];
ctx.font = 'bold 9px monospace';
const widths = controls.map(label => Math.ceil(ctx.measureText(label).width) + 16);
const total = widths.reduce((sum, width) => sum + width, 0) + (controls.length - 1) * 6;
let tx = (W - total) / 2;
for (let i = 0; i < controls.length; i++) {
drawPixelTag(ctx, controls[i], tx, 420, { width: widths[i], height: 24, color: '#a9958e', accent: i < 3 ? '#8f2637' : '#5f3943' });
tx += widths[i] + 6;
}
// ---- pressure dial ----
drawPressureDial(ctx);
ctx.fillStyle = '#655155'; ctx.font = '9px monospace';
ctx.fillText('ROOM-CLEAR COMBAT // PERK DRAFTS // SIXTEEN WEAPONS // CROWNED MEAT', W / 2, 530);
if (G.best) {
drawPixelTag(ctx, 'BEST CUT ' + String(G.best).padStart(6, '0'), W / 2 - 74, 556, {
width: 148, height: 26, color: '#e0b94e', accent: '#a47e25',
});
}
const exitHover = inRect(Input.mx, Input.my, MENU_EXIT_BUTTON);
const desktopExit = canQuitDesktop();
const exitConfirm = desktopExit && G.confirmAction === 'desktop';
drawPixelTag(ctx, !desktopExit ? '[X] DESKTOP APP ONLY' : (exitConfirm ? '[X] CONFIRM EXIT' : '[X] EXIT TO DESKTOP'), MENU_EXIT_BUTTON.x, MENU_EXIT_BUTTON.y, {
width: MENU_EXIT_BUTTON.w, height: MENU_EXIT_BUTTON.h,
color: !desktopExit ? '#65595b' : (exitConfirm ? '#ffd36a' : (exitHover ? '#f5e9d6' : '#ad8f8c')),
accent: !desktopExit ? '#3a2d31' : (exitConfirm ? '#d69a22' : (exitHover ? '#e22b46' : '#6e3843')),
});
}
function drawPressureDial(ctx) {
const sl = PRESSURE_SLIDER_RECT;
const d = G.pressureDial;
const gain = pressureGainUnits(d), drop = pressureDropUnits(d);
const accent = d < 0 ? '#3d8f99' : (d === 0 ? '#c59231' : '#e22b46');
drawPixelPanel(ctx, sl.x, sl.y, sl.w, sl.h, {
cut: 6, shadow: false, accent, fill: 'rgba(12,5,8,0.92)', border: '#63303a',
});
const name = d <= -9 ? 'FORGIVING' : d <= -6 ? 'MERCIFUL' : d <= -3 ? 'EASED' : d <= 0 ? 'STANDARD' :
d <= 3 ? 'TENSE' : d <= 5 ? 'SAVAGE' : d <= 7 ? 'RELENTLESS' : d <= 9 ? 'ATROCIOUS' : 'MEAT GRINDER';
ctx.textAlign = 'left'; ctx.fillStyle = '#9e857a'; ctx.font = 'bold 8px monospace';
ctx.fillText('PRESSURE DIAL', sl.x + 10, sl.y + 12);
ctx.textAlign = 'right'; ctx.fillStyle = accent; ctx.font = 'bold 9px monospace';
ctx.fillText((d > 0 ? '+' : '') + d + ' ' + name, sl.x + sl.w - 10, sl.y + 12);
// track with 20 steps / 21 notches
const bx = sl.x + 34, bw = sl.w - 68, by = sl.y + 20, bh = 10;
drawPixelBar(ctx, bx, by, bw, bh, (d + 10) / 20, {
fill: accent, glint: '#f4d86d', border: '#5d3c43', segments: 20,
});
// bright notch marker
const nx = bx + Math.round((d + 10) / 20 * bw);
ctx.fillStyle = '#f5e9d6';
ctx.fillRect(nx - 1, by - 2, 2, bh + 4);
// end labels
ctx.fillStyle = '#7d6262'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'center';
ctx.fillText('-10', bx - 18, by + 9); ctx.fillText('+10', bx + bw + 18, by + 9);
// derived readout
ctx.fillStyle = '#8f7770'; ctx.font = '8px monospace';
const rise = (gain >= 0 ? '+' : '') + gain.toFixed(1);
ctx.fillText('RISE ' + rise + '/10 · RELIEF ' + drop.toFixed(1) + '/10 · SCORE ×LIVE PRESSURE', sl.x + sl.w / 2, sl.y + sl.h - 7);
}
function drawPause(ctx) {
ctx.fillStyle = 'rgba(4,2,3,0.78)'; ctx.fillRect(0, 0, W, H);
drawPixelPanel(ctx, W / 2 - 270, 104, 540, 452, {
accent: '#c92a40', border: 'rgba(119,51,61,0.95)', fill: 'rgba(11,5,8,0.96)',
blood: true, seed: 8, bloodAlpha: 0.25,
});
ctx.textAlign = 'center';
ctx.fillStyle = '#7f6768'; ctx.font = 'bold 9px monospace';
ctx.fillText('RUN STATE // SUSPENDED', W / 2, 136);
ctx.fillStyle = '#f0e5d7'; ctx.font = 'bold 36px monospace';
ctx.fillText('PAUSED', W / 2, 180);
drawBloodTrim(ctx, W / 2 - 130, 191, 260, 8, 0.45);
ctx.fillStyle = '#b89532'; ctx.font = 'bold 10px monospace';
ctx.fillText('JUKEBOX // ACTIVE TRACK', W / 2, 248);
const sel = (G.musicSel === undefined || G.musicSel < 0) ? 'AUTO · FLOOR ROTATION' : Music.pretty(Music.PLAYLIST[G.musicSel]).toUpperCase();
drawPixelPanel(ctx, W / 2 - 170, 318, 340, 82, {
cut: 5, shadow: false, accent: '#468e9d', fill: 'rgba(5,10,12,0.7)', border: '#315b65',
});
ctx.fillStyle = '#8faeb2'; ctx.font = 'bold 9px monospace';
ctx.fillText('NOW PLAYING', W / 2, 341);
ctx.fillStyle = '#e9ded0'; ctx.font = 'bold 14px monospace';
ctx.fillText(sel, W / 2, 368);
for (const sx of [-200, 200]) {
const x = W / 2 + sx, y = 358;
const hover = dist(Input.mx, Input.my, x, y) < 22;
drawPixelPanel(ctx, x - 20, y - 20, 40, 40, {
cut: 5, shadow: false, fill: hover ? '#631526' : '#251017',
border: hover ? '#ef3150' : '#65303b', accent: hover ? '#ef3150' : '#773142',
});
ctx.fillStyle = '#eee0d3'; ctx.font = 'bold 18px monospace';
ctx.fillText(sx < 0 ? '<' : '>', x, y + 6);
}
// three sliders: SFX / MUSIC / HUD (drag or key pairs)
const sliders = [
{ label: 'SFX [- / +]', val: G.sfxVol, colors: { fill: '#c92a40', glint: '#ff7d82', border: '#63303a', segments: 10 } },
{ label: 'MUSIC [, / .]', val: G.musicVol, colors: { fill: '#468e9d', glint: '#a5edee', border: '#315b65', segments: 10 } },
{ label: "HUD [; / ']", val: (G.hudAlpha - 0.25) / 0.75, colors: { fill: '#b5892f', glint: '#f4d86d', border: '#5d3c43', segments: 10 } },
];
const bars = pauseBars();
ctx.textAlign = 'left'; ctx.font = 'bold 9px monospace'; ctx.fillStyle = '#a99a91';
for (let i = 0; i < sliders.length; i++) {
const by = bars[i].rect.y;
ctx.fillStyle = '#a99a91'; ctx.fillText(sliders[i].label, W / 2 - 170, by + 9);
drawPixelBar(ctx, bars[i].rect.x, by, bars[i].rect.w, 10, sliders[i].val, sliders[i].colors);
ctx.fillStyle = '#e9ded0'; ctx.textAlign = 'right';
const pct = i === 2 ? Math.round(G.hudAlpha * 100) : Math.round(sliders[i].val * 100);
ctx.fillText(pct + '%', W / 2 + 185, by + 9);
ctx.textAlign = 'left';
}
const gridX = [W / 2 - 237, W / 2 - 75, W / 2 + 87];
drawPixelTag(ctx, '[P] RESUME', gridX[0], 492, { width: 150, height: 28, color: '#f0e5d7', accent: '#b5243a' });
drawPixelTag(ctx, '[R] SWAP WEAPON', gridX[1], 492, { width: 150, height: 28, color: '#bba9a1', accent: '#6e3843' });
drawPixelTag(ctx, '[T] AUTO ' + (G.autoPerk ? 'ON' : 'OFF'), gridX[2], 492, { width: 150, height: 28, color: G.autoPerk ? '#79d9ca' : '#bba9a1', accent: G.autoPerk ? '#378b80' : '#6e3843' });
const menuHover = inRect(Input.mx, Input.my, PAUSE_MENU_BUTTON);
const exitHover = inRect(Input.mx, Input.my, PAUSE_EXIT_BUTTON);
const menuConfirm = G.confirmAction === 'menu';
const desktopExit = canQuitDesktop();
const exitConfirm = desktopExit && G.confirmAction === 'desktop';
drawPixelTag(ctx, '[M] MUTE AUDIO', gridX[0], 526, { width: 150, height: 28, color: '#bba9a1', accent: '#6e3843' });
drawPixelTag(ctx, menuConfirm ? '[Q] CONFIRM MENU' : '[Q] MAIN MENU', PAUSE_MENU_BUTTON.x, PAUSE_MENU_BUTTON.y, {
width: PAUSE_MENU_BUTTON.w, height: PAUSE_MENU_BUTTON.h,
color: menuConfirm ? '#ffd36a' : (menuHover ? '#f5e9d6' : '#bba9a1'),
accent: menuConfirm ? '#d69a22' : (menuHover ? '#b5243a' : '#6e3843'),
});
drawPixelTag(ctx, !desktopExit ? '[X] DESKTOP ONLY' : (exitConfirm ? '[X] CONFIRM EXIT' : '[X] EXIT'), PAUSE_EXIT_BUTTON.x, PAUSE_EXIT_BUTTON.y, {
width: PAUSE_EXIT_BUTTON.w, height: PAUSE_EXIT_BUTTON.h,
color: !desktopExit ? '#65595b' : (exitConfirm ? '#ffd36a' : (exitHover ? '#f5e9d6' : '#bba9a1')),
accent: !desktopExit ? '#3a2d31' : (exitConfirm ? '#d69a22' : (exitHover ? '#b5243a' : '#6e3843')),
});
const hb = HELP_BUTTON;
const helpHover = inRect(Input.mx, Input.my, hb);
drawPixelTag(ctx, '[?] HELP', hb.x, hb.y, {
width: hb.w, height: hb.h,
color: helpHover ? '#f1cb53' : '#b89c46', accent: helpHover ? '#c9a227' : '#7c6424',
});
}
function drawGameOver(ctx) {
const reveal = clamp((DEATH_LOCK - G.deathLockT) / 0.9, 0, 1);
ctx.save();
ctx.globalAlpha = reveal;
ctx.fillStyle = 'rgba(4,1,3,0.86)'; ctx.fillRect(0, 0, W, H);
drawPixelPanel(ctx, W / 2 - 230, 142, 460, 350, {
accent: '#e02945', border: '#772638', fill: 'rgba(12,4,7,0.97)',
blood: true, seed: G.kills + 3, bloodAlpha: 0.48,
});
ctx.textAlign = 'center';
ctx.fillStyle = '#8f6870'; ctx.font = 'bold 9px monospace';
ctx.fillText('RUN TERMINATED // CARCASS RECOVERED', W / 2, 174);
ctx.fillStyle = '#ee2947'; ctx.font = 'bold 48px monospace';
ctx.fillText('BUTCHERED', W / 2, 231);
drawBloodTrim(ctx, W / 2 - 170, 242, 340, G.kills + 3, 0.7);
drawPixelPanel(ctx, W / 2 - 188, 278, 116, 72, { cut: 5, shadow: false, accent: '#9c7428', fill: '#130b0d' });
drawPixelPanel(ctx, W / 2 - 58, 278, 116, 72, { cut: 5, shadow: false, accent: '#9c7428', fill: '#130b0d' });
drawPixelPanel(ctx, W / 2 + 72, 278, 116, 72, { cut: 5, shadow: false, accent: '#9c7428', fill: '#130b0d' });
const stats = [['FLOOR', G.floor], ['KILLS', G.kills], ['SCORE', G.score]];
for (let i = 0; i < stats.length; i++) {
const x = W / 2 - 130 + i * 130;
ctx.fillStyle = '#7e6a66'; ctx.font = 'bold 9px monospace'; ctx.fillText(stats[i][0], x, 300);
ctx.fillStyle = i === 2 ? '#e2b742' : '#eee0d3'; ctx.font = 'bold 18px monospace';
ctx.fillText(String(stats[i][1]).padStart(i === 2 ? 6 : 2, '0'), x, 329);
}
if (G.debugUsed) {
drawPixelTag(ctx, '[ DEBUG RUN — SCORE NOT RECORDED ]', W / 2 - 128, 367, { width: 256, height: 26, color: '#e02945', accent: '#77202c' });
} else if (G.score >= (G.best || 0) && G.score > 0) {
drawPixelTag(ctx, 'NEW BEST CUT', W / 2 - 68, 367, { width: 136, height: 26, color: '#f1cb53', accent: '#b38b2e' });
}
drawPixelPanel(ctx, W / 2 - 140, 414, 280, 50, {
cut: 5, shadow: false, accent: G.deathLockT > 0 ? '#77552a' : '#cf2942',
border: G.deathLockT > 0 ? '#604727' : '#8c293c', fill: '#1a080e',
});
ctx.fillStyle = G.deathLockT > 0 ? '#d4ad3d' : '#f1e4d5'; ctx.font = 'bold 12px monospace';
ctx.fillText(G.deathLockT > 0 ? '[ INPUT LOCKED · ' + G.deathLockT.toFixed(1) + ' ]' : '[ R / ENTER ] CUT AGAIN', W / 2, 444);
ctx.restore();
}
window.addEventListener('load', init);