Files
MeatSlicer/js/main.js
T
MeatSlicer Developer a02cff52cf Add pressure dial, wave spawn telegraphs, and readability pass
- Bigger gore-red Minis (r10->14, sprite 32->48) with regenerated art
- Wider, walkable doorways: clear passage for open, teeth only when locked
- 500ms spawn telegraph so wave spawns no longer blindside the player
- Item stickiness ramps 10%/owned item (cap 50%) instead of flat 50%
- Title-screen Pressure Dial (-5..+5) tunes rise/relief; score x live pressure
- Vitals HUD: two-row hearts, half-container outlines, half-heart shield pips
- Readable 8-page field manual (bigger panel + fonts, implants split)
- HUD opacity slider in pause menu (boss bar + toasts stay opaque)
- Scrap Feed now has diminishing returns like Thick Hide
- Docs, cache-bust v37, and smoke coverage for all of the above
2026-07-30 00:01:06 +10:00

470 lines
20 KiB
JavaScript
Raw 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 };
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;
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 startRun() {
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.pressure = 1; G.streak = 0; G.roomDamaged = false; G.roomEnterT = 0; G.recentHits = [];
G.shake = 0; G.flash = 0;
initPlayer();
genFloor(1);
enterRoom(0, 0);
G.player.shieldHp = G.player.stats.shieldPerk || 0;
G.mode = 'play';
Music.requestFloorMusic();
addToast('FLOOR 1', 'clear the rooms. find the stairs. feed.');
}
function gameOver() {
G.mode = 'gameover';
Sfx.playerDeath();
if (G.score > (G.best || 0)) {
G.best = G.score;
try { localStorage.setItem('meatslicer_best', String(G.best)); } catch (e) {}
}
Music.playMenu();
}
function loop(now) {
const dt = Math.min((now - lastT) / 1000 || 0.016, 0.05);
lastT = now;
update(dt);
draw();
clearInputEdges();
requestAnimationFrame(loop);
}
function update(dt) {
Music.update(dt);
// 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;
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) * 10) - 5);
}
if (!Input.mdown) G.menuDialDrag = false;
if (keyPressed('enter', ' ') || (Input.mpressed && !inRect(Input.mx, Input.my, sl))) startRun();
break;
}
case 'play':
G.time += dt;
if (keyPressed('p', 'escape')) { G.mode = 'pause'; Sfx.stopAllLoops(); Sfx.menu(); break; }
if (keyPressed('n')) Music.cycle(1);
if (keyPressed('r')) swapWeapon();
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();
updateParticles(dt);
break;
case 'pause':
if (G.pauseHelp) { updatePauseHelp(); break; }
if (keyPressed('p', 'escape')) { G.mode = 'play'; Sfx.menu(); break; }
if (keyPressed('h') || (Input.mpressed && inRect(Input.mx, Input.my, HELP_BUTTON))) {
G.pauseHelp = true; G.helpPage = 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 (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);
}
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',
});
}
}
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 <= -4 ? 'MERCIFUL' : d <= -2 ? 'EASED' : d <= 0 ? 'STANDARD' : d <= 2 ? 'TENSE' : d <= 4 ? 'SAVAGE' : 'RELENTLESS';
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 10 steps / 11 notches
const bx = sl.x + 34, bw = sl.w - 68, by = sl.y + 20, bh = 10;
drawPixelBar(ctx, bx, by, bw, bh, (d + 5) / 10, {
fill: accent, glint: '#f4d86d', border: '#5d3c43', segments: 10,
});
// bright notch marker
const nx = bx + Math.round((d + 5) / 10 * 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('-5', bx - 16, by + 9); ctx.fillText('+5', bx + bw + 16, by + 9);
// derived readout
ctx.fillStyle = '#8f7770'; ctx.font = '8px monospace';
ctx.fillText('RISE +' + gain.toFixed(1) + '/5 · RELIEF ' + drop.toFixed(1) + '/5 · 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';
}
drawPixelTag(ctx, '[P] RESUME', W / 2 - 250, 502, { width: 112, height: 28, color: '#f0e5d7', accent: '#b5243a' });
drawPixelTag(ctx, '[R] SWAP', W / 2 - 125, 502, { width: 112, height: 28, color: '#bba9a1', accent: '#6e3843' });
drawPixelTag(ctx, '[T] AUTO ' + (G.autoPerk ? 'ON' : 'OFF'), W / 2, 502, { width: 112, height: 28, color: G.autoPerk ? '#79d9ca' : '#bba9a1', accent: G.autoPerk ? '#378b80' : '#6e3843' });
drawPixelTag(ctx, '[M] MUTE', W / 2 + 125, 502, { width: 112, height: 28, color: '#bba9a1', accent: '#6e3843' });
ctx.fillStyle = '#69585a'; ctx.font = '9px monospace';
ctx.textAlign = 'center';
ctx.fillText('ARROWS / CLICK CHANGE TRACK · DRAG SLIDERS · LIVE BOSSES OVERRIDE TRACK', W / 2, 543);
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) {
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.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: '#cf2942', border: '#8c293c', fill: '#1a080e',
});
ctx.fillStyle = '#f1e4d5'; ctx.font = 'bold 12px monospace';
ctx.fillText('[ R / ENTER ] CUT AGAIN', W / 2, 444);
}
window.addEventListener('load', init);