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
This commit is contained in:
MeatSlicer Developer committed 2026-07-30 00:01:06 +10:00
1 parent d255be5e39
commit a02cff52cf
24 files changed
+667 -201

No files matched your search

+106 -80
View File
@@ -9,22 +9,25 @@ const HELP_PAGES = [
{ title: 'THE LOOP' },
{ title: 'ARSENAL' },
{ title: 'MUTATIONS' },
{ title: 'IMPLANTS' },
{ title: 'IMPLANTS I' },
{ title: 'IMPLANTS II' },
{ title: 'BESTIARY' },
{ title: 'PRESSURE' },
];
const HELP_PANEL = { w: 720, h: 500, y: 66 };
const HELP_PANEL = { w: 920, h: 600, y: 22 };
const HELP_SCALE = 1.4;
function hfont(px, bold) { return (bold ? 'bold ' : '') + Math.round(px * HELP_SCALE) + 'px monospace'; }
function helpPanelX() { return (W - HELP_PANEL.w) / 2; }
function helpTabRect(i) {
const tw = 96, gap = 6;
const tw = 108, gap = 6;
const total = HELP_PAGES.length * tw + (HELP_PAGES.length - 1) * gap;
return { x: W / 2 - total / 2 + i * (tw + gap), y: HELP_PANEL.y + 66, w: tw, h: 26 };
return { x: W / 2 - total / 2 + i * (tw + gap), y: HELP_PANEL.y + 96, w: tw, h: 32 };
}
function helpCloseRect() {
return { x: helpPanelX() + HELP_PANEL.w - 74, y: HELP_PANEL.y + 12, w: 62, h: 26 };
return { x: helpPanelX() + HELP_PANEL.w - 92, y: HELP_PANEL.y + 18, w: 62, h: 30 };
}
function inRect(mx, my, r) { return mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h; }
@@ -51,28 +54,30 @@ function updatePauseHelp() {
}
// ---- shared text helpers ----
// Body text starts at py + 172, clear of the title + tab row.
const HELP_BODY = 172;
function helpLines(ctx, px, ty, sections, maxW) {
ctx.textAlign = 'left';
for (const sec of sections) {
if (sec.head) {
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 10px monospace';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(10, true);
ctx.fillText(sec.head, px + 40, ty);
ty += 21;
ty += 24;
}
ctx.fillStyle = '#b8a89e'; ctx.font = '9px monospace';
ctx.fillStyle = '#b8a89e'; ctx.font = hfont(9, false);
for (const line of sec.lines) {
const wraps = ctx.measureText(line).width > maxW ? 1 : 0;
wrapText(ctx, line, px + 40, ty, maxW, 13);
ty += 15 + wraps * 13;
wrapText(ctx, line, px + 40, ty, maxW, 15);
ty += 17 + wraps * 15;
}
ty += 9;
ty += 6;
}
return ty;
}
// ---- page bodies ----
function drawHelpControls(ctx, px, py) {
helpLines(ctx, px, py + 116, [
helpLines(ctx, px, py + HELP_BODY, [
{ head: 'IN THE FIELD', lines: [
'WASD / ARROWS — MOVE MOUSE — AIM LMB (HOLD) — FIRE',
'LMB RELEASE — FIRE CHARGED BEAM (SPINAL TAP ONLY)',
@@ -85,7 +90,7 @@ function drawHelpControls(ctx, px, py) {
'R — REROLL ALL THREE (COSTS 1 REROLL TOKEN)',
] },
{ head: 'PAUSE / JUKEBOX', lines: [
'- / = — SFX VOLUME · , / . — MUSIC VOLUME',
'- / = — SFX VOLUME · , / . — MUSIC VOLUME · ; / \' — HUD OPACITY',
'[ / ] OR ARROWS — CHANGE TRACK (LIVE BOSS FIGHTS OVERRIDE)',
'H OR THE [?] BUTTON — THIS MANUAL',
] },
@@ -97,7 +102,7 @@ function drawHelpControls(ctx, px, py) {
}
function drawHelpLoop(ctx, px, py) {
helpLines(ctx, px, py + 116, [
helpLines(ctx, px, py + HELP_BODY, [
{ head: 'A RUN', lines: [
'ROOMS PER FLOOR: MIN(5 + FLOOR, 9). BOSS = FARTHEST ROOM.',
'ITEM ROOM = SECOND FARTHEST (DIST ≥ 2): PEDESTAL + 35% WEAPON.',
@@ -107,14 +112,14 @@ function drawHelpLoop(ctx, px, py) {
] },
{ head: 'WAVES PER COMBAT ROOM', lines: [
'BASE = 3 + CEIL(FLOOR × 1.4) · F2+: +CEIL(BASE × 0.7) · F4+: +CEIL(BASE × 0.5)',
'WAVES SPAWN 0.9S APART · HARD CAP 72 LIVE ENEMIES',
'WAVES SPAWN 0.9S APART · INCOMING SPAWNS TELEGRAPH FOR 0.5S · CAP 72',
] },
{ head: 'DROPS (LUCK SCALES MOST CHANCES)', lines: [
'ROOM CLEAR: 3–5+FLOOR XP · 24%+LUCK×30% AMMO · 10%+LUCK×30% HEART',
'NORMAL KILL: 7%+LUCK×4% AMMO · 2.2%+LUCK×3% HEART',
'ELITE KILL: 3× XP + 3–5 GEMS · 18%+LUCK×10% ITEM · ELSE 60%+LUCK×20% AMMO',
] },
{ head: 'SCORE', lines: [
{ head: 'SCORE (× LIVE PRESSURE)', lines: [
'KILL +10 · ELITE +40 · BOSS 500 + FLOOR × 100 · ROOM +50 · FLOOR +250',
'XP +1 · FULL-HP HEART +25 · TIER-IX DUPLICATE ITEM +150',
] },
@@ -123,35 +128,33 @@ function drawHelpLoop(ctx, px, py) {
function drawHelpArsenal(ctx, px, py) {
const x = px + 40;
let ty = py + 112;
let ty = py + HELP_BODY;
ctx.textAlign = 'left';
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(9, true);
ctx.fillText('16 WEAPONS · HIGHER TIERS DROP MORE ON DEEPER FLOORS · BONE POPPER = INFINITE', x, ty);
ty += 18;
const cols = [0, 168, 208, 252, 300, 372, 440];
ty += 20;
const cols = [0, 235, 291, 353, 420, 521, 616];
const head = ['NAME', 'TIER', 'DMG', 'RATE', 'AMMO', 'DPS', 'BEHAVIOR'];
ctx.fillStyle = '#7d6a68'; ctx.font = 'bold 8px monospace';
ctx.fillStyle = '#7d6a68'; ctx.font = hfont(8, true);
for (let c = 0; c < cols.length; c++) ctx.fillText(head[c], x + cols[c], ty);
ty += 13;
ctx.font = '8px monospace';
ty += 16;
ctx.font = hfont(8, false);
let row = 0;
for (const w of Object.values(WEAPONS)) {
if (row % 2 === 0) { ctx.fillStyle = 'rgba(255,220,200,0.03)'; ctx.fillRect(x - 4, ty - 9, HELP_PANEL.w - 72, 12); }
if (row % 2 === 0) { ctx.fillStyle = 'rgba(255,220,200,0.03)'; ctx.fillRect(x - 4, ty - 11, HELP_PANEL.w - 72, 15); }
const dps = Math.round(w.dmg / Math.max(0.01, w.interval) * (w.pellets || 1));
const vals = [w.name.toUpperCase(), w.tier < 0 ? 'SIDE' : 'T' + w.tier, String(w.dmg),
w.interval.toFixed(2) + 's', w.ammo === Infinity ? '∞' : String(w.ammo), String(dps), w.behavior.toUpperCase()];
ctx.fillStyle = '#c9b8ae';
for (let c = 0; c < cols.length; c++) ctx.fillText(vals[c], x + cols[c], ty);
ty += 12.5;
ty += 15;
row++;
}
ty += 8;
ty += 10;
helpLines(ctx, px, ty, [
{ head: 'SHARED WEAPON STATS (FROM IMPLANTS)', lines: [
'SPLIT / FAN / REAR ADD VOLLEYS TO ALL BEHAVIORS — BEAMS, SLAMS, SAWS INCLUDED.',
'HOMING STEERS EVERY PROJECTILE · RANGE EXTENDS BEAMS, SLAMS, LOBS AND SAW REACH',
'PIERCE / BOUNCE BECOME +6% POWER EACH ON MELEE-STYLE WEAPONS (SLAM/SAW/BEAM)',
'FIRE RATE SPEEDS THE CHAINSAW TICK AND SPINAL TAP CHARGE · SIZE SCALES ALL HITBOXES',
'SPLIT / FAN / REAR ADD VOLLEYS TO ALL BEHAVIORS · HOMING STEERS PROJECTILES · RANGE EXTENDS BEAMS/SLAMS/LOBS',
'PIERCE / BOUNCE = +6% POWER ON MELEE (SLAM/SAW/BEAM) · FIRE RATE SPEEDS SAW TICK & TAP CHARGE · SIZE SCALES ALL HITBOXES',
'AMMO PICKUPS REFILL HELD + HOLSTERED, CAPPED AT 150% OF ONE MAGAZINE',
] },
], HELP_PANEL.w - 80);
@@ -159,65 +162,71 @@ function drawHelpArsenal(ctx, px, py) {
function drawHelpPerkList(ctx, px, py) {
const x = px + 40;
let ty = py + 112;
let ty = py + HELP_BODY;
ctx.textAlign = 'left';
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
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);
ty += 8;
ctx.fillStyle = '#7d6a68'; ctx.font = '8px monospace';
ctx.fillText('REROLL RIB GRANTS TOKENS (R TO REDRAW) · AUTO-DRAFT (T) PICKS RANDOMLY', x, ty + 8);
ty += 26;
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);
ty += 34;
const colW = (HELP_PANEL.w - 80) / 2;
for (let i = 0; i < PERKS.length; i++) {
const col = Math.floor(i / 12), r = i % 12;
const lx = x + col * colW, ly = ty + r * 15;
ctx.fillStyle = '#e3b746'; ctx.font = 'bold 8px monospace';
const lx = x + col * colW, ly = ty + r * 20;
ctx.fillStyle = '#e3b746'; ctx.font = hfont(8, true);
ctx.fillText(PERKS[i].name.toUpperCase(), lx, ly);
ctx.fillStyle = '#a08d84'; ctx.font = '8px monospace';
ctx.fillText(fit(PERKS[i].desc.toUpperCase(), 46), lx, ly + 9);
ctx.fillStyle = '#a08d84'; ctx.font = hfont(8, false);
ctx.fillText(fit(PERKS[i].desc.toUpperCase(), 40), lx, ly + 12);
}
}
function drawHelpItemList(ctx, px, py) {
function drawHelpItemList(ctx, px, py, half) {
const x = px + 40;
let ty = py + 112;
let ty = py + HELP_BODY;
ctx.textAlign = 'left';
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(9, true);
ctx.fillText('46 IMPLANTS · STACK TO TIER IX · DUPLICATE ROLLS FAVORED · TIER IX → +150 SCORE', x, ty);
ty += 18;
ty += 11;
ctx.fillStyle = '#7d6a68'; ctx.font = hfont(8, false);
ctx.fillText('STICKINESS RAMPS 10% PER DISTINCT UPGRADABLE ITEM OWNED (CAP 50%) · PAGE ' + (half + 1) + ' OF 2', x, ty + 11);
ty += 34;
const entries = Object.entries(ITEMS);
const halfEntries = entries.slice(half * 23, half * 23 + 23);
const colW = (HELP_PANEL.w - 80) / 2;
const rows = Math.ceil(entries.length / 2);
for (let i = 0; i < entries.length; i++) {
const col = Math.floor(i / rows), r = i % rows;
const lx = x + col * colW, ly = ty + r * 12.6;
ctx.fillStyle = '#e3b746'; ctx.font = 'bold 7px monospace';
ctx.fillText(entries[i][1].name.toUpperCase(), lx, ly);
ctx.fillStyle = '#a08d84'; ctx.font = '7px monospace';
ctx.fillText(fit(entries[i][1].desc.toUpperCase(), 52), lx + 118, ly);
for (let i = 0; i < halfEntries.length; i++) {
const col = Math.floor(i / 12), r = i % 12;
const lx = x + col * colW, ly = ty + r * 20;
ctx.fillStyle = '#e3b746'; ctx.font = hfont(8, true);
ctx.fillText(halfEntries[i][1].name.toUpperCase(), lx, ly);
ctx.fillStyle = '#a08d84'; ctx.font = hfont(8, false);
ctx.fillText(fit(halfEntries[i][1].desc.toUpperCase(), 40), lx, ly + 12);
}
}
function drawHelpItemList1(ctx, px, py) { drawHelpItemList(ctx, px, py, 0); }
function drawHelpItemList2(ctx, px, py) { drawHelpItemList(ctx, px, py, 1); }
function drawHelpBestiary(ctx, px, py) {
const x = px + 40;
let ty = py + 112;
let ty = py + HELP_BODY;
ctx.textAlign = 'left';
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(9, true);
ctx.fillText('6 ENEMY TYPES · ELITES: ×2.6 HP, +1 DMG, 3× XP, GOLD RING, ITEM DROPS', x, ty);
ty += 16;
ty += 20;
const notes = { shambler: 'CHASER', runner: 'FAST CHASER', spitter: 'RANGED KITER', splitter: 'SPLITS INTO 2 MINIS', mini: 'SWARMER', exploder: 'FUSED SUICIDE ×2 DMG' };
ctx.font = '8px monospace';
ctx.font = hfont(8, false);
let row = 0;
for (const [type, t] of Object.entries(ENEMY_TYPES)) {
if (row % 2 === 0) { ctx.fillStyle = 'rgba(255,220,200,0.03)'; ctx.fillRect(x - 4, ty - 9, HELP_PANEL.w - 72, 12); }
if (row % 2 === 0) { ctx.fillStyle = 'rgba(255,220,200,0.03)'; ctx.fillRect(x - 4, ty - 11, HELP_PANEL.w - 72, 15); }
ctx.fillStyle = '#c9b8ae';
ctx.fillText(type.toUpperCase(), x, ty);
ctx.fillText('HP ' + t.hp, x + 100, ty);
ctx.fillText('SPD ' + t.spd, x + 150, ty);
ctx.fillText('DMG ' + t.dmg, x + 210, ty);
ctx.fillText('XP ' + t.xp, x + 260, ty);
ctx.fillText(notes[type] || '', x + 300, ty);
ty += 12.5;
ctx.fillText('HP ' + t.hp, x + 140, ty);
ctx.fillText('SPD ' + t.spd, x + 210, ty);
ctx.fillText('DMG ' + t.dmg, x + 294, ty);
ctx.fillText('XP ' + t.xp, x + 364, ty);
ctx.fillText(notes[type] || '', x + 420, ty);
ty += 15;
row++;
}
ty += 8;
@@ -229,27 +238,31 @@ function drawHelpBestiary(ctx, px, py) {
'CONTACT DAMAGE NEVER SCALES — ONLY ELITES GET +1',
] },
], HELP_PANEL.w - 80);
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
ctx.fillStyle = '#d7a934'; ctx.font = hfont(9, true);
ctx.fillText('9 BOSSES — ROTATION = (FLOOR − 1) MOD 9 · HP × (1 + 0.30(FLOOR−1)) · CYCLES ADD PATTERNS', x, ty);
ty += 16;
ctx.font = '8px monospace';
ty += 18;
ctx.font = hfont(8, false);
for (let i = 0; i < BOSS_DEFS.length; i++) {
const col = i % 3, r = Math.floor(i / 3);
ctx.fillStyle = '#c9b8ae';
ctx.fillText(BOSS_DEFS[i].name + ' · ' + BOSS_DEFS[i].hp + ' HP', x + col * 210, ty + r * 13);
ctx.fillText(BOSS_DEFS[i].name + ' · ' + BOSS_DEFS[i].hp + ' HP', x + col * 294, ty + r * 16);
}
}
function drawHelpPressure(ctx, px, py) {
helpLines(ctx, px, py + 116, [
const d = G.pressureDial;
const gain = pressureGainUnits(d), drop = pressureDropUnits(d), scale = pressureDropScale();
helpLines(ctx, px, py + HELP_BODY, [
{ head: 'ADAPTIVE PRESSURE (HUD, BOTTOM-RIGHT)', lines: [
'MULTIPLIES ENEMY AND BOSS HP + SPEED AT SPAWN TIME · RANGE 0.75× – 1.60×',
'+0.01 FOR EVERY ROOM CLEARED WITHOUT TAKING ANY DAMAGE',
'RELIEF ON HIT = 0.03 × SEVERITY × DESPERATION × CHURN',
'ALL SCORE IS MULTIPLIED BY LIVE PRESSURE',
'DIAL ' + (d > 0 ? '+' : '') + d + ' — RISE ' + gain.toFixed(1) + '/5 PER CLEAN ROOM · RELIEF ' + drop.toFixed(1) + '/5 ON HIT',
'+' + pressureGain().toFixed(3) + ' FOR EVERY ROOM CLEARED WITHOUT TAKING ANY DAMAGE',
'RELIEF ON HIT = ' + (0.03 * 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/S BELOW 35% HP · 0.008/S AFTER 90S IN ONE ROOM',
'DECAY: ' + (0.02 * scale).toFixed(3) + '/S BELOW 35% HP · ' + (0.008 * scale).toFixed(3) + '/S AFTER 90S IN ONE ROOM',
'LIVING ENEMIES KEEP THEIR STATS — ONLY NEW SPAWNS CHANGE',
'STREAK = CONSECUTIVE FLAWLESS CLEARS (COSMETIC)',
] },
@@ -263,7 +276,7 @@ function drawHelpPressure(ctx, px, py) {
], HELP_PANEL.w - 80);
}
const HELP_RENDERERS = [drawHelpControls, drawHelpLoop, drawHelpArsenal, drawHelpPerkList, drawHelpItemList, drawHelpBestiary, drawHelpPressure];
const HELP_RENDERERS = [drawHelpControls, drawHelpLoop, drawHelpArsenal, drawHelpPerkList, drawHelpItemList1, drawHelpItemList2, drawHelpBestiary, drawHelpPressure];
function drawPauseHelp(ctx) {
ctx.fillStyle = 'rgba(4,2,3,0.84)';
@@ -274,10 +287,10 @@ function drawPauseHelp(ctx) {
blood: true, seed: G.helpPage + 21, bloodAlpha: 0.2,
});
ctx.textAlign = 'center';
ctx.fillStyle = '#8eb7bd'; ctx.font = 'bold 9px monospace';
ctx.fillText('BUTCHER FIELD MANUAL — PAGE ' + (G.helpPage + 1) + ' / ' + HELP_PAGES.length, W / 2, py + 30);
ctx.fillStyle = '#f0e5d7'; ctx.font = 'bold 22px monospace';
ctx.fillText(HELP_PAGES[G.helpPage].title, W / 2, py + 56);
ctx.fillStyle = '#8eb7bd'; ctx.font = hfont(9, true);
ctx.fillText('BUTCHER FIELD MANUAL — PAGE ' + (G.helpPage + 1) + ' / ' + HELP_PAGES.length, W / 2, py + 42);
ctx.fillStyle = '#f0e5d7'; ctx.font = hfont(22, true);
ctx.fillText(HELP_PAGES[G.helpPage].title, W / 2, py + 78);
const cr = helpCloseRect();
drawPixelTag(ctx, '[X]', cr.x, cr.y, { width: cr.w, height: cr.h, color: '#e8bf47', accent: '#9d7726' });
@@ -290,18 +303,31 @@ function drawPauseHelp(ctx) {
color: active ? '#f1e4d5' : (hover ? '#cdbdb2' : '#7d6a68'),
accent: active ? '#4a9cad' : '#3c2f38',
fill: active ? 'rgba(24,15,22,0.95)' : 'rgba(12,7,11,0.85)',
font: 'bold 8px monospace',
font: hfont(8, true),
});
}
HELP_RENDERERS[G.helpPage](ctx, px, py);
ctx.textAlign = 'center';
const dotTotal = HELP_PAGES.length * 16 - 8;
for (let i = 0; i < HELP_PAGES.length; i++) {
ctx.fillStyle = i === G.helpPage ? '#d7a934' : '#4d3f44';
ctx.fillRect(W / 2 - 52 + i * 16, py + ph - 42, 8, 3);
ctx.fillRect(W / 2 - dotTotal / 2 + i * 16, py + ph - 42, 8, 3);
}
drawPixelTag(ctx, '[H / ESC] BACK [← →] PAGES [1-' + HELP_PAGES.length + '] JUMP', W / 2 - 150, py + ph - 30, {
width: 300, height: 22, color: '#8f7f79', accent: '#4d3a42', font: 'bold 8px monospace',
drawPixelTag(ctx, '[H / ESC] BACK [← →] PAGES [1-' + HELP_PAGES.length + '] JUMP', W / 2 - 180, py + ph - 30, {
width: 360, height: 22, color: '#8f7f79', accent: '#4d3a42', font: hfont(8, true),
});
}
function wrapText(ctx, text, x, y, maxW, lh) {
const words = text.split(' ');
let line = '', yy = y;
for (const w of words) {
const test = line ? line + ' ' + w : w;
if (ctx.measureText(test).width > maxW && line) {
ctx.fillText(line, x, yy); yy += lh; line = w;
} else line = test;
}
ctx.fillText(line, x, yy);
}