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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user