Add in-game field manual on the pause screen
A [?] HELP button (or H key) on the pause screen opens a seven-page technical manual: controls, run loop, arsenal, mutations, implants, bestiary and pressure/defense math. List pages render live from the WEAPONS/ITEMS/PERKS/ENEMY_TYPES/BOSS_DEFS tables so values never drift. Also unifies the sfxbank fetch cache-buster with the v36 bump.
This commit is contained in:
1 parent
27e7fa67ad
commit
3f856e416a
7 files changed
+364
-24
No files matched your search
+21
-20
@@ -6,30 +6,31 @@
|
||||
<meta name="theme-color" content="#0a0305">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><text y=%2252%22 x=%2250%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-size=%2252%22>🔪</text></svg>">
|
||||
<title>MEATSLICER</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=35">
|
||||
<link rel="stylesheet" href="css/style.css?v=36">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<canvas id="game" width="960" height="640"></canvas>
|
||||
</div>
|
||||
<script src="js/utils.js?v=35"></script>
|
||||
<script src="js/state.js?v=35"></script>
|
||||
<script src="js/input.js?v=35"></script>
|
||||
<script src="js/sfx.js?v=35"></script>
|
||||
<script src="js/sfxbank.js?v=35"></script>
|
||||
<script src="js/audio.js?v=35"></script>
|
||||
<script src="js/sprites.js?v=35"></script>
|
||||
<script src="js/particles.js?v=35"></script>
|
||||
<script src="js/weapons.js?v=35"></script>
|
||||
<script src="js/bullets.js?v=35"></script>
|
||||
<script src="js/pickups.js?v=35"></script>
|
||||
<script src="js/items.js?v=35"></script>
|
||||
<script src="js/perks.js?v=35"></script>
|
||||
<script src="js/enemies.js?v=35"></script>
|
||||
<script src="js/bosses.js?v=35"></script>
|
||||
<script src="js/rooms.js?v=35"></script>
|
||||
<script src="js/player.js?v=35"></script>
|
||||
<script src="js/hud.js?v=35"></script>
|
||||
<script src="js/main.js?v=35"></script>
|
||||
<script src="js/utils.js?v=36"></script>
|
||||
<script src="js/state.js?v=36"></script>
|
||||
<script src="js/input.js?v=36"></script>
|
||||
<script src="js/sfx.js?v=36"></script>
|
||||
<script src="js/sfxbank.js?v=36"></script>
|
||||
<script src="js/audio.js?v=36"></script>
|
||||
<script src="js/sprites.js?v=36"></script>
|
||||
<script src="js/particles.js?v=36"></script>
|
||||
<script src="js/weapons.js?v=36"></script>
|
||||
<script src="js/bullets.js?v=36"></script>
|
||||
<script src="js/pickups.js?v=36"></script>
|
||||
<script src="js/items.js?v=36"></script>
|
||||
<script src="js/perks.js?v=36"></script>
|
||||
<script src="js/enemies.js?v=36"></script>
|
||||
<script src="js/bosses.js?v=36"></script>
|
||||
<script src="js/rooms.js?v=36"></script>
|
||||
<script src="js/player.js?v=36"></script>
|
||||
<script src="js/hud.js?v=36"></script>
|
||||
<script src="js/help.js?v=36"></script>
|
||||
<script src="js/main.js?v=36"></script>
|
||||
</body>
|
||||
</html>
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
// ---- in-game field manual (opened from the pause screen) ----
|
||||
// List pages render live from WEAPONS / ITEMS / PERKS / ENEMY_TYPES / BOSS_DEFS
|
||||
// so the manual never drifts from the actual game data.
|
||||
|
||||
const HELP_BUTTON = { x: W - 104, y: 24, w: 80, h: 26 };
|
||||
|
||||
const HELP_PAGES = [
|
||||
{ title: 'CONTROLS' },
|
||||
{ title: 'THE LOOP' },
|
||||
{ title: 'ARSENAL' },
|
||||
{ title: 'MUTATIONS' },
|
||||
{ title: 'IMPLANTS' },
|
||||
{ title: 'BESTIARY' },
|
||||
{ title: 'PRESSURE' },
|
||||
];
|
||||
|
||||
const HELP_PANEL = { w: 720, h: 500, y: 66 };
|
||||
function helpPanelX() { return (W - HELP_PANEL.w) / 2; }
|
||||
|
||||
function helpTabRect(i) {
|
||||
const tw = 96, 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 };
|
||||
}
|
||||
|
||||
function helpCloseRect() {
|
||||
return { x: helpPanelX() + HELP_PANEL.w - 74, y: HELP_PANEL.y + 12, w: 62, h: 26 };
|
||||
}
|
||||
|
||||
function inRect(mx, my, r) { return mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h; }
|
||||
|
||||
function fit(line, n) { return line.length > n ? line.slice(0, n - 1) + '…' : line; }
|
||||
|
||||
function updatePauseHelp() {
|
||||
if (keyPressed('h', 'escape')) { G.pauseHelp = false; Sfx.menu(); return; }
|
||||
if (keyPressed('arrowright')) { G.helpPage = (G.helpPage + 1) % HELP_PAGES.length; Sfx.menu(); }
|
||||
else if (keyPressed('arrowleft')) { G.helpPage = (G.helpPage + HELP_PAGES.length - 1) % HELP_PAGES.length; Sfx.menu(); }
|
||||
else {
|
||||
for (let i = 0; i < HELP_PAGES.length; i++) {
|
||||
if (keyPressed(String(i + 1))) { G.helpPage = i; Sfx.menu(); break; }
|
||||
}
|
||||
}
|
||||
if (Input.mpressed) {
|
||||
if (inRect(Input.mx, Input.my, helpCloseRect()) || inRect(Input.mx, Input.my, HELP_BUTTON)) {
|
||||
G.pauseHelp = false; Sfx.menu(); return;
|
||||
}
|
||||
for (let i = 0; i < HELP_PAGES.length; i++) {
|
||||
if (inRect(Input.mx, Input.my, helpTabRect(i))) { G.helpPage = i; Sfx.menu(); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- shared text helpers ----
|
||||
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.fillText(sec.head, px + 40, ty);
|
||||
ty += 21;
|
||||
}
|
||||
ctx.fillStyle = '#b8a89e'; ctx.font = '9px monospace';
|
||||
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;
|
||||
}
|
||||
ty += 9;
|
||||
}
|
||||
return ty;
|
||||
}
|
||||
|
||||
// ---- page bodies ----
|
||||
function drawHelpControls(ctx, px, py) {
|
||||
helpLines(ctx, px, py + 116, [
|
||||
{ head: 'IN THE FIELD', lines: [
|
||||
'WASD / ARROWS — MOVE MOUSE — AIM LMB (HOLD) — FIRE',
|
||||
'LMB RELEASE — FIRE CHARGED BEAM (SPINAL TAP ONLY)',
|
||||
'R — SWAP BONE POPPER ↔ HOLSTERED SPECIAL (EMPTY SPECIALS LOCKED)',
|
||||
'P / ESC — PAUSE · M — MUTE · N — NEXT TRACK · T — AUTO-DRAFT',
|
||||
'DESKTOP APP: F11 — FULLSCREEN · F12 — DEVTOOLS · CTRL+R — RELOAD',
|
||||
] },
|
||||
{ head: 'MUTATION DRAFT (LEVEL-UP)', lines: [
|
||||
'1 / 2 / 3 OR CLICK — CHOOSE PERK · 4 / SPACE — RANDOM CUT',
|
||||
'R — REROLL ALL THREE (COSTS 1 REROLL TOKEN)',
|
||||
] },
|
||||
{ head: 'PAUSE / JUKEBOX', lines: [
|
||||
'- / = — SFX VOLUME · , / . — MUSIC VOLUME',
|
||||
'[ / ] OR ARROWS — CHANGE TRACK (LIVE BOSS FIGHTS OVERRIDE)',
|
||||
'H OR THE [?] BUTTON — THIS MANUAL',
|
||||
] },
|
||||
{ head: 'MENUS', lines: [
|
||||
'ENTER / SPACE / CLICK — START RUN · R / ENTER — RESTART AFTER DEATH',
|
||||
'PICKUPS ARE COLLECTED BY WALKING OVER THEM — NO INTERACT KEY',
|
||||
] },
|
||||
], HELP_PANEL.w - 80);
|
||||
}
|
||||
|
||||
function drawHelpLoop(ctx, px, py) {
|
||||
helpLines(ctx, px, py + 116, [
|
||||
{ head: 'A RUN', lines: [
|
||||
'ROOMS PER FLOOR: MIN(5 + FLOOR, 9). BOSS = FARTHEST ROOM.',
|
||||
'ITEM ROOM = SECOND FARTHEST (DIST ≥ 2): PEDESTAL + 35% WEAPON.',
|
||||
'DOORS LOCK UNTIL EVERY WAVE DIES. RE-ENTRY RESTARTS REMAINING WAVES.',
|
||||
'BOSS DROPS: ITEM PEDESTAL + WEAPON (ROLLED AT FLOOR + 2) + STAIRS.',
|
||||
'NO FINAL FLOOR — DESCEND, SCORE, SURVIVE. BEST CUT IS SAVED.',
|
||||
] },
|
||||
{ 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',
|
||||
] },
|
||||
{ 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: [
|
||||
'KILL +10 · ELITE +40 · BOSS 500 + FLOOR × 100 · ROOM +50 · FLOOR +250',
|
||||
'XP +1 · FULL-HP HEART +25 · TIER-IX DUPLICATE ITEM +150',
|
||||
] },
|
||||
], HELP_PANEL.w - 80);
|
||||
}
|
||||
|
||||
function drawHelpArsenal(ctx, px, py) {
|
||||
const x = px + 40;
|
||||
let ty = py + 112;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
|
||||
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];
|
||||
const head = ['NAME', 'TIER', 'DMG', 'RATE', 'AMMO', 'DPS', 'BEHAVIOR'];
|
||||
ctx.fillStyle = '#7d6a68'; ctx.font = 'bold 8px monospace';
|
||||
for (let c = 0; c < cols.length; c++) ctx.fillText(head[c], x + cols[c], ty);
|
||||
ty += 13;
|
||||
ctx.font = '8px monospace';
|
||||
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); }
|
||||
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;
|
||||
row++;
|
||||
}
|
||||
ty += 8;
|
||||
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',
|
||||
'AMMO PICKUPS REFILL HELD + HOLSTERED, CAPPED AT 150% OF ONE MAGAZINE',
|
||||
] },
|
||||
], HELP_PANEL.w - 80);
|
||||
}
|
||||
|
||||
function drawHelpPerkList(ctx, px, py) {
|
||||
const x = px + 40;
|
||||
let ty = py + 112;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
|
||||
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;
|
||||
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';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHelpItemList(ctx, px, py) {
|
||||
const x = px + 40;
|
||||
let ty = py + 112;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
|
||||
ctx.fillText('46 IMPLANTS · STACK TO TIER IX · DUPLICATE ROLLS FAVORED · TIER IX → +150 SCORE', x, ty);
|
||||
ty += 18;
|
||||
const entries = Object.entries(ITEMS);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHelpBestiary(ctx, px, py) {
|
||||
const x = px + 40;
|
||||
let ty = py + 112;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
|
||||
ctx.fillText('6 ENEMY TYPES · ELITES: ×2.6 HP, +1 DMG, 3× XP, GOLD RING, ITEM DROPS', x, ty);
|
||||
ty += 16;
|
||||
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';
|
||||
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); }
|
||||
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;
|
||||
row++;
|
||||
}
|
||||
ty += 8;
|
||||
ty = helpLines(ctx, px, ty, [
|
||||
{ head: 'SCALING (APPLIED AT SPAWN)', lines: [
|
||||
'POWER = (LEVEL − 1) × 0.5 + Σ ALL ITEM TIERS',
|
||||
'ENEMY HP = BASE × (1 + 0.22(FLOOR−1)) × (1 + 0.05·POWER) × PRESSURE',
|
||||
'ENEMY SPD = BASE × (1 + 0.04(FLOOR−1) + 0.01·POWER) × PRESSURE (±10%)',
|
||||
'CONTACT DAMAGE NEVER SCALES — ONLY ELITES GET +1',
|
||||
] },
|
||||
], HELP_PANEL.w - 80);
|
||||
ctx.fillStyle = '#d7a934'; ctx.font = 'bold 9px monospace';
|
||||
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';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHelpPressure(ctx, px, py) {
|
||||
helpLines(ctx, px, py + 116, [
|
||||
{ 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',
|
||||
' 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',
|
||||
'LIVING ENEMIES KEEP THEIR STATS — ONLY NEW SPAWNS CHANGE',
|
||||
'STREAK = CONSECUTIVE FLAWLESS CLEARS (COSMETIC)',
|
||||
] },
|
||||
{ head: 'DEFENSE MATH', lines: [
|
||||
'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',
|
||||
'CRIT: 5% BASE CHANCE, ×2 DAMAGE (CRITMUL STACKS)',
|
||||
] },
|
||||
], HELP_PANEL.w - 80);
|
||||
}
|
||||
|
||||
const HELP_RENDERERS = [drawHelpControls, drawHelpLoop, drawHelpArsenal, drawHelpPerkList, drawHelpItemList, drawHelpBestiary, drawHelpPressure];
|
||||
|
||||
function drawPauseHelp(ctx) {
|
||||
ctx.fillStyle = 'rgba(4,2,3,0.84)';
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
const px = helpPanelX(), py = HELP_PANEL.y, pw = HELP_PANEL.w, ph = HELP_PANEL.h;
|
||||
drawPixelPanel(ctx, px, py, pw, ph, {
|
||||
accent: '#4a9cad', border: 'rgba(74,91,110,0.95)', fill: 'rgba(8,6,10,0.97)',
|
||||
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);
|
||||
|
||||
const cr = helpCloseRect();
|
||||
drawPixelTag(ctx, '[X]', cr.x, cr.y, { width: cr.w, height: cr.h, color: '#e8bf47', accent: '#9d7726' });
|
||||
for (let i = 0; i < HELP_PAGES.length; i++) {
|
||||
const r = helpTabRect(i);
|
||||
const active = i === G.helpPage;
|
||||
const hover = inRect(Input.mx, Input.my, r);
|
||||
drawPixelTag(ctx, HELP_PAGES[i].title, r.x, r.y, {
|
||||
width: r.w, height: r.h,
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
HELP_RENDERERS[G.helpPage](ctx, px, py);
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
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);
|
||||
}
|
||||
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',
|
||||
});
|
||||
}
|
||||
+13
-1
@@ -66,6 +66,7 @@ function startRun() {
|
||||
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();
|
||||
@@ -131,7 +132,11 @@ function update(dt) {
|
||||
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(); }
|
||||
@@ -202,7 +207,7 @@ function draw() {
|
||||
}
|
||||
|
||||
if (G.mode === 'levelup') drawLevelup(ctx);
|
||||
else if (G.mode === 'pause') drawPause(ctx);
|
||||
else if (G.mode === 'pause') { drawPause(ctx); if (G.pauseHelp) drawPauseHelp(ctx); }
|
||||
else if (G.mode === 'gameover') drawGameOver(ctx);
|
||||
}
|
||||
|
||||
@@ -328,6 +333,13 @@ function drawPause(ctx) {
|
||||
ctx.fillStyle = '#69585a'; ctx.font = '9px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('ARROW KEYS / CLICK TO CHANGE TRACK · LIVE BOSS FIGHTS OVERRIDE SELECTION', W / 2, 503);
|
||||
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -115,7 +115,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=33')
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=36')
|
||||
.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)))
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ const Sprites = {
|
||||
const img = new Image();
|
||||
img.onload = () => { this.imgs[name] = img; if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.onerror = () => { if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.src = 'assets/' + name + '.png?v=35';
|
||||
img.src = 'assets/' + name + '.png?v=36';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ const G = {
|
||||
recentHits: [],
|
||||
transition: 0, // room transition fade
|
||||
muted: false,
|
||||
pauseHelp: false,
|
||||
helpPage: 0,
|
||||
sfxVol: 0.45,
|
||||
musicVol: 0.55,
|
||||
imagesLoaded: false,
|
||||
|
||||
+19
-1
@@ -54,7 +54,7 @@ vm.createContext(sandbox);
|
||||
|
||||
// load game scripts in index.html order
|
||||
const files = ['utils', 'state', 'input', 'sfx', 'sfxbank', 'audio', 'sprites', 'particles', 'weapons',
|
||||
'bullets', 'pickups', 'items', 'perks', 'enemies', 'bosses', 'rooms', 'player', 'hud', 'main'];
|
||||
'bullets', 'pickups', 'items', 'perks', 'enemies', 'bosses', 'rooms', 'player', 'hud', 'help', 'main'];
|
||||
for (const f of files) {
|
||||
const code = fs.readFileSync(path.join(__dirname, '..', 'js', f + '.js'), 'utf8');
|
||||
vm.runInContext(code, sandbox, { filename: f + '.js' });
|
||||
@@ -1116,6 +1116,24 @@ check('paused weapon swap stays silent', ctx.G.toasts[ctx.G.toasts.length - 1] =
|
||||
tap('r');
|
||||
tap('p');
|
||||
check('unpaused', ctx.G.mode === 'play');
|
||||
|
||||
console.log('== pause help screen ==');
|
||||
tap('p');
|
||||
check('paused for help', ctx.G.mode === 'pause');
|
||||
tap('h');
|
||||
check('H opens the field manual', ctx.G.pauseHelp === true);
|
||||
step(3, 16);
|
||||
tap('arrowright');
|
||||
check('arrow advances manual page', ctx.G.helpPage === 1);
|
||||
tap('4');
|
||||
check('number keys jump to a manual page', ctx.G.helpPage === 3);
|
||||
tap('escape');
|
||||
check('escape closes manual but stays paused', ctx.G.pauseHelp === false && ctx.G.mode === 'pause');
|
||||
tap('h'); tap('h');
|
||||
check('H toggles the manual closed', ctx.G.pauseHelp === false);
|
||||
tap('p');
|
||||
check('unpaused after manual', ctx.G.mode === 'play');
|
||||
|
||||
tap('m');
|
||||
check('muted', ctx.G.muted === true);
|
||||
|
||||
|
||||
Reference in new issue
Block a user