feat: clickable pause buttons, 2x dash with 30% slow, cleaver melee sweep

This commit is contained in:
Quintine committed 2026-08-13 13:50:50 +10:00
1 parent 59869945d0
commit 85238be844
10 files changed
+67 -30

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@ Your best score is saved in `localStorage` as `meatslicer_best` and shown on the
| **T** | Toggle Auto-Draft (auto-pick random perk on level-up) |
| **M** | Mute / unmute |
| **N** | Next music track |
| **Shift** | Dash in the movement direction; 0.6s of 50% slow after (net 95% of walking distance) |
| **Shift** | Dash in the movement direction; 2× length, 0.6s of 30% slow |
### Perk draft screen (level-up)
+7 -7
View File
@@ -106,18 +106,18 @@ Shotgun. Up to 66 damage per trigger pull at point blank before multipliers, but
### Cleaver Cadence
> *It always comes back*
> *A wide, dramatic melee sweep*
| Stat | Value |
|---|---|
| Damage | 15 |
| Fire interval | 0.38 s |
| Projectile speed | 400 (returns at up to 520) |
| Range | 1.10 s |
| Damage | 34 |
| Fire interval | 0.55 s |
| Reach | 90 px |
| Arc | 2.4 rad (~137°) |
| Ammo | 26 (refill 10 per pickup) |
| Recoil | 0.42 |
| Recoil | 0.95 (heavy) |
Boomerang: flies out while decelerating for 0.45 s, then accelerates back to you (caught within 20 px). **Always pierces** — it can hit enemies on both the outbound and return trip.
Melee sweep: one wide swing across the aim direction hits every enemy within 90 px inside a ~137° arc for 34 damage with heavy knockback.
### Sawblade Launcher
+22 -12
View File
@@ -7,6 +7,10 @@ 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 HD_TOGGLE_BUTTON = { x: 28, y: H - 48, w: 220, h: 28 };
const PAUSE_RESUME_BUTTON = { x: W / 2 - 237, y: 492, w: 150, h: 28 };
const PAUSE_SWAP_BUTTON = { x: W / 2 - 75, y: 492, w: 150, h: 28 };
const PAUSE_AUTO_BUTTON = { x: W / 2 + 87, y: 492, w: 150, h: 28 };
const PAUSE_MUTE_BUTTON = { x: W / 2 - 237, y: 526, w: 150, h: 28 };
const CONFIRM_WINDOW = 3;
function pauseBars() {
@@ -59,6 +63,11 @@ function setPressureDial(n) {
try { localStorage.setItem('meatslicer_pressure_dial', String(G.pressureDial)); } catch (e) {}
Sfx.menu();
}
function toggleMute() {
G.muted = !G.muted;
Music.setMuted(G.muted);
Sfx.setMuted(G.muted);
}
function setHudAlpha(v) {
G.hudAlpha = clamp(Math.round(v * 100) / 100, 0.25, 1);
@@ -194,11 +203,7 @@ function update(dt) {
}
// global keys
if (keyPressed('m')) {
G.muted = !G.muted;
Music.setMuted(G.muted);
Sfx.setMuted(G.muted);
}
if (keyPressed('m')) toggleMute();
switch (G.mode) {
case 'menu': {
@@ -265,12 +270,13 @@ function update(dt) {
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('p', 'escape') || (Input.mpressed && inRect(Input.mx, Input.my, PAUSE_RESUME_BUTTON))) { 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('r') || (Input.mpressed && inRect(Input.mx, Input.my, PAUSE_SWAP_BUTTON))) swapWeapon(true);
if (keyPressed('t') || (Input.mpressed && inRect(Input.mx, Input.my, PAUSE_AUTO_BUTTON))) setAutoPerk(!G.autoPerk);
if (Input.mpressed && inRect(Input.mx, Input.my, PAUSE_MUTE_BUTTON)) { toggleMute(); break; }
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(); }
@@ -541,15 +547,19 @@ function drawPause(ctx) {
}
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 resumeHover = inRect(Input.mx, Input.my, PAUSE_RESUME_BUTTON);
const swapHover = inRect(Input.mx, Input.my, PAUSE_SWAP_BUTTON);
const autoHover = inRect(Input.mx, Input.my, PAUSE_AUTO_BUTTON);
const muteHover = inRect(Input.mx, Input.my, PAUSE_MUTE_BUTTON);
drawPixelTag(ctx, '[P] RESUME', gridX[0], 492, { width: 150, height: 28, color: resumeHover ? '#f5e9d6' : '#bba9a1', accent: resumeHover ? '#b5243a' : '#6e3843' });
drawPixelTag(ctx, '[R] SWAP WEAPON', gridX[1], 492, { width: 150, height: 28, color: swapHover ? '#f5e9d6' : '#bba9a1', accent: swapHover ? '#b5243a' : '#6e3843' });
drawPixelTag(ctx, '[T] AUTO ' + (G.autoPerk ? 'ON' : 'OFF'), gridX[2], 492, { width: 150, height: 28, color: G.autoPerk ? '#79d9ca' : (autoHover ? '#f5e9d6' : '#bba9a1'), accent: G.autoPerk ? '#378b80' : (autoHover ? '#b5243a' : '#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, '[M] MUTE AUDIO', gridX[0], 526, { width: 150, height: 28, color: muteHover ? '#f5e9d6' : '#bba9a1', accent: muteHover ? '#b5243a' : '#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'),
+10
View File
@@ -104,6 +104,10 @@ function spawnShockwave(x, y, radius, color, alpha, important) {
}, important);
}
function spawnSweepFx(x, y, ang, reach, arc) {
addParticle({ type: 'sweep', x, y, ang, arc, r: 20, maxR: reach, life: 0.18, t: 0 }, true);
}
function spawnMuzzleFx(x, y, ang, behavior) {
if (behavior === 'slam') return;
const back = ang + Math.PI;
@@ -254,6 +258,12 @@ function drawParticles(ctx) {
ctx.stroke();
ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1.5;
ctx.stroke();
} else if (p.type === 'sweep') {
const progress = 1 - k;
const radius = lerp(p.r, p.maxR, progress);
ctx.globalAlpha = k;
ctx.strokeStyle = '#f0c8b2'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.arc(p.x, p.y, radius, p.ang - p.arc / 2, p.ang + p.arc / 2); ctx.stroke();
} else if (p.type === 'text') {
ctx.globalAlpha = Math.min(1, k * 2);
ctx.fillStyle = p.color;
+3 -3
View File
@@ -1,6 +1,6 @@
// ---- the butcher (player) ----
const DASH_SLOW_T = 0.6; // seconds of 50% move speed after a dash; doubles as the dash cooldown
const DASH_SLOW_T = 0.6; // seconds of 30% move speed after a dash; doubles as the dash cooldown
function defaultPlayerStats() {
return {
@@ -86,7 +86,7 @@ function updatePlayer(dt) {
const len = Math.hypot(mx, my);
const firing = held ? (w.id === 'bonepopper' ? 0.95 : 0.85) : 1;
if (keyPressed('shift') && p.dashSlowT <= 0) {
const dashD = 0.45 * (178 * st.speedMul * firing) * DASH_SLOW_T;
const dashD = 0.9 * (178 * st.speedMul * firing) * DASH_SLOW_T;
p.x += (mx / len) * dashD;
p.y += (my / len) * dashD;
p.dashSlowT = DASH_SLOW_T;
@@ -94,7 +94,7 @@ function updatePlayer(dt) {
spawnShockwave(p.x, p.y, 26, '#d8c9a8', 0.35);
Sfx.duck();
}
const spd = 178 * st.speedMul * firing * (p.dashSlowT > 0 ? 0.5 : 1);
const spd = 178 * st.speedMul * firing * (p.dashSlowT > 0 ? 0.3 : 1);
p.x += (mx / len) * spd * dt;
p.y += (my / len) * spd * dt;
p.step += dt * 9 * Math.min(st.speedMul, 1.8);
+1
View File
@@ -16,6 +16,7 @@ function angleLerp(a, b, t) {
if (d < -Math.PI) d += TAU;
return a + d * t;
}
function angleDelta(a, b) { let d = (b - a) % TAU; if (d > Math.PI) d -= TAU; if (d < -Math.PI) d += TAU; return d; }
// distance from point to segment (for beam weapons)
function distToSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
+17 -2
View File
@@ -1,6 +1,6 @@
// ---- The Butcher's Arsenal: 16 horrorcore weapons ----
// behavior: bullet | spread | boomerang | bounce | cone | flame | homing |
// pierce_drag | lob | lob_trap | lob_swarm | slam | saw | beam
// pierce_drag | lob | lob_trap | lob_swarm | slam | saw | beam | sweep
// Ammo economy: `ammo` is the magazine (trigger-time / burst feel), `refill`
// is what one ammo pickup hands back (~10 kills' worth). Sustained-stream
@@ -11,7 +11,7 @@ const WEAPONS = {
bonepopper: { id: 'bonepopper', name: 'Bone Popper', held: 38, torsoW: 106, torsoFwd: 18, muzzle: 62, tier: -1, ammo: Infinity, interval: 0.60, dmg: 8, spd: 540, range: 0.45, behavior: 'bullet', sprite: 'bullet_bone', sfx: 'default', punch: 0.42, desc: 'Trusty bone-shard sidearm' },
repeater: { id: 'repeater', name: 'Ribcage Repeater', held: 52, torsoW: 120, torsoFwd: 20, muzzle: 70, tier: 0, ammo: 84, refill: 42, interval: 0.09, dmg: 6, spd: 560, spread: 0.10, range: 0.42, behavior: 'bullet', sprite: 'bullet_bone', sfx: 'rapid', punch: 0.18, desc: 'Rattles off rib slivers' },
marrow: { id: 'marrow', name: 'Marrow Scatter', held: 54, torsoW: 120, torsoFwd: 20, muzzle: 69, tier: 0, ammo: 20, refill: 6, interval: 0.58, dmg: 11, spd: 500, pellets: 6, spread: 0.55, range: 0.38, behavior: 'spread', sprite: 'bullet_bone', sfx: 'spread', punch: 0.92, desc: 'A devastating close blast of jagged bone' },
cleaver: { id: 'cleaver', name: 'Cleaver Cadence', held: 60, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 1, ammo: 26, refill: 10, interval: 0.38, dmg: 15, spd: 400, range: 1.10, behavior: 'boomerang', sprite: 'bullet_cleaver', sfx: 'boomerang', punch: 0.42, desc: 'It always comes back' },
cleaver: { id: 'cleaver', name: 'Cleaver Cadence', held: 60, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 1, ammo: 26, refill: 10, interval: 0.55, dmg: 34, behavior: 'sweep', sprite: 'bullet_cleaver', sfx: 'heavy', punch: 0.95, sweepRange: 90, sweepArc: 2.4, desc: 'A wide, dramatic melee sweep' },
saw: { id: 'saw', name: 'Sawblade Launcher',held: 52, torsoW: 118, torsoFwd: 20, muzzle: 70, tier: 1, ammo: 24, refill: 9, interval: 0.34, dmg: 13, spd: 360, bounces: 5, range: 0.60, behavior: 'bounce', sprite: 'bullet_saw', sfx: 'saw', punch: 0.45, desc: 'Bouncing, hungry steel' },
bile: { id: 'bile', name: 'Bile Blunderbuss', held: 54, torsoW: 118, torsoFwd: 20, muzzle: 68, tier: 2, ammo: 80, refill: 21, drain: 8, interval: 0.12, dmg: 7, spd: 340, spread: 0.32, range: 0.38, behavior: 'cone', sprite: 'bullet_gore', sfx: 'flame', punch: 0.34, desc: 'A brutal corrosive torrent; pools linger' },
hemophage: { id: 'hemophage', name: 'Hemophage', held: 50, torsoW: 116, torsoFwd: 19, muzzle: 67, tier: 1, ammo: 70, refill: 42, interval: 0.19, dmg: 8, spd: 480, lifesteal: 0.5, range: 0.45, behavior: 'bullet', sprite: 'bullet_syringe', sfx: 'default', punch: 0.35, desc: 'Drinks what it touches' },
@@ -131,6 +131,21 @@ function fireWeapon(p, w) {
Sfx.explode({ x: p.x + Math.cos(ang) * 46, y: p.y + Math.sin(ang) * 46 });
break;
}
case 'sweep': {
const reach = (w.sweepRange || 90) * st.rangeMul;
const half = (w.sweepArc || 2.4) / 2;
for (const e of G.enemies) {
if (e.phased) continue;
const d = dist(p.x, p.y, e.x, e.y);
if (d > reach + e.r) continue;
const a = angleTo(p.x, p.y, e.x, e.y);
if (Math.abs(angleDelta(a, ang)) > half) continue;
damageEnemy(e, w.dmg * dmgMul, a, true, { source: 'player' });
}
spawnSweepFx(p.x, p.y, ang, reach, w.sweepArc || 2.4);
addShake(4);
break;
}
default: { // bullet, bounce and homing projectiles
for (const a of volley) mk(a + rand(-(w.spread || 0), w.spread || 0));
}
+4 -4
View File
@@ -1069,7 +1069,7 @@ console.log('== per-weapon muzzle spawn distances ==');
p.aim = 0;
const badMuzzles = [];
for (const w of Object.values(ctx.WEAPONS)) {
if (['slam', 'saw', 'beam', 'flame'].includes(w.behavior)) continue;
if (['slam', 'saw', 'beam', 'flame', 'sweep'].includes(w.behavior)) continue;
ctx.G.bullets.length = 0;
ctx.fireWeapon(p, w);
const b = ctx.G.bullets[0];
@@ -1200,7 +1200,7 @@ console.log('== dash: burst, slow, cooldown ==');
release('shift');
const dashDx = p.x - xBeforeDash;
check('dashSlowT > 0 after dash', p.dashSlowT > 0);
check('dash burst within 5% of 0.45*178*0.6', Math.abs(dashDx - 0.45*178*0.6) <= 0.05 * 0.45*178*0.6);
check('dash burst within 5% of 0.9*178*0.6', Math.abs(dashDx - 0.9*178*0.6) <= 0.05 * 0.9*178*0.6);
let slowDist = 0;
let slowFrames = 0;
let sampledSlowDx = 0;
@@ -1225,8 +1225,8 @@ console.log('== dash: burst, slow, cooldown ==');
slowFrames++;
}
}
check('slow per-frame dx is roughly half baseline', sampledSlowDx > 0 && sampledSlowDx >= 0.4 * baselineDx && sampledSlowDx <= 0.6 * baselineDx);
check('total dash (burst + slow) within 8% of 0.95*178*0.6', Math.abs((dashDx + slowDist) - 0.95*178*0.6) <= 0.08 * 0.95*178*0.6);
check('slow per-frame dx is roughly 30% of baseline', sampledSlowDx > 0 && sampledSlowDx >= 0.25 * baselineDx && sampledSlowDx <= 0.35 * baselineDx);
check('total dash (burst + slow) within 8% of 1.2*178*0.6', Math.abs((dashDx + slowDist) - 1.2*178*0.6) <= 0.08 * 1.2*178*0.6);
ctx.Input.keys.d = false;
step(3, 16);
const xBeforeIdle = p.x;
+1 -1
View File
@@ -83,7 +83,7 @@ function boot(seed) {
const AIM_JITTER = {
bullet: 0.07, spread: 0.05, bounce: 0.07, boomerang: 0.05, pierce_drag: 0.04,
cone: 0.03, flame: 0.03,
homing: 0, lob: 0, lob_trap: 0, lob_swarm: 0, slam: 0, saw: 0, beam: 0,
homing: 0, lob: 0, lob_trap: 0, lob_swarm: 0, slam: 0, saw: 0, beam: 0, sweep: 0,
};
const FLOOR = 3; // mid-game enemy mix
+1
View File
@@ -28,6 +28,7 @@ function effectiveDps(w) {
case 'lob_trap': return base * 1.3; // direct + trap zone control
case 'lob_swarm': return (w.dmg * 6) / w.interval * 0.85; // 6 homing maggots
case 'slam': return base * 1.3; // AoE multi-hit
case 'sweep': return base * 1.5; // arc can hit multiple enemies
case 'saw': return base * 1.1; // continuous contact
case 'beam': return (w.dmg * 1.0) / ((w.chargeTime || 0.8) + w.interval) * 1.2; // full-charge cycle, line pierce
default: return base;