Add locked gore death and enemy health pips
This commit is contained in:
1 parent
2f1769a528
commit
0eab705ed3
9 files changed
+153
-20
No files matched your search
+19
-8
@@ -10,6 +10,7 @@ const ENEMY_TYPES = {
|
||||
};
|
||||
const MAX_ENEMIES = 72;
|
||||
const SPAWN_WARN = 0.5; // seconds a wave-spawned enemy materializes, frozen + harmless
|
||||
const HP_BAR_SHOW = 2.5;
|
||||
|
||||
function makeEnemy(type, x, y, floor, elite) {
|
||||
const t = ENEMY_TYPES[type];
|
||||
@@ -28,7 +29,7 @@ function makeEnemy(type, x, y, floor, elite) {
|
||||
xp: t.xp * (elite ? 3 : 1),
|
||||
sprite: t.sprite,
|
||||
elite: !!elite,
|
||||
hitT: 0, flash: 0, burnT: 0, burnDps: 0, bleedT: 0, bleedDps: 0,
|
||||
hitT: 0, flash: 0, hpBarT: 0, burnT: 0, burnDps: 0, bleedT: 0, bleedDps: 0,
|
||||
slowT: 0, stunT: 0, rootT: 0,
|
||||
fireT: rand(1, 2.5), fuse: -1, wob: rand(0, TAU),
|
||||
animSeed: rand(0, TAU), animT: rand(0, 1), attackT: 0, actionT: 0,
|
||||
@@ -110,6 +111,7 @@ function damageEnemy(e, dmg, ang, knockback, opts) {
|
||||
dmg *= e.dmgTakenMul === undefined ? 1 : e.dmgTakenMul;
|
||||
e.hp -= dmg;
|
||||
e.flash = 0.12;
|
||||
e.hpBarT = HP_BAR_SHOW;
|
||||
spawnText(e.x, e.y, Math.max(1, Math.round(dmg)), mitigated ? '#d0b887' : '#ffb0a0');
|
||||
if (e.plates > 0) {
|
||||
e.plateDmg = (e.plateDmg || 0) + dmg;
|
||||
@@ -342,6 +344,7 @@ function updateEnemies(dt) {
|
||||
if (e.hp <= 0) { G.enemies.splice(i, 1); continue; }
|
||||
e.spawnT = Math.min((e.spawnT || 0) + dt, 1);
|
||||
e.animT = (e.animT || 0) + dt;
|
||||
if (e.hpBarT > 0) e.hpBarT = Math.max(0, e.hpBarT - dt);
|
||||
if (e.attackT > 0) { e.attackT -= dt; e.actionT += dt; }
|
||||
|
||||
// materializing spawns are frozen and harmless until the telegraph ends
|
||||
@@ -356,12 +359,14 @@ function updateEnemies(dt) {
|
||||
if (e.orbT > 0) e.orbT -= dt;
|
||||
if (e.burnT > 0) {
|
||||
e.burnT -= dt;
|
||||
e.hpBarT = HP_BAR_SHOW;
|
||||
tickEnemyDamage(e, e.burnDps * dt);
|
||||
if (chance(0.2)) addParticle({ type: 'flame', x: e.x + rand(-6, 6), y: e.y + rand(-6, 6), vx: 0, vy: -40, life: 0.3, t: 0, r: 3 });
|
||||
if (e.hp <= 0) { G.enemies.splice(i, 1); continue; }
|
||||
}
|
||||
if (e.bleedT > 0) {
|
||||
e.bleedT -= dt;
|
||||
e.hpBarT = HP_BAR_SHOW;
|
||||
tickEnemyDamage(e, e.bleedDps * dt);
|
||||
if (e.hp <= 0) { G.enemies.splice(i, 1); continue; }
|
||||
}
|
||||
@@ -497,13 +502,19 @@ function drawEnemies(ctx) {
|
||||
ctx.beginPath(); ctx.arc(e.x, e.y, e.r + 3, 0, TAU); ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
// boss & elite hp pip
|
||||
if (e.boss || e.elite) {
|
||||
const w = e.boss ? 60 : 26;
|
||||
ctx.fillStyle = '#400a10';
|
||||
ctx.fillRect(e.x - w / 2, e.y - e.r - 10, w, 4);
|
||||
ctx.fillStyle = '#d92038';
|
||||
ctx.fillRect(e.x - w / 2, e.y - e.r - 10, w * clamp(e.hp / e.maxHp, 0, 1), 4);
|
||||
// Damaged normal monsters show a compact timed pip; elites/bosses stay visible.
|
||||
if (!e.phased && (e.boss || e.elite || (e.hpBarT > 0 && e.hp < e.maxHp))) {
|
||||
const w = e.boss ? 60 : (e.elite ? 26 : (e.type === 'mini' ? 14 : 18));
|
||||
const h = e.boss || e.elite ? 4 : 3;
|
||||
const alpha = e.boss || e.elite ? 1 : clamp(e.hpBarT / 0.5, 0, 1);
|
||||
ctx.save(); ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = '#24070b';
|
||||
ctx.fillRect(e.x - w / 2 - 1, e.y - e.r - 11, w + 2, h + 2);
|
||||
ctx.fillStyle = '#5a0d18';
|
||||
ctx.fillRect(e.x - w / 2, e.y - e.r - 10, w, h);
|
||||
ctx.fillStyle = '#e22b46';
|
||||
ctx.fillRect(e.x - w / 2, e.y - e.r - 10, w * clamp(e.hp / e.maxHp, 0, 1), h);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -142,6 +142,8 @@ function confirmAction(name, action) {
|
||||
|
||||
function gameOver() {
|
||||
G.mode = 'gameover';
|
||||
G.deathLockT = DEATH_LOCK;
|
||||
spawnPlayerGore(G.player.x, G.player.y);
|
||||
Sfx.playerDeath();
|
||||
if (G.score > (G.best || 0)) {
|
||||
G.best = G.score;
|
||||
@@ -161,6 +163,7 @@ function loop(now) {
|
||||
|
||||
function update(dt) {
|
||||
Music.update(dt);
|
||||
if (G.deathLockT > 0) G.deathLockT = Math.max(0, G.deathLockT - dt);
|
||||
if (G.confirmT > 0) {
|
||||
G.confirmT = Math.max(0, G.confirmT - dt);
|
||||
if (G.confirmT <= 0) G.confirmAction = null;
|
||||
@@ -254,7 +257,7 @@ function update(dt) {
|
||||
}
|
||||
case 'gameover':
|
||||
updateParticles(dt);
|
||||
if (keyPressed('r', 'enter', ' ') || Input.mpressed) startRun();
|
||||
if (G.deathLockT <= 0 && (keyPressed('r', 'enter', ' ') || Input.mpressed)) startRun();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -503,6 +506,9 @@ function drawPause(ctx) {
|
||||
}
|
||||
|
||||
function drawGameOver(ctx) {
|
||||
const reveal = clamp((DEATH_LOCK - G.deathLockT) / 0.9, 0, 1);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = reveal;
|
||||
ctx.fillStyle = 'rgba(4,1,3,0.86)'; ctx.fillRect(0, 0, W, H);
|
||||
drawPixelPanel(ctx, W / 2 - 230, 142, 460, 350, {
|
||||
accent: '#e02945', border: '#772638', fill: 'rgba(12,4,7,0.97)',
|
||||
@@ -529,10 +535,12 @@ function drawGameOver(ctx) {
|
||||
drawPixelTag(ctx, 'NEW BEST CUT', W / 2 - 68, 367, { width: 136, height: 26, color: '#f1cb53', accent: '#b38b2e' });
|
||||
}
|
||||
drawPixelPanel(ctx, W / 2 - 140, 414, 280, 50, {
|
||||
cut: 5, shadow: false, accent: '#cf2942', border: '#8c293c', fill: '#1a080e',
|
||||
cut: 5, shadow: false, accent: G.deathLockT > 0 ? '#77552a' : '#cf2942',
|
||||
border: G.deathLockT > 0 ? '#604727' : '#8c293c', fill: '#1a080e',
|
||||
});
|
||||
ctx.fillStyle = '#f1e4d5'; ctx.font = 'bold 12px monospace';
|
||||
ctx.fillText('[ R / ENTER ] CUT AGAIN', W / 2, 444);
|
||||
ctx.fillStyle = G.deathLockT > 0 ? '#d4ad3d' : '#f1e4d5'; ctx.font = 'bold 12px monospace';
|
||||
ctx.fillText(G.deathLockT > 0 ? '[ INPUT LOCKED · ' + G.deathLockT.toFixed(1) + ' ]' : '[ R / ENTER ] CUT AGAIN', W / 2, 444);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
window.addEventListener('load', init);
|
||||
+17
-4
@@ -48,7 +48,7 @@ function onGore(x, y) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function spawnGibs(x, y, n, big) {
|
||||
function spawnGibs(x, y, n, big, important) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = rand(0, TAU), sp = rand(60, big ? 290 : 220);
|
||||
const bone = chance(big ? 0.3 : 0.18);
|
||||
@@ -59,7 +59,7 @@ function spawnGibs(x, y, n, big) {
|
||||
life: rand(0.5, big ? 1.35 : 1.1), t: 0, r,
|
||||
w: r * rand(1.3, 2.6), h: r * rand(0.65, 1.25),
|
||||
bone, shade: i % 3, rot: rand(0, TAU), vr: rand(-10, 10),
|
||||
});
|
||||
}, important);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,11 +97,11 @@ function spawnSmoke(x, y, ang, n, color) {
|
||||
}
|
||||
}
|
||||
|
||||
function spawnShockwave(x, y, radius, color, alpha) {
|
||||
function spawnShockwave(x, y, radius, color, alpha, important) {
|
||||
addParticle({
|
||||
type: 'shockwave', x, y, vx: 0, vy: 0, life: 0.42, t: 0,
|
||||
r: 5, maxR: radius, color: color || '#ffb06a', alpha: alpha === undefined ? 0.8 : alpha,
|
||||
});
|
||||
}, important);
|
||||
}
|
||||
|
||||
function spawnMuzzleFx(x, y, ang, behavior) {
|
||||
@@ -144,6 +144,19 @@ function spawnExplosionFx(x, y, r) {
|
||||
addShake(6);
|
||||
}
|
||||
|
||||
function spawnPlayerGore(x, y) {
|
||||
spawnExplosionFx(x, y, 110);
|
||||
spawnShockwave(x, y, 138, '#ff2947', 0.95, true);
|
||||
for (let i = 0; i < 4; i++) spawnBlood(x, y, i * TAU / 4, 10, true);
|
||||
spawnGibs(x, y, 26, true, true);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const a = rand(0, TAU), sp = rand(90, 350);
|
||||
addParticle({ type: 'flame', x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp,
|
||||
life: rand(0.22, 0.68), t: 0, r: rand(4, 10) }, true);
|
||||
}
|
||||
addShake(16);
|
||||
}
|
||||
|
||||
function spawnCorpse(e) {
|
||||
G.corpses.push({
|
||||
sprite: e.sprite, x: e.x, y: e.y,
|
||||
|
||||
+1
-2
@@ -411,8 +411,7 @@ function drawPlayer(ctx) {
|
||||
const kick = p.recoil * 3;
|
||||
Sprites.shadow(ctx, p.x, p.y + 17, 27 + p.moveBlend * 2, 9, 0.46);
|
||||
if (p.hp <= 0) {
|
||||
// Preserve the authored full-body fall while the live character stays layered.
|
||||
Sprites.actor(ctx, 'player', p.x, p.y, p.bodyFacing, 'death', p.deathT || 0, 96);
|
||||
Sprites.strip(ctx, 'player_death_sheet', p.x, p.y, p.deathT || 0, 12, 14, 128, 144, true);
|
||||
} else {
|
||||
Sprites.legs(ctx, p.x, p.y + 4, p.bodyFacing, p.step, 124);
|
||||
const torsoOffset = (w.torsoFwd || 18) - kick;
|
||||
|
||||
+15
-1
@@ -48,7 +48,7 @@ const SPRITE_MANIFEST = [
|
||||
'a_bonenova', 'a_offalbomb', 'a_bloodtransfusion', 'a_cleaverstorm', 'a_butchersbell',
|
||||
'a_marrowdraught', 'a_slaughtertime', 'a_panicroom', 'a_skinnerscoin', 'a_gutreroll',
|
||||
// full 64px/128px eight-direction action atlases
|
||||
'player_sheet', 'player_legs_sheet',
|
||||
'player_sheet', 'player_legs_sheet', 'player_death_sheet',
|
||||
'enemy_shambler_sheet', 'enemy_runner_sheet', 'enemy_spitter_sheet',
|
||||
'enemy_splitter_sheet', 'enemy_mini_sheet', 'enemy_exploder_sheet',
|
||||
'boss_bonesaw_sheet', 'boss_gorecrown_sheet', 'boss_knifecrawl_sheet',
|
||||
@@ -111,6 +111,20 @@ const Sprites = {
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
// Non-directional one-row effects such as the player's authored gore burst.
|
||||
strip(ctx, name, x, y, time, frames, fps, frameSize, targetW, hold) {
|
||||
const sheet = this.imgs[name];
|
||||
if (!sheet) {
|
||||
this.actor(ctx, 'player', x, y, 0, 'death', time, targetW || frameSize);
|
||||
return;
|
||||
}
|
||||
const raw = Math.floor(Math.max(0, time) * fps);
|
||||
const frame = hold ? Math.min(frames - 1, raw) : raw % frames;
|
||||
const tw = targetW || frameSize;
|
||||
ctx.drawImage(sheet, frame * frameSize, 0, frameSize, frameSize,
|
||||
x - tw / 2, y - tw / 2, tw, tw);
|
||||
},
|
||||
|
||||
actor(ctx, name, x, y, facing, action, time, targetW, alpha, scaleX, scaleY) {
|
||||
const sheet = this.imgs[name + '_sheet'];
|
||||
const def = ACTOR_ANIMS[action] || ACTOR_ANIMS.idle;
|
||||
|
||||
@@ -33,6 +33,7 @@ const G = {
|
||||
recentHits: [],
|
||||
confirmAction: null,
|
||||
confirmT: 0,
|
||||
deathLockT: 0,
|
||||
transition: 0, // room transition fade
|
||||
muted: false,
|
||||
pauseHelp: false,
|
||||
@@ -53,6 +54,7 @@ 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 DEATH_LOCK = 3;
|
||||
const PRESSURE_DIAL_MIN = -5, PRESSURE_DIAL_MAX = 5;
|
||||
|
||||
// -5 -> 0 units, 0 -> 1 unit, +5 -> 5 units (piecewise linear)
|
||||
|
||||
Reference in new issue
Block a user