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:
1 parent
d255be5e39
commit
a02cff52cf
24 files changed
+667
-201
No files matched your search
+33
-2
@@ -5,10 +5,11 @@ const ENEMY_TYPES = {
|
||||
runner: { hp: 12, spd: 236, dmg: 1, r: 15, drawSize: 64, xp: 1, sprite: 'enemy_runner' },
|
||||
spitter: { hp: 20, spd: 60, dmg: 1, r: 18, drawSize: 64, xp: 2, sprite: 'enemy_spitter', ranged: true },
|
||||
splitter: { hp: 30, spd: 70, dmg: 1, r: 21, drawSize: 64, xp: 1, sprite: 'enemy_splitter', splits: true },
|
||||
mini: { hp: 6, spd: 120, dmg: 1, r: 10, drawSize: 32, xp: 1, sprite: 'enemy_mini' },
|
||||
mini: { hp: 6, spd: 120, dmg: 1, r: 14, drawSize: 48, xp: 1, sprite: 'enemy_mini' },
|
||||
exploder: { hp: 14, spd: 110, dmg: 2, r: 16, drawSize: 64, xp: 2, sprite: 'enemy_exploder', explodes: true },
|
||||
};
|
||||
const MAX_ENEMIES = 72;
|
||||
const SPAWN_WARN = 0.5; // seconds a wave-spawned enemy materializes, frozen + harmless
|
||||
|
||||
function makeEnemy(type, x, y, floor, elite) {
|
||||
const t = ENEMY_TYPES[type];
|
||||
@@ -32,6 +33,7 @@ function makeEnemy(type, x, y, floor, elite) {
|
||||
fireT: rand(1, 2.5), fuse: -1, wob: rand(0, TAU),
|
||||
animSeed: rand(0, TAU), animT: rand(0, 1), attackT: 0, actionT: 0,
|
||||
spawnT: 0, dead: false,
|
||||
warmT: 0,
|
||||
boss: false,
|
||||
};
|
||||
return e;
|
||||
@@ -83,6 +85,7 @@ function spawnWave(count, floor) {
|
||||
const pos = spawnPosAwayFromPlayer();
|
||||
const elite = chance(Math.min(0.03 + floor * 0.02, 0.2));
|
||||
const e = makeEnemy(pickEnemyType(floor), pos.x, pos.y, floor, elite);
|
||||
e.warmT = SPAWN_WARN; // telegraph the incoming spawn so the player isn't blindsided
|
||||
G.enemies.push(e);
|
||||
spawnBlood(pos.x, pos.y, rand(0, TAU), 4);
|
||||
Sfx.spawn(e);
|
||||
@@ -269,7 +272,7 @@ function killEnemy(e, ang) {
|
||||
if (ENEMY_TYPES[e.type].splits && !e.elite) {
|
||||
Sfx.split(e);
|
||||
for (let i = 0; i < 2 && G.enemies.length < MAX_ENEMIES; i++) {
|
||||
const m = makeEnemy('mini', e.x + rand(-12, 12), e.y + rand(-12, 12), G.floor, false);
|
||||
const m = makeEnemy('mini', e.x + rand(-18, 18), e.y + rand(-18, 18), G.floor, false);
|
||||
G.enemies.push(m);
|
||||
}
|
||||
}
|
||||
@@ -297,6 +300,13 @@ function updateEnemies(dt) {
|
||||
e.animT = (e.animT || 0) + dt;
|
||||
if (e.attackT > 0) { e.attackT -= dt; e.actionT += dt; }
|
||||
|
||||
// materializing spawns are frozen and harmless until the telegraph ends
|
||||
if (e.warmT > 0) {
|
||||
e.warmT -= dt;
|
||||
e.vx = 0; e.vy = 0;
|
||||
if (e.warmT > 0) continue;
|
||||
}
|
||||
|
||||
// status effects
|
||||
if (e.flash > 0) e.flash -= dt;
|
||||
if (e.orbT > 0) e.orbT -= dt;
|
||||
@@ -393,6 +403,27 @@ function updateEnemyAI(e, dt) {
|
||||
function drawEnemies(ctx) {
|
||||
for (const e of G.enemies) {
|
||||
if (e.hp <= 0) continue;
|
||||
// spawn telegraph: pulsing sigil + ghost, harmless until warmT expires
|
||||
if (e.warmT > 0) {
|
||||
const k = clamp(1 - e.warmT / SPAWN_WARN, 0, 1);
|
||||
const ringR = e.r + 30 * (1 - k);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.35 + Math.sin(G.time * 26) * 0.2;
|
||||
ctx.strokeStyle = '#ff2a3c'; ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.arc(e.x, e.y, ringR, 0, TAU); ctx.stroke();
|
||||
ctx.globalAlpha = 0.22 + k * 0.30;
|
||||
ctx.fillStyle = '#4a0713';
|
||||
ctx.beginPath(); ctx.arc(e.x, e.y, e.r * (0.3 + k * 0.7), 0, TAU); ctx.fill();
|
||||
ctx.restore();
|
||||
Sprites.actor(ctx, e.sprite, e.x, e.y, 0, 'idle', e.animT || 0,
|
||||
e.drawSize || (e.boss ? 128 : 64), 0.18 + k * 0.55, 0.45 + k * 0.55, 0.45 + k * 0.55);
|
||||
if (e.elite) {
|
||||
ctx.strokeStyle = '#ffd060'; ctx.lineWidth = 2; ctx.globalAlpha = 0.7;
|
||||
ctx.beginPath(); ctx.arc(e.x, e.y, e.r + 3, 0, TAU); ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const p = G.player;
|
||||
const face = angleTo(e.x, e.y, p.x, p.y);
|
||||
const speed = Math.min(Math.hypot(e.vx, e.vy) / Math.max(e.spd, 1), 1);
|
||||
|
||||
+106
-80
@@ -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);
|
||||
}
|
||||
@@ -3,7 +3,12 @@
|
||||
function drawHUD(ctx) {
|
||||
const p = G.player;
|
||||
ctx.save();
|
||||
const panelH = p.holstered ? 140 : 112;
|
||||
const A = clamp(G.hudAlpha === undefined ? 1 : G.hudAlpha, 0.25, 1);
|
||||
ctx.globalAlpha = A;
|
||||
// vitals use a 2-row grid; a second row pushes the rest of the panel down
|
||||
const totalVitalIcons = Math.ceil(p.stats.maxHp / 2) + Math.ceil(p.shieldHp / 2);
|
||||
const vOff = totalVitalIcons > 12 ? 22 : 0;
|
||||
const panelH = (p.holstered ? 140 : 112) + vOff;
|
||||
drawPixelPanel(ctx, 8, 8, 304, panelH, {
|
||||
accent: '#cf2942', border: 'rgba(115,43,56,0.95)', blood: true,
|
||||
seed: G.floor, bloodAlpha: 0.34,
|
||||
@@ -14,14 +19,17 @@ function drawHUD(ctx) {
|
||||
ctx.fillStyle = '#d7a934'; ctx.textAlign = 'right';
|
||||
ctx.fillText('LV ' + String(p.level).padStart(2, '0'), 300, 23);
|
||||
|
||||
// Hearts retain their illustrative icons on a cleaner fixed baseline.
|
||||
const hearts = Math.ceil(p.stats.maxHp / 2);
|
||||
const maxVitalIcons = 11;
|
||||
const shownHearts = Math.min(hearts, maxVitalIcons);
|
||||
for (let i = 0; i < shownHearts; i++) {
|
||||
const x = 21 + i * 22, y = 39;
|
||||
// hearts: full containers = 2 HP, trailing odd maxHp draws as a half outline
|
||||
const fullHearts = Math.floor(p.stats.maxHp / 2);
|
||||
const halfContainer = p.stats.maxHp % 2 === 1;
|
||||
const maxVitalIcons = 24; // 2 rows x 12
|
||||
const shownFull = Math.min(fullHearts, maxVitalIcons);
|
||||
let slot = 0;
|
||||
for (let i = 0; i < shownFull; i++) {
|
||||
const x = 21 + (i % 12) * 22, y = 39 + Math.floor(i / 12) * 22;
|
||||
slot = i + 1;
|
||||
const fill = clamp(p.hp - i * 2, 0, 2) / 2;
|
||||
Sprites.draw(ctx, 'heart', x, y, 0, 18, false, 0.25);
|
||||
Sprites.draw(ctx, 'heart', x, y, 0, 18, false, 0.25 * A);
|
||||
if (fill > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath(); ctx.rect(x - 10, y - 10, 20 * fill, 20); ctx.clip();
|
||||
@@ -29,35 +37,63 @@ function drawHUD(ctx) {
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
const shownShields = Math.min(p.shieldHp, Math.max(0, maxVitalIcons - shownHearts));
|
||||
for (let i = 0; i < shownShields; i++) {
|
||||
const x = 21 + (shownHearts + i) * 22, y = 39;
|
||||
// half container outline (only when there's icon budget left)
|
||||
if (halfContainer && slot < maxVitalIcons) {
|
||||
const x = 21 + (slot % 12) * 22, y = 39 + Math.floor(slot / 12) * 22;
|
||||
ctx.save();
|
||||
ctx.beginPath(); ctx.rect(x - 10, y - 10, 10, 20); ctx.clip();
|
||||
Sprites.draw(ctx, 'heart', x, y, 0, 18, false, 0.25 * A);
|
||||
ctx.restore();
|
||||
const fill = clamp(p.hp - slot * 2, 0, 1);
|
||||
if (fill > 0) {
|
||||
ctx.save();
|
||||
ctx.beginPath(); ctx.rect(x - 10, y - 10, 10, 20); ctx.clip();
|
||||
Sprites.draw(ctx, 'heart', x, y, 0, 18);
|
||||
ctx.restore();
|
||||
}
|
||||
slot++;
|
||||
}
|
||||
// shield pips: 2 shieldHp = one full cyan heart, odd = half (matches hearts)
|
||||
const shieldFull = Math.floor(p.shieldHp / 2), shieldHalf = p.shieldHp % 2 === 1;
|
||||
const shieldIcons = shieldFull + (shieldHalf ? 1 : 0);
|
||||
const shownShieldIcons = Math.min(shieldIcons, Math.max(0, maxVitalIcons - slot));
|
||||
for (let i = 0; i < shownShieldIcons; i++) {
|
||||
const x = 21 + ((slot + i) % 12) * 22, y = 39 + Math.floor((slot + i) / 12) * 22;
|
||||
const isHalf = shieldHalf && i === shownShieldIcons - 1;
|
||||
ctx.fillStyle = '#3bc9e0';
|
||||
ctx.beginPath(); ctx.arc(x - 3, y - 2, 4, 0, TAU); ctx.arc(x + 3, y - 2, 4, 0, TAU); ctx.fill();
|
||||
ctx.beginPath(); ctx.moveTo(x - 6.5, y); ctx.lineTo(x, y + 7); ctx.lineTo(x + 6.5, y); ctx.fill();
|
||||
if (isHalf) {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.beginPath(); ctx.arc(x + 3, y - 2, 4.5, 0, TAU); ctx.fill();
|
||||
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + 7, y); ctx.lineTo(x, y + 8); ctx.closePath(); ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
const hiddenVitals = hearts + p.shieldHp - shownHearts - shownShields;
|
||||
slot += shownShieldIcons;
|
||||
const hiddenVitals = Math.ceil(p.stats.maxHp / 2) + shieldIcons - slot;
|
||||
if (hiddenVitals > 0) {
|
||||
drawPixelTag(ctx, '+' + hiddenVitals, 266, 29, { width: 34, height: 21, color: '#d7b24a', accent: '#8f6724' });
|
||||
}
|
||||
|
||||
const need = xpForLevel(p.level);
|
||||
drawPixelBar(ctx, 16, 53, 220, 10, p.xp / need, {
|
||||
drawPixelBar(ctx, 16, 53 + vOff, 220, 10, p.xp / need, {
|
||||
fill: '#3d9fc2', glint: '#a5edee', border: '#465d69', segments: 12,
|
||||
});
|
||||
ctx.fillStyle = '#88b9c4'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'right';
|
||||
ctx.fillText(Math.floor(p.xp) + ' / ' + need + ' XP', 299, 62);
|
||||
ctx.fillText(Math.floor(p.xp) + ' / ' + need + ' XP', 299, 62 + vOff);
|
||||
|
||||
const w = WEAPONS[p.weapon.id];
|
||||
ctx.fillStyle = 'rgba(3,2,3,0.6)'; ctx.fillRect(15, 70, 48, 34);
|
||||
ctx.strokeStyle = 'rgba(116,56,65,0.65)'; ctx.strokeRect(15.5, 70.5, 47, 33);
|
||||
Sprites.draw(ctx, 'w_' + w.id, 39, 87, 0, 54);
|
||||
ctx.fillStyle = 'rgba(3,2,3,0.6)'; ctx.fillRect(15, 70 + vOff, 48, 34);
|
||||
ctx.strokeStyle = 'rgba(116,56,65,0.65)'; ctx.strokeRect(15.5, 70.5 + vOff, 47, 33);
|
||||
Sprites.draw(ctx, 'w_' + w.id, 39, 87 + vOff, 0, 54);
|
||||
ctx.textAlign = 'left'; ctx.fillStyle = '#f0e5d7'; ctx.font = 'bold 12px monospace';
|
||||
ctx.fillText(w.name.toUpperCase(), 72, 84);
|
||||
ctx.fillText(w.name.toUpperCase(), 72, 84 + vOff);
|
||||
ctx.fillStyle = '#7d6262'; ctx.font = '9px monospace';
|
||||
ctx.fillText('ACTIVE WEAPON', 72, 99);
|
||||
ctx.fillText('ACTIVE WEAPON', 72, 99 + vOff);
|
||||
const ammoText = p.weapon.ammo === Infinity ? 'SIDEARM' : String(Math.ceil(p.weapon.ammo)).padStart(3, '0');
|
||||
drawPixelTag(ctx, ammoText, 238, 74, {
|
||||
drawPixelTag(ctx, ammoText, 238, 74 + vOff, {
|
||||
width: 58, height: 26,
|
||||
color: p.weapon.ammo === Infinity ? '#9c8d86' : (p.weapon.ammo < w.ammo * 0.25 ? '#ff4057' : '#e0b94e'),
|
||||
accent: p.weapon.ammo === Infinity ? '#5d4c50' : '#a47e25', font: 'bold 10px monospace',
|
||||
@@ -65,12 +101,12 @@ function drawHUD(ctx) {
|
||||
|
||||
if (p.holstered) {
|
||||
const holsterEmpty = p.holstered.ammo <= 0;
|
||||
ctx.fillStyle = 'rgba(86,40,48,0.35)'; ctx.fillRect(15, 112, 281, 20);
|
||||
Sprites.draw(ctx, 'w_' + p.holstered.id, 29, 122, 0, 28, false, 0.68);
|
||||
ctx.fillStyle = 'rgba(86,40,48,0.35)'; ctx.fillRect(15, 112 + vOff, 281, 20);
|
||||
Sprites.draw(ctx, 'w_' + p.holstered.id, 29, 122 + vOff, 0, 28, false, 0.68 * A);
|
||||
ctx.fillStyle = holsterEmpty ? '#d94652' : '#aa9290'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'left';
|
||||
ctx.fillText((holsterEmpty ? '[EMPTY] ' : '[R] ') + WEAPONS[p.holstered.id].name.toUpperCase(), 47, 125);
|
||||
ctx.fillText((holsterEmpty ? '[EMPTY] ' : '[R] ') + WEAPONS[p.holstered.id].name.toUpperCase(), 47, 125 + vOff);
|
||||
ctx.fillStyle = holsterEmpty ? '#ef4057' : '#d0aa4a'; ctx.textAlign = 'right';
|
||||
ctx.fillText(holsterEmpty ? 'NEEDS AMMO' : Math.ceil(p.holstered.ammo) + ' AMMO', 289, 125);
|
||||
ctx.fillText(holsterEmpty ? 'NEEDS AMMO' : Math.ceil(p.holstered.ammo) + ' AMMO', 289, 125 + vOff);
|
||||
}
|
||||
|
||||
const allItemEntries = Object.entries(p.items);
|
||||
@@ -88,7 +124,7 @@ function drawHUD(ctx) {
|
||||
const [iid, lvl] = itemEntries[index];
|
||||
const ix = 28 + (index % 12) * 34;
|
||||
const iy = trayY + 31 + Math.floor(index / 12) * 34;
|
||||
Sprites.draw(ctx, 'i_' + iid, ix, iy, 0, 30, false, 0.95);
|
||||
Sprites.draw(ctx, 'i_' + iid, ix, iy, 0, 30, false, 0.95 * A);
|
||||
if (lvl > 1) {
|
||||
ctx.fillStyle = '#ffd060'; ctx.font = 'bold 8px monospace';
|
||||
ctx.fillText(romanNum(lvl), ix - 7, iy + 11);
|
||||
@@ -112,14 +148,19 @@ function drawHUD(ctx) {
|
||||
ctx.fillText('KILLS ' + String(G.kills).padStart(3, '0'), W - 18, H - 67);
|
||||
ctx.fillStyle = '#f0e5d7'; ctx.font = 'bold 11px monospace';
|
||||
ctx.fillText('SCORE ' + String(G.score).padStart(6, '0'), W - 18, H - 46);
|
||||
ctx.textAlign = 'left'; ctx.fillStyle = G.pressure > 1 ? '#d6aa39' : '#69b8bd'; ctx.font = 'bold 8px monospace';
|
||||
ctx.fillText('PRESSURE ' + Math.round(G.pressure * 100) + '%', W - 168, H - 28);
|
||||
ctx.fillStyle = '#8f7770'; ctx.font = '8px monospace';
|
||||
ctx.fillText('×' + (G.pressure || 1).toFixed(2), W - 168, H - 46);
|
||||
ctx.textAlign = 'left';
|
||||
const dialTxt = ' D' + (G.pressureDial > 0 ? '+' : '') + G.pressureDial;
|
||||
ctx.fillStyle = G.pressure > 1 ? '#d6aa39' : '#69b8bd'; ctx.font = 'bold 8px monospace';
|
||||
ctx.fillText('PRESSURE ' + Math.round(G.pressure * 100) + '%' + dialTxt, W - 168, H - 28);
|
||||
ctx.textAlign = 'right'; ctx.fillText('STREAK ' + G.streak, W - 18, H - 28);
|
||||
drawPixelBar(ctx, W - 168, H - 20, 150, 8, (G.pressure - PRESSURE_MIN) / (PRESSURE_MAX - PRESSURE_MIN), {
|
||||
fill: G.pressure > 1 ? '#c59231' : '#3d8f99', glint: G.pressure > 1 ? '#f4d86d' : '#8ce0df', border: '#5d3c43', segments: 10,
|
||||
});
|
||||
|
||||
if (G.boss && G.boss.hp > 0) {
|
||||
ctx.save(); ctx.globalAlpha = 1; // boss bar stays fully opaque
|
||||
const bw = 460, bx = W / 2 - bw / 2, by = H - 48;
|
||||
drawPixelPanel(ctx, bx, by, bw, 40, {
|
||||
accent: '#e12842', cut: 6, fill: 'rgba(12,3,6,0.92)', blood: true,
|
||||
@@ -133,6 +174,7 @@ function drawHUD(ctx) {
|
||||
drawPixelBar(ctx, bx + 11, by + 22, bw - 22, 10, k, {
|
||||
fill: '#b51e35', glint: '#ff7d82', border: '#6e2936', segments: 20,
|
||||
});
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
@@ -152,7 +194,7 @@ function drawHUD(ctx) {
|
||||
ctx.fillStyle = '#9f8981'; ctx.font = '9px monospace';
|
||||
ctx.fillText(t.sub.toUpperCase(), W / 2, y + 31);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalAlpha = A;
|
||||
}
|
||||
|
||||
if (G.muted) {
|
||||
|
||||
+4
-3
@@ -49,12 +49,13 @@ const ITEMS = {
|
||||
rerollrib: { name: 'Reroll Rib', desc: '+1 perk reroll each level', apply: s => { s.rerollPerLevel += 1; s.rerolls += 1; } },
|
||||
};
|
||||
|
||||
// 50% of rolls favor items the player already owns (below cap) so high tiers
|
||||
// are reachable if you commit to them
|
||||
// Stickiness ramps with commitment: 10% per distinct upgradable item owned,
|
||||
// capped at 50%. Your first pickup barely biases the pool, so early drops vary.
|
||||
function randomItemId() {
|
||||
const p = G.player;
|
||||
const owned = p ? Object.keys(p.items).filter(iid => (p.items[iid] || 0) < ITEM_LEVEL_CAP) : [];
|
||||
if (owned.length && chance(0.5)) return choice(owned);
|
||||
const stick = Math.min(0.5, owned.length * 0.10);
|
||||
if (owned.length && chance(stick)) return choice(owned);
|
||||
return choice(Object.keys(ITEMS));
|
||||
}
|
||||
|
||||
|
||||
+117
-26
@@ -2,6 +2,17 @@
|
||||
|
||||
let canvas, ctx, lastT = 0;
|
||||
|
||||
const PRESSURE_SLIDER_RECT = { x: 300, y: 470, w: 360, h: 46 };
|
||||
|
||||
function pauseBars() {
|
||||
const x = W / 2 - 55, w = 225;
|
||||
return [
|
||||
{ rect: { x, y: 418, w, h: 10 }, set: v => Sfx.setVolume(v) },
|
||||
{ rect: { x, y: 446, w, h: 10 }, set: v => Music.setVolume(v) },
|
||||
{ rect: { x, y: 474, w, h: 10 }, set: v => setHudAlpha(0.25 + v * 0.75) },
|
||||
];
|
||||
}
|
||||
|
||||
function init() {
|
||||
canvas = document.getElementById('game');
|
||||
ctx = canvas.getContext('2d');
|
||||
@@ -25,10 +36,25 @@ 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';
|
||||
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'));
|
||||
if (Number.isFinite(ha)) G.hudAlpha = clamp(ha, 0.25, 1);
|
||||
} catch (e) {}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
function setPressureDial(n) {
|
||||
G.pressureDial = clamp(Math.round(n), PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX);
|
||||
try { localStorage.setItem('meatslicer_pressure_dial', String(G.pressureDial)); } catch (e) {}
|
||||
Sfx.menu();
|
||||
}
|
||||
|
||||
function setHudAlpha(v) {
|
||||
G.hudAlpha = clamp(Math.round(v * 100) / 100, 0.25, 1);
|
||||
try { localStorage.setItem('meatslicer_hud_alpha', String(G.hudAlpha)); } catch (e) {}
|
||||
}
|
||||
|
||||
function setAutoPerk(enabled) {
|
||||
G.autoPerk = !!enabled;
|
||||
try { localStorage.setItem('meatslicer_autoperk', G.autoPerk ? '1' : '0'); } catch (e) {}
|
||||
@@ -108,9 +134,18 @@ function update(dt) {
|
||||
}
|
||||
|
||||
switch (G.mode) {
|
||||
case 'menu':
|
||||
if (Input.mpressed || keyPressed('enter', ' ')) startRun();
|
||||
case 'menu': {
|
||||
const sl = PRESSURE_SLIDER_RECT;
|
||||
if (keyPressed('arrowleft', 'a', '-', '_')) setPressureDial(G.pressureDial - 1);
|
||||
if (keyPressed('arrowright', 'd', '=', '+')) setPressureDial(G.pressureDial + 1);
|
||||
if (Input.mpressed && inRect(Input.mx, Input.my, sl)) G.menuDialDrag = true;
|
||||
if (G.menuDialDrag && Input.mdown) {
|
||||
setPressureDial(Math.round(((Input.mx - sl.x) / sl.w) * 10) - 5);
|
||||
}
|
||||
if (!Input.mdown) G.menuDialDrag = false;
|
||||
if (keyPressed('enter', ' ') || (Input.mpressed && !inRect(Input.mx, Input.my, sl))) startRun();
|
||||
break;
|
||||
}
|
||||
case 'play':
|
||||
G.time += dt;
|
||||
if (keyPressed('p', 'escape')) { G.mode = 'pause'; Sfx.stopAllLoops(); Sfx.menu(); break; }
|
||||
@@ -143,6 +178,21 @@ function update(dt) {
|
||||
if (keyPressed('=', '+')) { Sfx.setVolume(G.sfxVol + 0.1); Sfx.menu(); }
|
||||
if (keyPressed(',', '<')) { Music.setVolume(G.musicVol - 0.1); Sfx.menu(); }
|
||||
if (keyPressed('.', '>')) { Music.setVolume(G.musicVol + 0.1); Sfx.menu(); }
|
||||
if (keyPressed(';', ':')) { setHudAlpha(G.hudAlpha - 0.1); Sfx.menu(); }
|
||||
if (keyPressed("'", '"')) { setHudAlpha(G.hudAlpha + 0.1); Sfx.menu(); }
|
||||
// click-and-drag on the three pause sliders
|
||||
{
|
||||
const bars = pauseBars();
|
||||
if (Input.mpressed) {
|
||||
for (let i = 0; i < bars.length; i++) if (inRect(Input.mx, Input.my, bars[i].rect)) { G.pauseDrag = i; break; }
|
||||
}
|
||||
if (G.pauseDrag !== null && G.pauseDrag !== undefined && Input.mdown) {
|
||||
const b = bars[G.pauseDrag];
|
||||
const v = clamp((Input.mx - b.rect.x) / b.rect.w, 0, 1);
|
||||
b.set(v);
|
||||
}
|
||||
if (!Input.mdown) G.pauseDrag = null;
|
||||
}
|
||||
if (keyPressed('arrowleft', '[')) Music.cycle(-1);
|
||||
else if (keyPressed('arrowright', ']')) Music.cycle(1);
|
||||
else if (Input.mpressed) {
|
||||
@@ -272,41 +322,74 @@ function drawMenu(ctx) {
|
||||
tx += widths[i] + 6;
|
||||
}
|
||||
|
||||
// ---- pressure dial ----
|
||||
drawPressureDial(ctx);
|
||||
|
||||
ctx.fillStyle = '#655155'; ctx.font = '9px monospace';
|
||||
ctx.fillText('ROOM-CLEAR COMBAT // PERK DRAFTS // SIXTEEN WEAPONS // CROWNED MEAT', W / 2, 474);
|
||||
ctx.fillText('ROOM-CLEAR COMBAT // PERK DRAFTS // SIXTEEN WEAPONS // CROWNED MEAT', W / 2, 530);
|
||||
if (G.best) {
|
||||
drawPixelTag(ctx, 'BEST CUT ' + String(G.best).padStart(6, '0'), W / 2 - 74, 503, {
|
||||
drawPixelTag(ctx, 'BEST CUT ' + String(G.best).padStart(6, '0'), W / 2 - 74, 556, {
|
||||
width: 148, height: 26, color: '#e0b94e', accent: '#a47e25',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function drawPressureDial(ctx) {
|
||||
const sl = PRESSURE_SLIDER_RECT;
|
||||
const d = G.pressureDial;
|
||||
const gain = pressureGainUnits(d), drop = pressureDropUnits(d);
|
||||
const accent = d < 0 ? '#3d8f99' : (d === 0 ? '#c59231' : '#e22b46');
|
||||
drawPixelPanel(ctx, sl.x, sl.y, sl.w, sl.h, {
|
||||
cut: 6, shadow: false, accent, fill: 'rgba(12,5,8,0.92)', border: '#63303a',
|
||||
});
|
||||
const name = d <= -4 ? 'MERCIFUL' : d <= -2 ? 'EASED' : d <= 0 ? 'STANDARD' : d <= 2 ? 'TENSE' : d <= 4 ? 'SAVAGE' : 'RELENTLESS';
|
||||
ctx.textAlign = 'left'; ctx.fillStyle = '#9e857a'; ctx.font = 'bold 8px monospace';
|
||||
ctx.fillText('PRESSURE DIAL', sl.x + 10, sl.y + 12);
|
||||
ctx.textAlign = 'right'; ctx.fillStyle = accent; ctx.font = 'bold 9px monospace';
|
||||
ctx.fillText((d > 0 ? '+' : '') + d + ' ' + name, sl.x + sl.w - 10, sl.y + 12);
|
||||
// track with 10 steps / 11 notches
|
||||
const bx = sl.x + 34, bw = sl.w - 68, by = sl.y + 20, bh = 10;
|
||||
drawPixelBar(ctx, bx, by, bw, bh, (d + 5) / 10, {
|
||||
fill: accent, glint: '#f4d86d', border: '#5d3c43', segments: 10,
|
||||
});
|
||||
// bright notch marker
|
||||
const nx = bx + Math.round((d + 5) / 10 * bw);
|
||||
ctx.fillStyle = '#f5e9d6';
|
||||
ctx.fillRect(nx - 1, by - 2, 2, bh + 4);
|
||||
// end labels
|
||||
ctx.fillStyle = '#7d6262'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText('-5', bx - 16, by + 9); ctx.fillText('+5', bx + bw + 16, by + 9);
|
||||
// derived readout
|
||||
ctx.fillStyle = '#8f7770'; ctx.font = '8px monospace';
|
||||
ctx.fillText('RISE +' + gain.toFixed(1) + '/5 · RELIEF ' + drop.toFixed(1) + '/5 · SCORE ×LIVE PRESSURE', sl.x + sl.w / 2, sl.y + sl.h - 7);
|
||||
}
|
||||
|
||||
function drawPause(ctx) {
|
||||
ctx.fillStyle = 'rgba(4,2,3,0.78)'; ctx.fillRect(0, 0, W, H);
|
||||
drawPixelPanel(ctx, W / 2 - 270, 126, 540, 388, {
|
||||
drawPixelPanel(ctx, W / 2 - 270, 104, 540, 452, {
|
||||
accent: '#c92a40', border: 'rgba(119,51,61,0.95)', fill: 'rgba(11,5,8,0.96)',
|
||||
blood: true, seed: 8, bloodAlpha: 0.25,
|
||||
});
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#7f6768'; ctx.font = 'bold 9px monospace';
|
||||
ctx.fillText('RUN STATE // SUSPENDED', W / 2, 158);
|
||||
ctx.fillText('RUN STATE // SUSPENDED', W / 2, 136);
|
||||
ctx.fillStyle = '#f0e5d7'; ctx.font = 'bold 36px monospace';
|
||||
ctx.fillText('PAUSED', W / 2, 202);
|
||||
drawBloodTrim(ctx, W / 2 - 130, 213, 260, 8, 0.45);
|
||||
ctx.fillText('PAUSED', W / 2, 180);
|
||||
drawBloodTrim(ctx, W / 2 - 130, 191, 260, 8, 0.45);
|
||||
|
||||
ctx.fillStyle = '#b89532'; ctx.font = 'bold 10px monospace';
|
||||
ctx.fillText('JUKEBOX // ACTIVE TRACK', W / 2, 270);
|
||||
ctx.fillText('JUKEBOX // ACTIVE TRACK', W / 2, 248);
|
||||
const sel = (G.musicSel === undefined || G.musicSel < 0) ? 'AUTO · FLOOR ROTATION' : Music.pretty(Music.PLAYLIST[G.musicSel]).toUpperCase();
|
||||
drawPixelPanel(ctx, W / 2 - 170, H / 2 + 20, 340, 82, {
|
||||
drawPixelPanel(ctx, W / 2 - 170, 318, 340, 82, {
|
||||
cut: 5, shadow: false, accent: '#468e9d', fill: 'rgba(5,10,12,0.7)', border: '#315b65',
|
||||
});
|
||||
ctx.fillStyle = '#8faeb2'; ctx.font = 'bold 9px monospace';
|
||||
ctx.fillText('NOW PLAYING', W / 2, H / 2 + 43);
|
||||
ctx.fillText('NOW PLAYING', W / 2, 341);
|
||||
ctx.fillStyle = '#e9ded0'; ctx.font = 'bold 14px monospace';
|
||||
ctx.fillText(sel, W / 2, H / 2 + 70);
|
||||
ctx.fillText(sel, W / 2, 368);
|
||||
|
||||
for (const sx of [-200, 200]) {
|
||||
const x = W / 2 + sx, y = H / 2 + 60;
|
||||
const x = W / 2 + sx, y = 358;
|
||||
const hover = dist(Input.mx, Input.my, x, y) < 22;
|
||||
drawPixelPanel(ctx, x - 20, y - 20, 40, 40, {
|
||||
cut: 5, shadow: false, fill: hover ? '#631526' : '#251017',
|
||||
@@ -316,23 +399,31 @@ function drawPause(ctx) {
|
||||
ctx.fillText(sx < 0 ? '<' : '>', x, y + 6);
|
||||
}
|
||||
|
||||
// three sliders: SFX / MUSIC / HUD (drag or key pairs)
|
||||
const sliders = [
|
||||
{ label: 'SFX [- / +]', val: G.sfxVol, colors: { fill: '#c92a40', glint: '#ff7d82', border: '#63303a', segments: 10 } },
|
||||
{ label: 'MUSIC [, / .]', val: G.musicVol, colors: { fill: '#468e9d', glint: '#a5edee', border: '#315b65', segments: 10 } },
|
||||
{ label: "HUD [; / ']", val: (G.hudAlpha - 0.25) / 0.75, colors: { fill: '#b5892f', glint: '#f4d86d', border: '#5d3c43', segments: 10 } },
|
||||
];
|
||||
const bars = pauseBars();
|
||||
ctx.textAlign = 'left'; ctx.font = 'bold 9px monospace'; ctx.fillStyle = '#a99a91';
|
||||
ctx.fillText('SFX [- / +]', W / 2 - 170, 435);
|
||||
drawPixelBar(ctx, W / 2 - 55, 426, 225, 10, G.sfxVol, {
|
||||
fill: '#c92a40', glint: '#ff7d82', border: '#63303a', segments: 10,
|
||||
});
|
||||
ctx.fillText('MUSIC [, / .]', W / 2 - 170, 445);
|
||||
drawPixelBar(ctx, W / 2 - 55, 436, 225, 10, G.musicVol, {
|
||||
fill: '#468e9d', glint: '#a5edee', border: '#315b65', segments: 10,
|
||||
});
|
||||
for (let i = 0; i < sliders.length; i++) {
|
||||
const by = bars[i].rect.y;
|
||||
ctx.fillStyle = '#a99a91'; ctx.fillText(sliders[i].label, W / 2 - 170, by + 9);
|
||||
drawPixelBar(ctx, bars[i].rect.x, by, bars[i].rect.w, 10, sliders[i].val, sliders[i].colors);
|
||||
ctx.fillStyle = '#e9ded0'; ctx.textAlign = 'right';
|
||||
const pct = i === 2 ? Math.round(G.hudAlpha * 100) : Math.round(sliders[i].val * 100);
|
||||
ctx.fillText(pct + '%', W / 2 + 185, by + 9);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
|
||||
drawPixelTag(ctx, '[P] RESUME', W / 2 - 250, 462, { width: 112, height: 28, color: '#f0e5d7', accent: '#b5243a' });
|
||||
drawPixelTag(ctx, '[R] SWAP', W / 2 - 125, 462, { width: 112, height: 28, color: '#bba9a1', accent: '#6e3843' });
|
||||
drawPixelTag(ctx, '[T] AUTO ' + (G.autoPerk ? 'ON' : 'OFF'), W / 2, 462, { width: 112, height: 28, color: G.autoPerk ? '#79d9ca' : '#bba9a1', accent: G.autoPerk ? '#378b80' : '#6e3843' });
|
||||
drawPixelTag(ctx, '[M] MUTE', W / 2 + 125, 462, { width: 112, height: 28, color: '#bba9a1', accent: '#6e3843' });
|
||||
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' });
|
||||
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);
|
||||
ctx.fillText('ARROWS / CLICK CHANGE TRACK · DRAG SLIDERS · LIVE BOSSES OVERRIDE TRACK', W / 2, 543);
|
||||
|
||||
const hb = HELP_BUTTON;
|
||||
const helpHover = inRect(Input.mx, Input.my, hb);
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ const PERKS = [
|
||||
{ id: 'heavyhand',name: 'Heavy Hand', desc: '+20% knockback', apply: s => { s.knockbackMul *= 1.20; } },
|
||||
{ id: 'thickhide',name: 'Thick Hide', desc: '+4% chance to ignore damage', apply: s => { s.armor += 1 / 24; } },
|
||||
{ id: 'secondwind',name: 'Second Wind', desc: '+0.15s hurt immunity', apply: s => { s.invBonus += 0.15; } },
|
||||
{ id: 'scrapfeed',name: 'Scrap Feed', desc: '5% less ammo consumed', apply: s => { s.ammoEff *= 1.05; } },
|
||||
{ id: 'scrapfeed',name: 'Scrap Feed', desc: '5% less ammo (diminishing)', apply: s => { s.ammoEff += 0.05; } },
|
||||
{ id: 'boneknit', name: 'Bone Knit', desc: '3% chance to heal ½ heart after combat rooms', apply: s => { s.roomHealChance += 0.03; } },
|
||||
{ id: 'spiteflesh',name: 'Spite Flesh', desc: 'Contact attackers take damage', apply: s => { s.thorns += 4; } },
|
||||
{ id: 'carrion', name: 'Carrion Sense', desc: '+8% pickup radius and pull speed', apply: s => { s.magnet *= 1.08; s.magnetPull *= 1.08; } },
|
||||
|
||||
+1
-1
@@ -177,7 +177,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 0.03 * severity * desperation * churn;
|
||||
return PRESSURE_DROP_BASE * PRESSURE_UNIT * severity * desperation * churn * pressureDropScale();
|
||||
}
|
||||
|
||||
function hurtPlayer(dmg, ang, attacker) {
|
||||
|
||||
+65
-5
@@ -116,7 +116,7 @@ function updateRoom(dt) {
|
||||
const stalled = G.time - G.roomEnterT > 90;
|
||||
if (lowHealth || stalled) {
|
||||
const decay = lowHealth ? 0.02 : 0.008;
|
||||
G.pressure = Math.max(PRESSURE_MIN, G.pressure - decay * dt);
|
||||
G.pressure = Math.max(PRESSURE_MIN, G.pressure - decay * dt * pressureDropScale());
|
||||
}
|
||||
}
|
||||
G.recentHits = (G.recentHits || []).filter(t => G.time - t < 12);
|
||||
@@ -171,7 +171,7 @@ function updateRoom(dt) {
|
||||
function recordRoomClear(room) {
|
||||
if (!G.roomDamaged) {
|
||||
G.streak++;
|
||||
G.pressure = Math.min(PRESSURE_MAX, G.pressure + PRESSURE_GAIN);
|
||||
G.pressure = Math.min(PRESSURE_MAX, G.pressure + pressureGain());
|
||||
} else {
|
||||
G.streak = 0;
|
||||
}
|
||||
@@ -363,13 +363,73 @@ function drawWalls(ctx, r) {
|
||||
else if (dir === 's') { x = W / 2; y = H - WALL / 2; rot = 0; }
|
||||
else if (dir === 'e') { x = W - WALL / 2; y = H / 2; rot = Math.PI / 2; }
|
||||
else { x = WALL / 2; y = H / 2; rot = Math.PI / 2; }
|
||||
Sprites.draw(ctx, sprite, x, y, rot, 96);
|
||||
// ---- doorway framing (visual only; DOOR_HALF governs function) ----
|
||||
const horiz = dir === 'n' || dir === 's';
|
||||
const dirSeed = { n: 0, s: 2.1, e: 4.2, w: 6.3 }[dir];
|
||||
// clear dark passage under the art (guaranteed clean opening)
|
||||
ctx.fillStyle = '#050103';
|
||||
if (horiz) ctx.fillRect(x - DOOR_HALF, y - WALL / 2, DOOR_HALF * 2, WALL);
|
||||
else ctx.fillRect(x - WALL / 2, y - DOOR_HALF, WALL, DOOR_HALF * 2);
|
||||
// door art (wide, walkable passage for open; sealed jaws for locked)
|
||||
Sprites.draw(ctx, sprite, x, y, rot, 150, false, 1, 1, 0.82);
|
||||
// accents on top, in wall-aligned local space
|
||||
ctx.save();
|
||||
if (horiz) ctx.translate(x, y); else { ctx.translate(x, y); ctx.rotate(Math.PI / 2); }
|
||||
// warm glow spilling from the threshold so the exit reads as inviting
|
||||
ctx.globalAlpha = 0.20 + Math.sin(G.time * 2.0 + dirSeed) * 0.06;
|
||||
ctx.fillStyle = locked ? '#ff2a3c' : '#e8724a';
|
||||
ctx.fillRect(-DOOR_HALF, WALL / 2 - 8, DOOR_HALF * 2, 8);
|
||||
// frame rim light on both jamb edges
|
||||
const rimAlpha = 0.30 + Math.sin(G.time * 2.2 + dirSeed) * 0.12;
|
||||
ctx.globalAlpha = rimAlpha;
|
||||
ctx.fillStyle = locked ? '#ff2a3c' : '#c4172a';
|
||||
ctx.fillRect(-DOOR_HALF - 2, -WALL / 2 - 2, 3, WALL + 4);
|
||||
ctx.fillRect(DOOR_HALF - 1, -WALL / 2 - 2, 3, WALL + 4);
|
||||
ctx.globalAlpha = 1;
|
||||
// jagged bone teeth ONLY when sealed (locked reads as dangerous, open reads as safe)
|
||||
if (locked) {
|
||||
for (let side = -1; side <= 1; side += 2) {
|
||||
for (let k = 0; k < 7; k++) {
|
||||
const hh = hashTile((r.gx * 3 + 17) * 31 + side * 7 + k, (r.gy * 5 + 11) * 13 + k * 3);
|
||||
const ty = -WALL / 2 + 3 + (hh % 41);
|
||||
const th = 4 + (hh % 3);
|
||||
const tw = 3 + (hh % 2);
|
||||
ctx.fillStyle = hh % 4 === 0 ? '#e8dcc2' : '#cfc0a4';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(side * DOOR_HALF, ty - tw);
|
||||
ctx.lineTo(side * DOOR_HALF, ty + tw);
|
||||
ctx.lineTo(side * (DOOR_HALF - th), ty);
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
// outward chevrons past the threshold so the exit reads from across the room
|
||||
const chevOff = dir === 'n' ? -1 : (dir === 's' ? 1 : 0);
|
||||
const chevOffX = dir === 'w' ? -1 : (dir === 'e' ? 1 : 0);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.5 + Math.sin(G.time * 3 + dirSeed) * 0.2;
|
||||
ctx.strokeStyle = locked ? '#ff2a3c' : '#e8b04a';
|
||||
ctx.lineWidth = 3;
|
||||
for (let c = 1; c <= 2; c++) {
|
||||
const d0 = WALL + 10 + c * 12;
|
||||
ctx.beginPath();
|
||||
if (horiz) {
|
||||
const yy = y + chevOff * d0;
|
||||
ctx.moveTo(x - 10, yy); ctx.lineTo(x, yy + chevOff * 8); ctx.lineTo(x + 10, yy);
|
||||
} else {
|
||||
const xx = x + chevOffX * d0;
|
||||
ctx.moveTo(xx, y - 10); ctx.lineTo(xx + chevOffX * 8, y); ctx.lineTo(xx, y + 10);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
if (locked) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.5 + Math.sin(G.time * 6) * 0.2;
|
||||
ctx.fillStyle = '#8f1f2a';
|
||||
if (dir === 'n' || dir === 's') ctx.fillRect(x - DOOR_HALF, y - 6, DOOR_HALF * 2, 12);
|
||||
else ctx.fillRect(x - 6, y - DOOR_HALF, 12, DOOR_HALF * 2);
|
||||
if (horiz) ctx.fillRect(x - DOOR_HALF, y - 8, DOOR_HALF * 2, 16);
|
||||
else ctx.fillRect(x - 8, y - DOOR_HALF, 16, DOOR_HALF * 2);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
+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=36')
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=37')
|
||||
.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)))
|
||||
|
||||
+17
-4
@@ -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=36';
|
||||
img.src = 'assets/' + name + '.png?v=37';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -193,8 +193,8 @@ const Sprites = {
|
||||
ctx.fillStyle = C('#e8e0d0');
|
||||
ctx.beginPath(); ctx.arc(-3, -3, 3.5, 0, TAU); ctx.arc(-3, 4, 3.5, 0, TAU); ctx.fill(); // eyes
|
||||
} else if (name.startsWith('enemy_')) {
|
||||
const cols = { enemy_shambler: '#6a8f3c', enemy_runner: '#a33e2e', enemy_spitter: '#7a4f9e', enemy_splitter: '#8f6b3c', enemy_mini: '#7a5a30', enemy_exploder: '#c93b3b' };
|
||||
const r = name === 'enemy_mini' ? 6 : (name === 'enemy_exploder' ? 10 : 11);
|
||||
const cols = { enemy_shambler: '#6a8f3c', enemy_runner: '#a33e2e', enemy_spitter: '#7a4f9e', enemy_splitter: '#8f6b3c', enemy_mini: '#b8202f', enemy_exploder: '#c93b3b' };
|
||||
const r = name === 'enemy_mini' ? 9 : (name === 'enemy_exploder' ? 10 : 11);
|
||||
ctx.fillStyle = C(cols[name] || '#888');
|
||||
ctx.beginPath(); ctx.arc(0, 0, r, 0, TAU); ctx.fill();
|
||||
ctx.fillStyle = C('#1a0505');
|
||||
@@ -221,7 +221,20 @@ const Sprites = {
|
||||
ctx.fillStyle = C('#3d1f22'); ctx.fillRect(-16, -16, 32, 32);
|
||||
ctx.fillStyle = C('#54282c'); ctx.fillRect(-16, -16, 32, 5);
|
||||
} else if (name === 'door_open' || name === 'door_locked') {
|
||||
ctx.fillStyle = C('#180d0e'); ctx.fillRect(-15, -8, 30, 16);
|
||||
// void
|
||||
ctx.fillStyle = C('#060104'); ctx.fillRect(-16, -10, 32, 20);
|
||||
// jamb
|
||||
ctx.fillStyle = C('#241116'); ctx.fillRect(-16, -12, 32, 3); ctx.fillRect(-16, 9, 32, 3);
|
||||
// crimson rim light
|
||||
ctx.fillStyle = C(name === 'door_locked' ? '#ff2a3c' : '#c4172a');
|
||||
ctx.fillRect(-16, -11, 2, 22); ctx.fillRect(14, -11, 2, 22);
|
||||
// bone teeth
|
||||
ctx.fillStyle = C('#d8ccb2');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ty = -8 + i * 4;
|
||||
ctx.beginPath(); ctx.moveTo(-14, ty); ctx.lineTo(-10, ty + 2); ctx.lineTo(-14, ty + 4); ctx.closePath(); ctx.fill();
|
||||
ctx.beginPath(); ctx.moveTo(14, ty); ctx.lineTo(10, ty + 2); ctx.lineTo(14, ty + 4); ctx.closePath(); ctx.fill();
|
||||
}
|
||||
if (name === 'door_locked') {
|
||||
ctx.fillStyle = C('#8f2f2f');
|
||||
for (let i = -13; i < 15; i += 6) { ctx.beginPath(); ctx.moveTo(i, -8); ctx.lineTo(i + 3, 0); ctx.lineTo(i, 8); ctx.fill(); }
|
||||
|
||||
+19
-2
@@ -26,6 +26,7 @@ const G = {
|
||||
perkChoices: null,
|
||||
autoPerk: false,
|
||||
pressure: 1,
|
||||
pressureDial: 0,
|
||||
streak: 0,
|
||||
roomDamaged: false,
|
||||
roomEnterT: 0,
|
||||
@@ -36,6 +37,7 @@ const G = {
|
||||
helpPage: 0,
|
||||
sfxVol: 0.45,
|
||||
musicVol: 0.55,
|
||||
hudAlpha: 1,
|
||||
imagesLoaded: false,
|
||||
roomLayer: null,
|
||||
roomLayerKey: '',
|
||||
@@ -43,7 +45,18 @@ const G = {
|
||||
};
|
||||
|
||||
const PERK_POWER_WEIGHT = 0.5;
|
||||
const PRESSURE_MIN = 0.75, PRESSURE_MAX = 1.60, PRESSURE_GAIN = 0.01;
|
||||
const PRESSURE_MIN = 0.75, PRESSURE_MAX = 1.60;
|
||||
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_DIAL_MIN = -5, PRESSURE_DIAL_MAX = 5;
|
||||
|
||||
// -5 -> 0 units, 0 -> 1 unit, +5 -> 5 units (piecewise linear)
|
||||
function pressureGainUnits(dial) { const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); return d <= 0 ? 1 + d * 0.2 : 1 + d * 0.8; }
|
||||
// -5 -> 5 units, 0 -> 3 units, +5 -> 0 units (piecewise linear)
|
||||
function pressureDropUnits(dial) { const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); return d <= 0 ? 3 - d * 0.4 : 3 - d * 0.6; }
|
||||
function pressureGain() { return pressureGainUnits(G.pressureDial) * PRESSURE_UNIT; }
|
||||
function pressureDropScale() { return pressureDropUnits(G.pressureDial) / PRESSURE_DROP_BASE; }
|
||||
|
||||
// total player power: perks drafted (level - 1) + item tiers owned.
|
||||
// monsters scale against this so difficulty tracks the build.
|
||||
@@ -60,4 +73,8 @@ function addToast(text, sub) {
|
||||
G.toasts.push({ text, sub: sub || '', t: 0 });
|
||||
if (G.toasts.length > 3) G.toasts.shift();
|
||||
}
|
||||
function addScore(n) { G.score += n; }
|
||||
function addScore(n) {
|
||||
if (!n) return;
|
||||
const scaled = Math.round(n * (G.pressure || 1));
|
||||
G.score += n > 0 ? Math.max(1, scaled) : scaled;
|
||||
}
|
||||
Reference in new issue
Block a user