diff --git a/js/help.js b/js/help.js index 8d72d02..1d05398 100644 --- a/js/help.js +++ b/js/help.js @@ -292,9 +292,9 @@ function drawHelpPressure(ctx, px, py) { 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×', + 'MULTIPLIES ENEMY AND BOSS HP + SPEED AT SPAWN TIME · RANGE ' + PRESSURE_MIN.toFixed(2) + '× – ' + PRESSURE_MAX.toFixed(2) + '×', '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', + 'DIAL ' + (d > 0 ? '+' : '') + d + ' — RISE ' + gain.toFixed(1) + '/10 PER CLEAN ROOM · RELIEF ' + drop.toFixed(1) + '/10 ON HIT', '+' + pressureGain().toFixed(3) + ' FOR EVERY ROOM CLEARED WITHOUT TAKING ANY DAMAGE', 'RELIEF ON HIT = ' + (0.03 * scale).toFixed(3) + ' × SEVERITY × DESPERATION × CHURN', ' SEVERITY = HIT / (MAX HP × 0.25), CLAMPED 0.4 – 2', diff --git a/js/main.js b/js/main.js index 6068715..7d1aad1 100644 --- a/js/main.js +++ b/js/main.js @@ -188,7 +188,7 @@ function update(dt) { 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); + setPressureDial(Math.round(((Input.mx - sl.x) / sl.w) * 20) - 10); } if (!Input.mdown) G.menuDialDrag = false; if (keyPressed('enter', ' ') || (Input.mpressed && !inRect(Input.mx, Input.my, sl) && !exitClick)) startRun(); @@ -403,26 +403,28 @@ function drawPressureDial(ctx) { 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'; + const name = d <= -9 ? 'FORGIVING' : d <= -6 ? 'MERCIFUL' : d <= -3 ? 'EASED' : d <= 0 ? 'STANDARD' : + d <= 3 ? 'TENSE' : d <= 5 ? 'SAVAGE' : d <= 7 ? 'RELENTLESS' : d <= 9 ? 'ATROCIOUS' : 'MEAT GRINDER'; 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 + // track with 20 steps / 21 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, + drawPixelBar(ctx, bx, by, bw, bh, (d + 10) / 20, { + fill: accent, glint: '#f4d86d', border: '#5d3c43', segments: 20, }); // bright notch marker - const nx = bx + Math.round((d + 5) / 10 * bw); + const nx = bx + Math.round((d + 10) / 20 * 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); + ctx.fillText('-10', bx - 18, by + 9); ctx.fillText('+10', bx + bw + 18, 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); + const rise = (gain >= 0 ? '+' : '') + gain.toFixed(1); + ctx.fillText('RISE ' + rise + '/10 · RELIEF ' + drop.toFixed(1) + '/10 · SCORE ×LIVE PRESSURE', sl.x + sl.w / 2, sl.y + sl.h - 7); } function drawPause(ctx) { diff --git a/js/rooms.js b/js/rooms.js index 5214a03..cbee65f 100644 --- a/js/rooms.js +++ b/js/rooms.js @@ -215,7 +215,7 @@ function updateRoom(dt) { function recordRoomClear(room) { if (!G.roomDamaged) { G.streak++; - G.pressure = Math.min(PRESSURE_MAX, G.pressure + pressureGain()); + G.pressure = clamp(G.pressure + pressureGain(), PRESSURE_MIN, PRESSURE_MAX); } else { G.streak = 0; } diff --git a/js/state.js b/js/state.js index 666d112..800f7f5 100644 --- a/js/state.js +++ b/js/state.js @@ -51,17 +51,28 @@ const G = { const PERK_POWER_WEIGHT = 0.5; // ammo economy: global dial for drop frequency (refill size is per-weapon) const AMMO_DROP_SCALE = 0.75; -const PRESSURE_MIN = 0.75, PRESSURE_MAX = 1.60; +const PRESSURE_MIN = 0.60, PRESSURE_MAX = 2.00; 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 DEATH_LOCK = 3; -const PRESSURE_DIAL_MIN = -5, PRESSURE_DIAL_MAX = 5; +const PRESSURE_DIAL_MIN = -10, PRESSURE_DIAL_MAX = 10; -// -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; } +// Preserve the old -5 / 0 / +5 balance while adding genuinely wider extremes. +function pressureGainUnits(dial) { + const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); + if (d <= -5) return (d + 5) * 0.4; // -10 -> -2, -5 -> 0 + if (d <= 0) return 1 + d * 0.2; // -5 -> 0, 0 -> 1 + if (d <= 5) return 1 + d * 0.8; // 0 -> 1, +5 -> 5 + return 5 + (d - 5); // +5 -> 5, +10 -> 10 +} +function pressureDropUnits(dial) { + const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); + if (d <= -5) return 5 - (d + 5) * 0.6; // -10 -> 8, -5 -> 5 + if (d <= 0) return 3 - d * 0.4; // -5 -> 5, 0 -> 3 + if (d <= 5) return 3 - d * 0.6; // 0 -> 3, +5 -> 0 + return 0; +} function pressureGain() { const base = pressureGainUnits(G.pressureDial) * PRESSURE_UNIT; return base * (G.player && G.player.stats.abattoirEngine > 0 ? 2 : 1); } function pressureDropScale() { return pressureDropUnits(G.pressureDial) / PRESSURE_DROP_BASE; } diff --git a/test/smoke.js b/test/smoke.js index f066a0c..c4505a6 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -61,7 +61,7 @@ for (const f of files) { } // const/let top-level bindings live in the context's lexical scope, not on the // global object — expose the ones the harness pokes at directly. -vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN })', sandbox); +vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_MIN, PRESSURE_MAX, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN })', sandbox); let simTime = 0; const ctx = sandbox; @@ -968,12 +968,12 @@ console.log('== adaptive clean-room pressure =='); check('fully shielded hits preserve a clean-room streak', ctx.G.pressure === 1.2 && ctx.G.streak === 4 && !ctx.G.roomDamaged); p.shieldHp = 0; - ctx.G.pressure = 0.751; ctx.G.recentHits = []; p.hp = 3; p.invT = 0; ctx.G.mode = 'play'; + ctx.G.pressure = 0.601; ctx.G.recentHits = []; p.hp = 3; p.invT = 0; ctx.G.mode = 'play'; ctx.hurtPlayer(1, 0); - check('pressure relief is floored at 75%', ctx.G.pressure === 0.75); + check('pressure relief is floored at 60%', ctx.G.pressure === 0.60); ctx.G.pressure = 9; ctx.G.roomDamaged = false; ctx.recordRoomClear({ type: 'combat' }); - check('pressure is capped at 160%', ctx.G.pressure === 1.6); + check('pressure is capped at 200%', ctx.G.pressure === 2.0); ctx.startRun(); step(3, 16); check('new runs reset pressure and streak', ctx.G.pressure === 1 && ctx.G.streak === 0); } @@ -1038,15 +1038,19 @@ console.log('== 500ms wave spawn telegraph =='); console.log('== pressure dial scaling =='); { - const GAIN = { '-5': 0, '-4': 0.2, '-3': 0.4, '-2': 0.6, '-1': 0.8, '0': 1, '1': 1.8, '2': 2.6, '3': 3.4, '4': 4.2, '5': 5 }; - const DROP = { '-5': 5, '-4': 4.6, '-3': 4.2, '-2': 3.8, '-1': 3.4, '0': 3, '1': 2.4, '2': 1.8, '3': 1.2, '4': 0.6, '5': 0 }; + const GAIN = [-2, -1.6, -1.2, -0.8, -0.4, 0, 0.2, 0.4, 0.6, 0.8, 1, 1.8, 2.6, 3.4, 4.2, 5, 6, 7, 8, 9, 10]; + const DROP = [8, 7.4, 6.8, 6.2, 5.6, 5, 4.6, 4.2, 3.8, 3.4, 3, 2.4, 1.8, 1.2, 0.6, 0, 0, 0, 0, 0, 0]; let gainOK = true, dropOK = true; - for (let d = -5; d <= 5; d++) { - if (Math.abs(ctx.pressureGainUnits(d) - GAIN[d]) > 1e-6) gainOK = false; - if (Math.abs(ctx.pressureDropUnits(d) - DROP[d]) > 1e-6) dropOK = false; + for (let d = -10; d <= 10; d++) { + if (Math.abs(ctx.pressureGainUnits(d) - GAIN[d + 10]) > 1e-6) gainOK = false; + if (Math.abs(ctx.pressureDropUnits(d) - DROP[d + 10]) > 1e-6) dropOK = false; } - check('gain units match all 11 notches', gainOK); - check('drop units match all 11 notches', dropOK); + check('gain units match all 21 notches', gainOK); + check('drop units match all 21 notches', dropOK); + ctx.G.pressureDial = 10; + ctx.G.pressure = 1; ctx.G.roomDamaged = false; + ctx.recordRoomClear({ type: 'combat' }); + check('dial +10 clean room adds 10%', Math.abs(ctx.G.pressure - 1.10) < 1e-4); // dial +5: gain 0.05/clean room, zero relief on hit ctx.G.pressureDial = 5; ctx.G.pressure = 1; ctx.G.roomDamaged = false; @@ -1061,6 +1065,13 @@ console.log('== pressure dial scaling =='); ctx.G.pressure = 1; ctx.G.roomDamaged = false; ctx.recordRoomClear({ type: 'combat' }); check('dial -5 clean room adds nothing', Math.abs(ctx.G.pressure - 1) < 1e-6); + ctx.G.pressureDial = -10; + ctx.G.pressure = 1; ctx.G.roomDamaged = false; + ctx.recordRoomClear({ type: 'combat' }); + check('dial -10 clean room bleeds 2% pressure', Math.abs(ctx.G.pressure - 0.98) < 1e-6); + ctx.G.pressure = 0.605; ctx.G.roomDamaged = false; + ctx.recordRoomClear({ type: 'combat' }); + check('negative clean-room gain respects the 60% floor', ctx.G.pressure === 0.60); const mkHit = (dial) => { ctx.G.pressureDial = dial; ctx.G.pressure = 1.2; ctx.G.recentHits = []; @@ -1069,9 +1080,11 @@ console.log('== pressure dial scaling =='); return 1.2 - ctx.G.pressure; }; const reliefNeg = mkHit(-5); + const reliefExtreme = mkHit(-10); const reliefZero = mkHit(0); check('dial -5 relief beats dial 0 (' + reliefNeg.toFixed(4) + ' vs ' + reliefZero.toFixed(4) + ')', reliefNeg > reliefZero && Math.abs(reliefNeg - reliefZero * (5 / 3)) < 1e-4); + check('dial -10 relief is 8/3 of dial 0', reliefExtreme > reliefNeg && Math.abs(reliefExtreme - reliefZero * (8 / 3)) < 1e-4); // score multiplier ctx.G.pressureDial = 0; ctx.G.pressure = 1.5;