fix: persistent mute toggle, 25%/floor shield regen, halved pressure relief, shielded hits no relief, drop Scavenger perk (0.9.2)

This commit is contained in:
Quintine committed 2026-08-13 17:24:27 +10:00
1 parent 683e7a47ec
commit c41ce5fdb0
16 files changed
+105 -71

No files matched your search

+4 -4
View File
@@ -178,7 +178,7 @@ function drawHelpPerkList(ctx, px, py) {
let ty = py + HELP_BODY;
ctx.textAlign = 'left';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(9, true);
ctx.fillText('23 PERKS · DRAFT 1 OF 3 PER LEVEL · XP FOR NEXT LEVEL: 8 + LEVEL × 4', x, ty);
ctx.fillText(PERKS.length + ' PERKS · DRAFT 1 OF 3 PER LEVEL · XP FOR NEXT LEVEL: 8 + LEVEL × 4', x, ty);
ty += 11;
ctx.fillStyle = '#7d6a68'; ctx.font = hfont(8, false);
ctx.fillText('REROLL RIB GRANTS TOKENS (R TO REDRAW) · AUTO-DRAFT (T) PICKS RANDOMLY', x, ty + 11);
@@ -304,11 +304,11 @@ function drawHelpPressure(ctx, px, py) {
'ALL SCORE IS MULTIPLIED BY LIVE PRESSURE',
'DIAL ' + (d > 0 ? '+' : '') + d + ' — RISE ' + gain.toFixed(1) + '/10 PER CLEAN ROOM · RELIEF ' + drop.toFixed(1) + '/10 ON HIT',
'+' + pressureGain().toFixed(3) + ' FOR EVERY ROOM CLEARED WITHOUT TAKING ANY DAMAGE',
'RELIEF ON HIT = ' + (0.03 * scale).toFixed(3) + ' × SEVERITY × DESPERATION × CHURN',
'RELIEF ON HIT = ' + (PRESSURE_DROP_BASE * PRESSURE_UNIT * PRESSURE_RELIEF_MUL * scale).toFixed(3) + ' × SEVERITY × DESPERATION × CHURN',
' SEVERITY = HIT / (MAX HP × 0.25), CLAMPED 0.4 – 2',
' DESPERATION = 1 + 2 × (1 − HP FRACTION) — MORE RELIEF WHEN HURT',
' CHURN = 1 + 0.35 × HITS IN LAST 12S (CAP +4) — MORE WHEN SWARMED',
'DECAY: ' + (0.02 * scale).toFixed(3) + '/S BELOW 35% HP · ' + (0.008 * scale).toFixed(3) + '/S AFTER 90S IN ONE ROOM',
'DECAY: ' + (0.02 * scale * PRESSURE_RELIEF_MUL).toFixed(3) + '/S BELOW 35% HP · ' + (0.008 * scale * PRESSURE_RELIEF_MUL).toFixed(3) + '/S AFTER 90S IN ONE ROOM',
'LIVING ENEMIES KEEP THEIR STATS — ONLY NEW SPAWNS CHANGE',
'STREAK = CONSECUTIVE FLAWLESS CLEARS (COSMETIC)',
] },
@@ -316,7 +316,7 @@ function drawHelpPressure(ctx, px, py) {
'ARMOR = BLOCK CHANCE: RATING / (1 + RATING), HARD CAP 75%',
' THICK HIDE +1/24 RATING · TANNED HIDE +0.08/0.92 (DIMINISHING)',
'BLOCKED HIT: 0 DAMAGE, 0.2S I-FRAMES, STREAK PRESERVED',
'SHIELD HEARTS ABSORB FIRST, REFILL EACH FLOOR · 0.9S I-FRAMES ON HIT',
'SHIELD HEARTS ABSORB FIRST, REGEN 25% EACH FLOOR · SHIELDED HITS NO RELIEF · 0.9S I-FRAMES ON HIT',
'CRIT: 5% BASE CHANCE, ×2 DAMAGE (CRITMUL STACKS)',
] },
], HELP_PANEL.w - 80);
+1 -1
View File
@@ -53,7 +53,7 @@ const ITEMS = {
orbitalknives: { name: 'Orbital Knives', desc: '+1 circling knife', rarity: 'rare', cap: 3, apply: s => { s.orbitals += 1; } },
volatilebile: { name: 'Volatile Bile', desc: 'Kills explode (bigger per tier)', rarity: 'rare', cap: 3, apply: s => { s.explodeOnKill = (s.explodeOnKill || 0) + 1; } },
deadmanswitch: { name: "Dead Man's Switch", desc: 'Kills trigger a fire-rate frenzy', rarity: 'rare', cap: 3, apply: s => { s.frenzy += 0.12; } },
boneplate: { name: 'Bone Plate', desc: '+½ shield heart each floor', rarity: 'rare', cap: 3, apply: (s, p) => { s.shieldPerk = (s.shieldPerk || 0) + 1; p.shieldHp += 1; } },
boneplate: { name: 'Bone Plate', desc: '+½ max shield heart (regens 25% per floor)', rarity: 'rare', cap: 3, apply: (s, p) => { s.shieldPerk = (s.shieldPerk || 0) + 1; p.shieldHp += 1; } },
wormgut: { name: 'Worm Gut', desc: 'Heal ½ heart after combat rooms', rarity: 'rare', cap: 3, apply: s => { s.roomHeal += 1; } },
secondstomach: { name: 'Second Stomach', desc: '+½ heart and excess healing shields', rarity: 'rare', cap: 3, apply: (s, p) => { s.maxHp += 1; s.overShield += 1; p.hp = Math.min(s.maxHp, p.hp + 1); } },
twinhearts: { name: 'Twin Hearts', desc: '+1 max heart, heal to full', rarity: 'rare', cap: 3, apply: (s, p) => { s.maxHp += 2; p.hp = s.maxHp; } },
+5 -1
View File
@@ -50,6 +50,7 @@ function init() {
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';
G.muted = localStorage.getItem('meatslicer_muted') === '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'));
@@ -67,6 +68,9 @@ function toggleMute() {
G.muted = !G.muted;
Music.setMuted(G.muted);
Sfx.setMuted(G.muted);
try { localStorage.setItem('meatslicer_muted', G.muted ? '1' : '0'); } catch (e) {}
addToast(G.muted ? 'AUDIO MUTED' : 'AUDIO UNMUTED', G.muted ? 'all sound is silenced' : 'sound restored');
if (!G.muted) Sfx.menu();
}
function setHudAlpha(v) {
@@ -559,7 +563,7 @@ function drawPause(ctx) {
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: muteHover ? '#f5e9d6' : '#bba9a1', accent: muteHover ? '#b5243a' : '#6e3843' });
drawPixelTag(ctx, '[M] MUTE ' + (G.muted ? 'ON' : 'OFF'), gridX[0], 526, { width: 150, height: 28, color: G.muted ? '#79d9ca' : (muteHover ? '#f5e9d6' : '#bba9a1'), accent: G.muted ? '#378b80' : (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'),
+1 -2
View File
@@ -7,8 +7,7 @@ const PERKS = [
{ id: 'longbone', name: 'Long Bone', desc: '+5% range', apply: s => { s.rangeMul *= 1.05; } },
{ id: 'bigheart', name: 'Big Heart', desc: '+½ max heart, heal ½ heart', apply: (s, p) => { s.maxHp += 1; p.hp = Math.min(s.maxHp, p.hp + 1); } },
{ id: 'magnetb', name: 'Magnet Bile', desc: '+10% pickup radius', apply: s => { s.magnet *= 1.10; } },
{ id: 'shieldheart', name: 'Shield Heart', desc: '+½ shield heart at the start of each floor', apply: (s, p) => { s.shieldPerk = (s.shieldPerk || 0) + 1; p.shieldHp = (p.shieldHp || 0) + 1; } },
{ id: 'scavenge', name: 'Scavenger', desc: '+4% drop luck', apply: s => { s.luck += 0.04; } },
{ id: 'shieldheart', name: 'Shield Heart', desc: '+½ max shield heart (regens 25% per floor)', apply: (s, p) => { s.shieldPerk = (s.shieldPerk || 0) + 1; p.shieldHp = (p.shieldHp || 0) + 1; } },
{ id: 'bloodrush',name: 'Bloodrush', desc: '+5% XP gain, bonus crystal chance', apply: s => { s.xpMul *= 1.05; } },
{ id: 'deadeye', name: 'Deadeye', desc: '+5% shot speed', apply: s => { s.shotSpeedMul *= 1.05; } },
{ id: 'critbone', name: 'Bone Splitter', desc: '+2% critical chance', apply: s => { s.crit += 0.02; } },
+4 -3
View File
@@ -305,7 +305,7 @@ function pressureRelief(dmgTaken) {
const severity = clamp(dmgTaken / Math.max(2, p.stats.maxHp * 0.25), 0.4, 2);
const desperation = 1 + (1 - hpFrac) * 2;
const churn = 1 + Math.min(G.recentHits.length, 4) * 0.35;
return PRESSURE_DROP_BASE * PRESSURE_UNIT * severity * desperation * churn * pressureDropScale();
return PRESSURE_DROP_BASE * PRESSURE_UNIT * severity * desperation * churn * pressureDropScale() * PRESSURE_RELIEF_MUL;
}
function hurtPlayer(dmg, ang, attacker) {
@@ -333,8 +333,9 @@ function hurtPlayer(dmg, ang, attacker) {
// Meat Grinder: aura damages you for +1 per hit taken
if (p.stats.meatGrinder > 0) reduced += p.stats.meatGrinder;
// shield hearts absorb first (from the Shield Heart perk)
let absorbed = 0;
if (p.shieldHp > 0) {
const absorbed = Math.min(p.shieldHp, reduced);
absorbed = Math.min(p.shieldHp, reduced);
p.shieldHp -= absorbed;
reduced -= absorbed;
if (p.shieldHp <= 0) { spawnText(p.x, p.y - 20, 'SHIELD DOWN', '#3bc9e0'); Sfx.shieldBreak(); }
@@ -352,7 +353,7 @@ function hurtPlayer(dmg, ang, attacker) {
G.roomDamaged = true;
G.recentHits = (G.recentHits || []).filter(t => G.time - t < 12);
G.recentHits.push(G.time);
applyPressureDelta(-pressureRelief(reduced));
if (absorbed === 0) applyPressureDelta(-pressureRelief(reduced));
G.streak = 0;
}
if (attacker && attacker.hp > 0 && p.stats.thorns > 0) {
+7 -4
View File
@@ -202,7 +202,7 @@ function updateRoom(dt) {
const stalled = G.time - G.roomEnterT > 90;
if (lowHealth || stalled) {
const decay = lowHealth ? 0.02 : 0.008;
applyPressureDelta(-decay * dt * pressureDropScale());
applyPressureDelta(-decay * dt * pressureDropScale() * PRESSURE_RELIEF_MUL);
}
}
G.recentHits = (G.recentHits || []).filter(t => G.time - t < 12);
@@ -304,9 +304,12 @@ function nextFloor() {
Sfx.stairs();
genFloor(G.floor);
enterRoom(0, 0);
// Shield Heart perk: fresh shield hearts each floor
G.player.shieldHp = G.player.stats.shieldPerk || 0;
if (G.player.shieldHp > 0) { spawnText(G.player.x, G.player.y - 20, 'SHIELD UP', '#3bc9e0'); Sfx.shieldUp(); }
// Shield Heart: regen 25% of max shields per floor, rounded up to the next
// whole shield (1 shield = 2 shieldHp). Full shields only at run start.
const maxShield = G.player.stats.shieldPerk || 0;
const shieldBefore = G.player.shieldHp;
G.player.shieldHp = Math.min(maxShield, G.player.shieldHp + 2 * Math.ceil(maxShield / 8));
if (G.player.shieldHp > shieldBefore) { spawnText(G.player.x, G.player.y - 20, 'SHIELD UP', '#3bc9e0'); Sfx.shieldUp(); }
// legendary once-per-floor flags reset
G.player.secondSkinUsed = false;
G.player.lastCutActive = false;
+1 -1
View File
@@ -111,7 +111,7 @@ const SfxBank = {
if (this.buffers.has(name) || this.pending.has(name) || this.failed.has(name)) return;
const file = this.FILES[name];
if (!file || !this.ctx || typeof fetch !== 'function') return;
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=57')
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=58')
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
.then(bytes => this.ctx.decodeAudioData(bytes))
.then(buffer => this.buffers.set(name, this.trim(buffer)))
+3 -3
View File
@@ -18,7 +18,7 @@ const SPRITE_MANIFEST = [
'decal_blood1', 'decal_blood2', 'decal_blood3', 'decal_blood4',
// level-up perk emblems
'perk_adrenal', 'perk_sharpen', 'perk_quick', 'perk_longbone', 'perk_bigheart',
'perk_magnetb', 'perk_shieldheart', 'perk_scavenge', 'perk_bloodrush', 'perk_deadeye',
'perk_magnetb', 'perk_shieldheart', 'perk_bloodrush', 'perk_deadeye',
'perk_critbone', 'perk_critmeat', 'perk_flensing', 'perk_ember', 'perk_frostbile',
'perk_heavyhand', 'perk_thickhide', 'perk_secondwind', 'perk_scrapfeed', 'perk_boneknit',
'perk_spiteflesh', 'perk_carrion', 'perk_sinew',
@@ -88,10 +88,10 @@ const Sprites = {
const finish = () => { if (--pending === 0) G.imagesLoaded = true; };
img.onload = () => { this.imgs[name] = img; if (typeof img.decode === 'function') img.decode().then(finish, finish); else finish(); };
img.onerror = () => {
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=57'; return; }
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=58'; return; }
finish();
};
img.src = hd ? 'assets/hd/' + name + '.webp?v=57' : 'assets/' + name + '.png?v=57';
img.src = hd ? 'assets/hd/' + name + '.webp?v=58' : 'assets/' + name + '.png?v=58';
}
},
+1
View File
@@ -70,6 +70,7 @@ const WEAPON_DROP_LOCKOUT = 2.5;
const PRESSURE_UNIT = 0.01; // one "point" on the dial scale
const PRESSURE_GAIN = PRESSURE_UNIT; // retained: dial 0 == 1 point
const PRESSURE_DROP_BASE = 3; // dial 0 == 0.03 relief base
const PRESSURE_RELIEF_MUL = 0.5; // global brake: all pressure drops (hit relief + passive decay) are halved
const DEATH_LOCK = 3;
const PRESSURE_DIAL_MIN = -10, PRESSURE_DIAL_MAX = 10;