Phase 5: active items — room-clear charged, dedicated pedestals, Space to use

- ACTIVES table with 10 actives (Bone Nova, Offal Bomb, Blood Transfusion,
  Cleaver Storm, Butcher's Bell, Marrow Draught, Slaughter Time, Panic Room,
  Skinner's Coin, Gut Reroll); charges accrue +1/combat room, +2/boss
- Dedicated 'active' pickup with weapon-style swap-drop; 30% boss / 15% item-room
  bonus pedestal spawns; flat +2 powerScore while held
- HUD active slot with charge pips + READY state; ACTIVES help page (12 pages);
  teal medallion frame variant in draw_sprites.py; 10 generated icons
- Procedural Sfx.active()/activeEmpty() (real clips deferred to audio batch)
- Fix: toast panel width now fits the wider of title/subtitle and clamps to screen
- Bump asset cache to v=40
This commit is contained in:
MeatSlicer Developer committed 2026-07-30 17:14:25 +10:00
1 parent f71d202850
commit 0ec12cd72c
27 files changed
+337 -37

No files matched your search

+40 -3
View File
@@ -11,6 +11,7 @@ function initPlayer() {
holstered: null, // a special weapon set aside while the pistol is out
level: 1, xp: 0,
items: {}, // iid -> quality tier (1..3)
active: null, // { iid, charges } — active item, room-clear charged
orbAng: 0,
stats: {
dmgMul: 1, rateMul: 1, speedMul: 1, shotSpeedMul: 1, rangeMul: 1,
@@ -107,7 +108,8 @@ function updatePlayer(dt) {
Sfx.syncWeaponLoop(w, sustained && Input.mdown && p.weapon.ammo > 0);
if (p.fireT > 0) p.fireT -= dt;
const frenzyMul = p.frenzyT > 0 ? 1 + st.frenzy : 1;
const effectiveRate = st.rateMul * (1 + (st.rhythmRateBonus || 0)) * frenzyMul;
const draughtMul = p.marrowDraughtT > 0 ? 2 : 1;
const effectiveRate = st.rateMul * (1 + (st.rhythmRateBonus || 0)) * frenzyMul * draughtMul;
const rate = w.interval / effectiveRate;
if (w.behavior === 'saw') {
@@ -128,7 +130,7 @@ function updatePlayer(dt) {
p.charge = Math.max(0, p.charge - dt * 2);
}
} else {
if (Input.mdown && p.fireT <= 0 && p.weapon.ammo > 0) {
if (Input.mdown && p.fireT <= 0 && p.weapon.ammo > 0 && !(p.panicRoomT > 0)) {
fireWeapon(p, w);
// Twin Sidearm: the Bone Popper double-taps
if (st.twinSidearm > 0 && p.weapon.id === 'bonepopper') fireWeapon(p, w);
@@ -146,7 +148,7 @@ function updatePlayer(dt) {
p.muzzleT = 0.07;
p.attackT = 0.4; p.actionT = 0;
p.fireT = rate;
if (p.weapon.ammo !== Infinity) p.weapon.ammo -= (st.twinSidearm > 0 ? 2 : 1) / st.ammoEff;
if (p.weapon.ammo !== Infinity && !(p.marrowDraughtT > 0)) p.weapon.ammo -= (st.twinSidearm > 0 ? 2 : 1) / st.ammoEff;
Sfx.shoot(w);
if (w.behavior === 'slam') addShake(5);
}
@@ -210,6 +212,34 @@ function updatePlayer(dt) {
}
}
// ---- active-item timed effects ----
// Marrow Draught: +100% fire rate and free ammo while active
if (p.marrowDraughtT > 0) p.marrowDraughtT -= dt;
// Panic Room: invulnerable but cannot fire
if (p.panicRoomT > 0) p.panicRoomT -= dt;
// Cleaver Storm: 12 orbiting cleavers shred on contact
if (p.cleaverStormT > 0) {
p.cleaverStormT -= dt;
p.cleaverStormAng = (p.cleaverStormAng || 0) + dt * 6;
p.cleaverStormTick = (p.cleaverStormTick || 0) - dt;
const doTick = p.cleaverStormTick <= 0;
if (doTick) p.cleaverStormTick = 0.25;
for (let i = 0; i < 12; i++) {
const a = p.cleaverStormAng + (i / 12) * TAU;
const cx = p.x + Math.cos(a) * 90, cy = p.y + Math.sin(a) * 90;
if (!doTick) continue;
for (const e of G.enemies) {
if (e.hp <= 0 || e.stormHitT > 0) continue;
if (dist2(cx, cy, e.x, e.y) < (14 + e.r) * (14 + e.r)) {
e.stormHitT = 0.3;
damageEnemy(e, 10 * st.dmgMul * st.dmgLiveMul, a + Math.PI / 2, true, { source: 'player' });
spawnBlood(e.x, e.y, a, 3);
}
}
}
for (const e of G.enemies) if (e.stormHitT > 0) e.stormHitT -= dt;
}
// Thresher Plate: passive contact-damage aura (no need to be hit)
if (st.thresherPlate > 0) {
p.thresherTick = (p.thresherTick || 0) - dt;
@@ -358,6 +388,13 @@ function drawPlayer(ctx) {
Sprites.draw(ctx, 'bullet_cleaver', p.x + Math.cos(a) * 58, p.y + Math.sin(a) * 58, a + Math.PI / 2, 24);
}
}
// Cleaver Storm active: 12 orbiting cleavers
if (p.cleaverStormT > 0) {
for (let i = 0; i < 12; i++) {
const a = (p.cleaverStormAng || 0) + (i / 12) * TAU;
Sprites.draw(ctx, 'bullet_cleaver', p.x + Math.cos(a) * 90, p.y + Math.sin(a) * 90, a + Math.PI / 2, 22);
}
}
const w = WEAPONS[p.weapon.id];
const kick = p.recoil * 3;