From d96d492a43dca909ba96068f48dded5a26c722be Mon Sep 17 00:00:00 2001 From: MeatSlicer Developer Date: Thu, 30 Jul 2026 22:57:36 +1000 Subject: [PATCH] Polish pause actions and duplicate weapons --- docs/weapons.md | 1 + js/main.js | 13 +++++++------ js/pickups.js | 19 +++++++++++++++++++ test/smoke.js | 10 ++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/docs/weapons.md b/docs/weapons.md index 3a2421e..1d95a95 100644 --- a/docs/weapons.md +++ b/docs/weapons.md @@ -11,6 +11,7 @@ Weapons have **no upgrade levels** — their stats are fixed. All scaling comes - You always carry the **Bone Popper** (infinite ammo sidearm) plus **one special weapon** in a holster slot. - Press **R** to swap between them. Holstered weapons keep their remaining ammo. - Picking up a new special weapon drops your current one (with its remaining ammo) so you can swap back. +- Picking up a weapon that matches either your active or holstered special acts like an ammo pickup for that matching weapon instead of replacing it. Refill uses the weapon's authored `refill`, respects the 150% cap, and converts overflow to score. - When a special weapon runs dry, you automatically fall back to the Bone Popper. - Most special weapons consume ammo per shot: `1 / ammoEff` units per shot (so Brass Magazine's additive `ammoEff` stretches every magazine). - **Stream weapons** (Bile Blunderbuss, The Cauterizer, Red Right Hand) instead drain ammo per second while held: `drain / ammoEff` units per second. Their magazines are measured in seconds of fire. diff --git a/js/main.js b/js/main.js index 7d1aad1..61c2b62 100644 --- a/js/main.js +++ b/js/main.js @@ -4,8 +4,8 @@ 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 - 122, y: 522, w: 116, h: 28 }; -const PAUSE_EXIT_BUTTON = { x: W / 2 + 6, y: 522, w: 116, 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() { @@ -480,14 +480,15 @@ function drawPause(ctx) { 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' }); + 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 exitConfirm = 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'), diff --git a/js/pickups.js b/js/pickups.js index 9481ada..d409cd7 100644 --- a/js/pickups.js +++ b/js/pickups.js @@ -93,6 +93,25 @@ function collectPickup(k) { } case 'weapon': { const w = WEAPONS[k.wid]; + const match = p.weapon && p.weapon.id === w.id ? p.weapon : + (p.holstered && p.holstered.id === w.id ? p.holstered : null); + if (match) { + // A duplicate becomes an ammo box for the matching carried instance; + // it never replaces the weapon or creates a drop loop. + if (match.ammo === Infinity || w.ammo === Infinity) { + addScore(15); + spawnText(p.x, p.y, 'AMMO FULL', '#d0b060'); + } else { + const cap = w.ammo * 1.5; + const wanted = Math.max(1, Math.ceil((w.refill || 1) * p.stats.ammoPickupMul)); + const add = Math.max(0, Math.min(wanted, cap - match.ammo)); + match.ammo += add; + if (wanted > add) addScore(Math.round((wanted - add) * 2)); + spawnText(p.x, p.y, add > 0 ? '+' + add + ' ' + w.name.toUpperCase() + ' AMMO' : 'AMMO FULL', '#d0b060'); + } + Sfx.ammo(); + break; + } // drop the old special with its remaining ammo so it's never wasted const old = p.weapon.id !== 'bonepopper' ? p.weapon : p.holstered; if (old) { diff --git a/test/smoke.js b/test/smoke.js index fc99b3d..217982e 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -1259,12 +1259,22 @@ console.log('== weapon swap (R) =='); ctx.spawnPickup('weapon', p.x, p.y, { wid: 'repeater' }); step(2, 16); check('picked up special', p.weapon.id === 'repeater'); + p.weapon.ammo = 5; + ctx.spawnPickup('weapon', p.x, p.y, { wid: 'repeater' }); + step(2, 16); + check('duplicate active weapon refills ammo instead of replacing it', p.weapon.id === 'repeater' && p.weapon.ammo > 5 && !p.holstered); + check('duplicate active weapon does not create a dropped-weapon loop', !ctx.G.pickups.some(k => k.type === 'weapon')); p.weapon.ammo = 77; const playSwapToast = ctx.G.toasts[ctx.G.toasts.length - 1]; tap('r'); check('R swaps to pistol', p.weapon.id === 'bonepopper' && p.holstered && p.holstered.id === 'repeater'); check('play weapon swap emits a toast', ctx.G.toasts[ctx.G.toasts.length - 1] !== playSwapToast); check('holster keeps remaining ammo', p.holstered.ammo === 77); + p.holstered.ammo = 5; + ctx.spawnPickup('weapon', p.x, p.y, { wid: 'repeater' }); + step(2, 16); + check('duplicate holstered weapon refills ammo without forcing a swap', p.weapon.id === 'bonepopper' && p.holstered.id === 'repeater' && p.holstered.ammo > 5); + p.holstered.ammo = 77; tap('r'); check('R swaps back', p.weapon.id === 'repeater' && p.weapon.ammo === 77 && !p.holstered); // picking up another special drops the old one with its ammo