Seal boss stairs after victory
This commit is contained in:
1 parent
d96d492a43
commit
92491dd06e
9 files changed
+55
-15
No files matched your search
@@ -41,6 +41,7 @@ Killing a boss grants **500 + floor × 100** score, clears all hostile bullets/t
|
||||
- **1 item pedestal** (left of room center)
|
||||
- **1 weapon drop** rolled at **floor + 2** (right of center)
|
||||
- **1 ammo pickup** (above center) — the boss room also stocks one when you walk in, so you can afford to spend the magazine on the fight
|
||||
- **1 sealed stair hatch** (below center) — it remains closed and uncollectable for 3 seconds after the kill so the player cannot fall through it accidentally
|
||||
- **Stairs Down** (below center)
|
||||
|
||||
> *"[NAME] DESTROYED — take the meat, take the stairs"*
|
||||
|
||||
+1
-1
@@ -202,7 +202,7 @@ Clearing a combat room drops:
|
||||
|---|---|
|
||||
| Normal enemy | XP gems (enemy's XP value) · 5.25% + luck×3% ammo · 2.2% + luck×3% heart |
|
||||
| Elite enemy | 3× XP + 3–5 bonus gems · 18% + luck×10% **item** · else 45% + luck×15% ammo |
|
||||
| Boss | 1 item pedestal + 1 weapon (rolled at floor + 2) + 1 guaranteed ammo + stairs · the boss room also stocks 1 ammo when you enter |
|
||||
| Boss | 1 item pedestal + 1 weapon (rolled at floor + 2) + 1 guaranteed ammo + stairs · the boss room also stocks 1 ammo when you enter · the stair hatch stays sealed for 3 seconds after the kill |
|
||||
|
||||
**Luck** is a player stat (Lucky Coin, Crow Bait, Scavenger perk) that scales most drop chances as shown above.
|
||||
|
||||
|
||||
+1
-1
@@ -599,7 +599,7 @@ function onBossDeath(e) {
|
||||
if (chance(0.30)) spawnActivePedestal(G.arena.cx - 160, G.arena.cy + 60);
|
||||
const bw = rollWeaponDrop(G.floor + 2); // bosses drop the good stuff
|
||||
spawnPickup('weapon', G.arena.cx + 90, G.arena.cy, { wid: bw.id });
|
||||
spawnPickup('stairs', G.arena.cx, G.arena.cy + 80);
|
||||
spawnPickup('stairs', G.arena.cx, G.arena.cy + 80, { delay: 3 });
|
||||
addToast(e.name + ' DESTROYED', 'take the meat, take the stairs');
|
||||
Music.requestFloorMusic();
|
||||
}
|
||||
@@ -521,6 +521,7 @@ function updateEnemyAI(e, dt) {
|
||||
e.vx += Math.cos(e.faceDir) * spd * 6 * dt; e.vy += Math.sin(e.faceDir) * spd * 6 * dt;
|
||||
if (e.lungeT <= 0) { e.ambushState = 'stalk'; e.ambushT = rand(2.2, 3.4); }
|
||||
} else {
|
||||
e.faceDir = angleLerp(e.faceDir, a, clamp(dt * 8, 0, 1));
|
||||
e.vx += Math.cos(a) * spd * 2.7 * dt; e.vy += Math.sin(a) * spd * 2.7 * dt;
|
||||
e.ambushT -= dt;
|
||||
if (e.ambushT <= 0) {
|
||||
|
||||
+6
-2
@@ -250,9 +250,13 @@ function drawHelpBestiary(ctx, px, py) {
|
||||
let ty = py + HELP_BODY;
|
||||
ctx.textAlign = 'left';
|
||||
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);
|
||||
ctx.fillText(Object.keys(ENEMY_TYPES).length + ' ENEMY TYPES · ELITES: ×2.6 HP, +1 DMG, 3× XP, GOLD RING, ITEM DROPS', x, ty);
|
||||
ty += 20;
|
||||
const notes = { shambler: 'CHASER', runner: 'FAST CHASER', spitter: 'RANGED KITER', splitter: 'SPLITS INTO 2 MINIS', mini: 'SWARMER', exploder: 'FUSED SUICIDE ×2 DMG' };
|
||||
const notes = {
|
||||
shambler: 'CHASER', runner: 'FAST CHASER', spitter: 'RANGED KITER', splitter: 'SPLITS INTO 2 MINIS',
|
||||
mini: 'SWARMER', exploder: 'FUSED SUICIDE ×2 DMG', censer: 'HOSTILE ACID ZONES', bulwark: 'FLANK ITS FRONT ARMOR',
|
||||
choirmaster: 'SPEED + REPAIR AURA', flenserling: 'PHASE AMBUSHER', broodsac: 'SPAWNS MINIS',
|
||||
};
|
||||
ctx.font = hfont(8, false);
|
||||
let row = 0;
|
||||
for (const [type, t] of Object.entries(ENEMY_TYPES)) {
|
||||
|
||||
@@ -98,6 +98,7 @@ function resetRun() {
|
||||
G.parts = []; G.corpses = []; G.toasts = []; G.pendingLevelups = 0; G.perkChoices = null;
|
||||
G.pauseHelp = false; G.helpPage = 0;
|
||||
G.confirmAction = null; G.confirmT = 0;
|
||||
G.deathLockT = 0;
|
||||
G.pressure = 1; G.streak = 0; G.roomDamaged = false; G.roomEnterT = 0; G.recentHits = [];
|
||||
G.shake = 0; G.flash = 0;
|
||||
initPlayer();
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ function addParticle(p, important) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function spawnBlood(x, y, ang, n, big) {
|
||||
function spawnBlood(x, y, ang, n, big, important) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = ang + rand(-0.9, 0.9);
|
||||
const sp = rand(40, big ? 260 : 160);
|
||||
@@ -23,7 +23,7 @@ function spawnBlood(x, y, ang, n, big) {
|
||||
life: rand(0.3, 0.7), t: 0,
|
||||
r: rand(1.5, big ? 4.5 : 3),
|
||||
mist: i % 4 === 0, shade: i % 3,
|
||||
});
|
||||
}, important);
|
||||
}
|
||||
// persistent floor decal
|
||||
if (G.cur && chance(0.55)) {
|
||||
@@ -147,7 +147,7 @@ function spawnExplosionFx(x, y, r) {
|
||||
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);
|
||||
for (let i = 0; i < 4; i++) spawnBlood(x, y, i * TAU / 4, 10, true, true);
|
||||
spawnGibs(x, y, 26, true, true);
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const a = rand(0, TAU), sp = rand(90, 350);
|
||||
|
||||
+19
-8
@@ -215,13 +215,24 @@ function drawPickups(ctx) {
|
||||
ctx.restore();
|
||||
Sprites.draw(ctx, 'a_' + k.aid, k.x, k.y - 28 + bob, 0, 64);
|
||||
} else if (k.type === 'stairs') {
|
||||
Sprites.draw(ctx, 'stairs', k.x, k.y, 0, 64);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.5 + Math.sin(k.t * 3) * 0.25;
|
||||
ctx.strokeStyle = '#d8b84e'; ctx.lineWidth = 2;
|
||||
const inset = 3 + Math.sin(k.t * 3) * 2;
|
||||
ctx.strokeRect(k.x - 31 + inset, k.y - 31 + inset, 62 - inset * 2, 62 - inset * 2);
|
||||
ctx.restore();
|
||||
if (k.delay > 0) {
|
||||
ctx.save(); ctx.translate(k.x, k.y);
|
||||
ctx.fillStyle = '#171116'; ctx.fillRect(-31, -31, 62, 62);
|
||||
ctx.strokeStyle = '#75525a'; ctx.lineWidth = 3; ctx.strokeRect(-29, -29, 58, 58);
|
||||
ctx.fillStyle = '#38242a';
|
||||
for (let sy = -20; sy <= 20; sy += 10) ctx.fillRect(-25, sy, 50, 5);
|
||||
ctx.fillStyle = '#d8b84e'; ctx.font = 'bold 8px monospace'; ctx.textAlign = 'center';
|
||||
ctx.fillText('SEALED ' + k.delay.toFixed(1), 0, 3);
|
||||
ctx.restore();
|
||||
} else {
|
||||
Sprites.draw(ctx, 'stairs', k.x, k.y, 0, 64);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.5 + Math.sin(k.t * 3) * 0.25;
|
||||
ctx.strokeStyle = '#d8b84e'; ctx.lineWidth = 2;
|
||||
const inset = 3 + Math.sin(k.t * 3) * 2;
|
||||
ctx.strokeRect(k.x - 31 + inset, k.y - 31 + inset, 62 - inset * 2, 62 - inset * 2);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// hover nameplates when close
|
||||
@@ -229,7 +240,7 @@ function drawPickups(ctx) {
|
||||
if (k.type === 'weapon') drawPickupLabel(ctx, WEAPONS[k.wid].name, k.x, k.y - 44, '#ffd060');
|
||||
else if (k.type === 'item') drawPickupLabel(ctx, ITEMS[k.iid].name, k.x, k.y - 66, ITEM_RARITY[ITEMS[k.iid].rarity].color);
|
||||
else if (k.type === 'active') drawPickupLabel(ctx, ACTIVES[k.aid].name, k.x, k.y - 66, '#55f5dc');
|
||||
else if (k.type === 'stairs') drawPickupLabel(ctx, 'STAIRS DOWN', k.x, k.y - 52, '#e8e0d0');
|
||||
else if (k.type === 'stairs') drawPickupLabel(ctx, k.delay > 0 ? 'HATCH SEALED' : 'STAIRS DOWN', k.x, k.y - 52, k.delay > 0 ? '#b89c46' : '#e8e0d0');
|
||||
else if (k.type === 'ammo') drawPickupLabel(ctx, 'AMMO', k.x, k.y - 26, '#d0b060');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1346,6 +1346,7 @@ for (let g2 = 0; g2 < 400 && ctx.G.boss; g2++) {
|
||||
check('boss dead', !ctx.G.boss);
|
||||
check('boss death clears remaining minions', ctx.G.enemies.length === 0);
|
||||
check('stairs spawned', ctx.G.pickups.some(p => p.type === 'stairs') || ctx.G.mode !== 'play');
|
||||
check('boss stairs begin with a three-second safety seal', ctx.G.pickups.some(p => p.type === 'stairs' && p.delay > 0 && p.delay <= 3));
|
||||
check('boss drops a weapon', ctx.G.pickups.some(p => p.type === 'weapon'));
|
||||
check('boss reward pedestal', ctx.G.pickups.some(p => p.type === 'item'));
|
||||
|
||||
@@ -1370,6 +1371,8 @@ const stairs = ctx.G.pickups.find(p => p.type === 'stairs');
|
||||
if (stairs) {
|
||||
ctx.G.player.x = stairs.x; ctx.G.player.y = stairs.y;
|
||||
step(5, 16);
|
||||
check('sealed boss stairs cannot advance the floor', ctx.G.floor === 2);
|
||||
step(190, 16);
|
||||
check('floor advanced', ctx.G.floor === 3);
|
||||
}
|
||||
|
||||
@@ -1577,6 +1580,25 @@ tap('r');
|
||||
step(5, 16);
|
||||
check('restart works', ctx.G.mode === 'play' && ctx.G.floor === 1);
|
||||
|
||||
console.log('== confirmed destructive actions ==');
|
||||
{
|
||||
let fired = 0;
|
||||
ctx.confirmAction('probe', () => fired++);
|
||||
check('first destructive action arms confirmation without firing', ctx.G.confirmAction === 'probe' && ctx.G.confirmT > 2.9 && fired === 0);
|
||||
ctx.confirmAction('probe', () => fired++);
|
||||
check('second matching action confirms and clears state', fired === 1 && ctx.G.confirmAction === null && ctx.G.confirmT === 0);
|
||||
ctx.confirmAction('expire', () => fired++);
|
||||
ctx.G.mode = 'inspect';
|
||||
ctx.update(3.1);
|
||||
check('confirmation expires without firing', fired === 1 && ctx.G.confirmAction === null && ctx.G.confirmT === 0);
|
||||
ctx.startRun(); step(3, 16);
|
||||
ctx.G.score = 999; ctx.G.mode = 'pause'; ctx.G.deathLockT = 2;
|
||||
ctx.confirmAction('menu', ctx.returnToMenu);
|
||||
ctx.confirmAction('menu', ctx.returnToMenu);
|
||||
check('confirmed return to menu fully resets the abandoned run', ctx.G.mode === 'menu' && ctx.G.score === 0 && ctx.G.deathLockT === 0);
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== pause / mute ==');
|
||||
tap('p');
|
||||
check('paused', ctx.G.mode === 'pause');
|
||||
|
||||
Reference in new issue
Block a user