Phase 3: 12 hook passives with conditional damage, auras, and kill/room effects

- dmgLiveMul live damage channel wired into fireWeapon, sawTick, fireBeam, orbitals
- Dead Weight (execute), Cauterized Veins (burn bonus), Pain Engine, Iron Lung,
  Slaughter Rhythm, Hollow Choir, Sawbone Coil, Glutton's Gut, Thresher Plate,
  Blood Moat, Meat Hook, Blood Debt
- Iron Lung re-armed per room in enterRoom; Slaughter Rhythm stamps kills
- Targeted smoke tests for the five stat-conditional effects; icons + docs
This commit is contained in:
MeatSlicer Developer committed 2026-07-30 15:44:31 +10:00
1 parent 4f46d54e99
commit a93802ebd9
23 files changed
+195 -13

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+25 -1
View File
@@ -1,6 +1,6 @@
# Items
All **60 passive items** in MeatSlicer, with exact effects pulled from `js/items.js` and the proc logic in `js/enemies.js` / `js/player.js`.
All **72 passive items** in MeatSlicer, with exact effects pulled from `js/items.js` and the proc logic in `js/enemies.js` / `js/player.js`.
---
@@ -77,6 +77,18 @@ All of these trigger from your weapon hits (orbital knives proc at half chance).
| **Ember Jar** | 20% chance to ignite | `+0.20` chance — burn for 1.8 s at `5 × dmgMul` DPS |
| **Chill Gland** | 18% chance to chill | `+0.18` chance — slows enemies (activates the previously unused `slowOnHit` stat) |
| **Cinder Sump** | +12% ignite, +12% acid | `igniteChance +0.12`, `acidOnHit +0.12` — dual-element hybrid |
| **Dead Weight** | +40% damage to enemies under 30% HP | `executeBonus +0.40` — checked in `damageEnemy` |
| **Cauterized Veins** | +15% ignite, +25% damage to burning enemies | `igniteChance +0.15`, `burnDamageBonus +0.25` |
| **Hollow Choir** | Every 4th shot fires a free extra volley | `choirEvery +1` — fires a second `fireWeapon` every 4th trigger pull |
| **Sawbone Coil** | Expiring bullets split into 2 shards | `sawboneCoil +1` — on bullet expiry, spawns 2 shards at 40% damage |
| **Glutton's Gut** | Hearts heal +1 extra; overheal becomes score | `gluttonGut +1` — heart heal `2 + tier`, full-HP score `25 + tier × 10` |
| **Slaughter Rhythm** | +4% fire rate per recent kill (cap +40%) | `slaughterRhythm +0.04` — kills stamp `killStamps`; rate bonus decays after 3 s |
| **Pain Engine** | +30% damage for 4s after being hit | `painEngine +0.30` — `dmgLiveMul` rises while `painEngineT` is active |
| **Thresher Plate** | Passive contact-damage aura | `thresherPlate +1` — ticks `6 × tier × dmgMul` on nearby enemies every 0.4 s |
| **Blood Moat** | Kills leave an acid pool | `bloodMoat +1` — on kill, spawns an acid hazard at the corpse |
| **Iron Lung** | The first hit each room is blocked | `ironLung +1` — re-armed by `enterRoom`; blocks one hit with a 0.5 s i-frame |
| **Meat Hook** | Kills yank nearby enemies to the corpse | `meatHook +1` — on kill, nearby enemies are pulled toward the body |
| **Blood Debt** | +35% damage, −½ heart container | `dmgMul ×1.35`, `maxHp −1` — the Rare trade-off item |
| **Acid Gland** | Hits may leave acid pools | `+0.12` chance (cap 65%) — pool radius 18 × sizeMul, 2.5 s, `5 × dmgMul` DPS |
| **Hook Rounds** | Hits pull enemies toward you | `+0.18` chance (cap 80%) — yanks the enemy toward you at speed 170 |
| **Sledge Rounds** | More knockback, hits may stun | `knockbackMul ×1.35`, `+0.08` stun chance (cap 75%) — 0.35 s stun, non-bosses only |
@@ -200,6 +212,18 @@ All of these trigger from your weapon hits (orbital knives proc at half chance).
| 58 | Volatile Bile | Rare | III | On-kill |
| 59 | Whipcord Tendon | Uncommon | V | Damage |
| 60 | Worm Gut | Rare | III | Survivability |
| 61 | Blood Debt | Rare | III | Damage |
| 62 | Blood Moat | Uncommon | IV | On-kill |
| 63 | Cauterized Veins | Uncommon | V | On-hit proc |
| 64 | Dead Weight | Uncommon | V | Damage |
| 65 | Glutton's Gut | Common | V | Survivability |
| 66 | Hollow Choir | Rare | III | Shot modifier |
| 67 | Iron Lung | Rare | III | Survivability |
| 68 | Meat Hook | Uncommon | IV | On-kill |
| 69 | Pain Engine | Rare | III | Damage |
| 70 | Sawbone Coil | Uncommon | V | Shot modifier |
| 71 | Slaughter Rhythm | Rare | III | Fire rate |
| 72 | Thresher Plate | Uncommon | V | Survivability |
## Synergy notes
+14
View File
@@ -89,6 +89,20 @@ function updateBullets(dt) {
if (b.behavior === 'cone' && chance(0.35)) {
G.hazards.push({ kind: 'acid', x: b.x, y: b.y, r: 24 * (b.sizeMul || 1), life: 3.5, t: 0, dps: b.dmg * 1.5 });
}
// Sawbone Coil: bullets split into 2 shards on expiry
if (p.stats.sawboneCoil > 0 && !b.shard && b.behavior !== 'lob' && b.behavior !== 'lob_trap' && b.behavior !== 'lob_swarm') {
for (let k = 0; k < 2; k++) {
const sa = rand(0, TAU);
G.bullets.push({
x: b.x, y: b.y, ang: sa,
vx: Math.cos(sa) * 260 * p.stats.shotSpeedMul, vy: Math.sin(sa) * 260 * p.stats.shotSpeedMul,
r: 3 * p.stats.sizeMul, dmg: b.dmg * 0.4, pierce: 0, bounce: p.stats.bounce,
life: 0.4 * p.stats.rangeMul, t: 0, behavior: 'bullet', sprite: 'bullet_bone',
homing: p.stats.homing ? 1.6 + p.stats.homing * 0.7 : 0, shard: true, sizeMul: p.stats.sizeMul,
});
}
spawnSpark(b.x, b.y, rand(0, TAU));
}
}
// lobbed projectiles trigger on expiry (reached target)
+21
View File
@@ -102,6 +102,10 @@ function damageEnemy(e, dmg, ang, knockback, opts) {
const o = opts || {};
const crit = !!(p && !o.noCrit && chance(p.stats.crit || 0));
if (crit) dmg *= p.stats.critMul || 2;
// Dead Weight: execute wounded enemies
if (p && p.stats.executeBonus > 0 && e.maxHp > 0 && e.hp / e.maxHp < 0.30) dmg *= 1 + p.stats.executeBonus;
// Cauterized Veins: bonus damage to burning enemies
if (p && p.stats.burnDamageBonus > 0 && e.burnT > 0) dmg *= 1 + p.stats.burnDamageBonus;
const mitigated = e.dmgTakenMul !== undefined && e.dmgTakenMul < 1;
dmg *= e.dmgTakenMul === undefined ? 1 : e.dmgTakenMul;
e.hp -= dmg;
@@ -222,6 +226,7 @@ function killEnemy(e, ang) {
const p = G.player;
if (p.stats.frenzy > 0) p.frenzyT = Math.max(p.frenzyT || 0, 3);
G.kills++;
if (p.stats.slaughterRhythm > 0) { p.killStamps = p.killStamps || []; p.killStamps.push(G.time); }
addScore(e.boss ? 500 : (e.elite ? 40 : 10));
spawnBlood(e.x, e.y, ang || rand(0, TAU), e.boss ? 40 : (e.elite ? 20 : 10), e.boss || e.elite);
spawnGibs(e.x, e.y, e.boss ? 16 : (e.elite ? 8 : 4), e.boss || e.elite);
@@ -236,6 +241,22 @@ function killEnemy(e, ang) {
if (e.boss) { onBossDeath(e); return; }
// Blood Moat: kills leave an acid pool
if (p.stats.bloodMoat > 0 && G.hazards.length < 80) {
G.hazards.push({ kind: 'acid', x: e.x, y: e.y, r: 16 * p.stats.sizeMul, life: 2.5, t: 0, dps: 5 * p.stats.dmgMul * p.stats.dmgLiveMul });
}
// Meat Hook: kills yank nearby enemies to the corpse
if (p.stats.meatHook > 0) {
const yankR = 120 + p.stats.meatHook * 15;
for (const o of G.enemies) {
if (o === e || o.hp <= 0 || o.boss) continue;
if (dist2(o.x, o.y, e.x, e.y) < yankR * yankR) {
const a = angleTo(o.x, o.y, e.x, e.y);
o.vx += Math.cos(a) * 240; o.vy += Math.sin(a) * 240;
}
}
}
// Volatile Bile: kills explode — radius and damage scale with tier
if (p.stats.explodeOnKill > 0) {
const rad = 60 + p.stats.explodeOnKill * 15;
+14
View File
@@ -75,6 +75,20 @@ const ITEMS = {
rimedfang: { name: 'Rimed Fang', desc: '+4% crit chance, +25% crit damage', rarity: 'common', cap: 6, apply: s => { s.crit += 0.04; s.critMul += 0.25; } },
butcherstwine: { name: 'Butcher\'s Twine', desc: '+12% bleed, +8% fire rate', rarity: 'uncommon', cap: 5, apply: s => { s.bleed += 0.12; s.rateMul *= 1.08; } },
cindersump: { name: 'Cinder Sump', desc: '+12% ignite, +12% acid', rarity: 'uncommon', cap: 5, apply: s => { s.igniteChance += 0.12; s.acidOnHit += 0.12; } },
// ---- phase 3 additions (small engine hooks) ----
deadweight: { name: 'Dead Weight', desc: '+40% damage to enemies under 30% HP', rarity: 'uncommon', cap: 5, apply: s => { s.executeBonus += 0.40; } },
cauterizedveins:{ name: 'Cauterized Veins', desc: '+15% ignite, +25% damage to burning enemies', rarity: 'uncommon', cap: 5, apply: s => { s.igniteChance += 0.15; s.burnDamageBonus += 0.25; } },
hollowchoir: { name: 'Hollow Choir', desc: 'Every 4th shot fires a free extra volley', rarity: 'rare', cap: 3, apply: s => { s.choirEvery += 1; } },
sawbonecoil: { name: 'Sawbone Coil', desc: 'Expiring bullets split into 2 shards', rarity: 'uncommon', cap: 5, apply: s => { s.sawboneCoil += 1; } },
gluttonsgut: { name: "Glutton's Gut", desc: 'Hearts heal +1 extra; overheal becomes score', rarity: 'common', cap: 5, apply: s => { s.gluttonGut += 1; } },
slaughterrhythm:{ name: 'Slaughter Rhythm', desc: '+4% fire rate per recent kill (cap +40%)', rarity: 'rare', cap: 3, apply: s => { s.slaughterRhythm += 0.04; } },
painengine: { name: 'Pain Engine', desc: '+30% damage for 4s after being hit', rarity: 'rare', cap: 3, apply: s => { s.painEngine += 0.30; } },
thresherplate: { name: 'Thresher Plate', desc: 'Passive contact-damage aura', rarity: 'uncommon', cap: 5, apply: s => { s.thresherPlate += 1; } },
bloodmoat: { name: 'Blood Moat', desc: 'Kills leave an acid pool', rarity: 'uncommon', cap: 4, apply: s => { s.bloodMoat += 1; } },
ironlung: { name: 'Iron Lung', desc: 'The first hit each room is blocked', rarity: 'rare', cap: 3, apply: s => { s.ironLung += 1; } },
meathook: { name: 'Meat Hook', desc: 'Kills yank nearby enemies to the corpse', rarity: 'uncommon', cap: 4, apply: s => { s.meatHook += 1; } },
blooddebt: { name: 'Blood Debt', desc: '+35% damage, −½ heart container', rarity: 'rare', cap: 3, apply: (s, p) => { s.dmgMul *= 1.35; s.maxHp = Math.max(1, s.maxHp - 1); p.hp = Math.min(p.hp, s.maxHp); } },
};
const ITEM_LEVEL_CAP = 9;
+6 -3
View File
@@ -69,11 +69,14 @@ function collectPickup(k) {
gainXP(k.v * p.stats.xpMul);
Sfx.gem();
break;
case 'heart':
if (p.hp < p.stats.maxHp) { healPlayer(2); spawnText(p.x, p.y, '+2', '#d92038'); }
else addScore(25);
case 'heart': {
// Glutton's Gut: hearts heal +1 extra per tier; overheal becomes score
const healAmt = 2 + (p.stats.gluttonGut || 0);
if (p.hp < p.stats.maxHp) { healPlayer(healAmt); spawnText(p.x, p.y, '+' + healAmt, '#d92038'); }
else addScore(25 + (p.stats.gluttonGut || 0) * 10);
Sfx.heart();
break;
}
case 'ammo': {
// Generic rounds refill every finite-ammo weapon the butcher is carrying,
// including a special weapon currently holstered behind the Bone Popper.
+44 -2
View File
@@ -22,6 +22,10 @@ function initPlayer() {
roomHeal: 0, roomHealChance: 0, thorns: 0, retaliate: 0, sizeMul: 1, mortar: 0, frenzy: 0,
orbSpeedMul: 1, orbDmgMul: 1, overShield: 0,
rerolls: 0, rerollPerLevel: 0,
// phase-3 conditional / hook passives
dmgLiveMul: 1, executeBonus: 0, burnDamageBonus: 0, choirEvery: 0, sawboneCoil: 0,
gluttonGut: 0, slaughterRhythm: 0, painEngine: 0, thresherPlate: 0, bloodMoat: 0,
ironLung: 0, meatHook: 0, bloodDebt: 0,
maxHp: 6,
},
};
@@ -100,7 +104,7 @@ 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 * frenzyMul;
const effectiveRate = st.rateMul * (1 + (st.rhythmRateBonus || 0)) * frenzyMul;
const rate = w.interval / effectiveRate;
if (w.behavior === 'saw') {
@@ -123,6 +127,11 @@ function updatePlayer(dt) {
} else {
if (Input.mdown && p.fireT <= 0 && p.weapon.ammo > 0) {
fireWeapon(p, w);
// Hollow Choir: every 4th shot fires a free extra volley
if (st.choirEvery > 0) {
p.choirCount = (p.choirCount || 0) + 1;
if (p.choirCount >= 4) { p.choirCount = 0; fireWeapon(p, w); }
}
p.recoil = Math.min(1, p.recoil + (w.behavior === 'slam' ? 1 : 0.45));
p.muzzleT = 0.07;
p.attackT = 0.4; p.actionT = 0;
@@ -154,7 +163,7 @@ function updatePlayer(dt) {
if (e.orbT > 0) continue;
if (dist2(ox, oy, e.x, e.y) < (12 + e.r) * (12 + e.r)) {
e.orbT = 0.35;
damageEnemy(e, 12 * st.dmgMul * st.orbDmgMul, a + Math.PI / 2, true, {
damageEnemy(e, 12 * st.dmgMul * st.dmgLiveMul * st.orbDmgMul, a + Math.PI / 2, true, {
source: 'player', procScale: 0.5, procMagnitudeScale: 0.5,
});
spawnBlood(e.x, e.y, a, 3);
@@ -165,6 +174,30 @@ function updatePlayer(dt) {
// open queued level-ups
if (G.pendingLevelups > 0 && G.mode === 'play') openPerkDraft();
// ---- phase-3 live multipliers & auras ----
// Slaughter Rhythm: +rate per recent kill, Pain Engine: +dmg after being hit
p.killStamps = (p.killStamps || []).filter(t => G.time - t < 3);
const rhythm = st.slaughterRhythm > 0 ? Math.min(p.killStamps.length * st.slaughterRhythm, 0.40) : 0;
st.rhythmRateBonus = rhythm;
if (p.painEngineT > 0) p.painEngineT -= dt;
st.dmgLiveMul = p.painEngineT > 0 ? 1 + st.painEngine : 1;
// Thresher Plate: passive contact-damage aura (no need to be hit)
if (st.thresherPlate > 0) {
p.thresherTick = (p.thresherTick || 0) - dt;
if (p.thresherTick <= 0) {
p.thresherTick = 0.4;
const range = 46 * Math.sqrt(st.sizeMul);
for (const e of G.enemies) {
if (e.hp <= 0 || e.boss) continue;
if (dist2(e.x, e.y, p.x, p.y) < (range + e.r) * (range + e.r)) {
damageEnemy(e, 6 * st.thresherPlate * st.dmgMul * st.dmgLiveMul, angleTo(p.x, p.y, e.x, e.y), true, { noProc: true });
}
}
}
}
// Iron Lung room-block cooldown flag is set in enterRoom
}
function armorBlockChance(rating) {
@@ -183,6 +216,14 @@ function pressureRelief(dmgTaken) {
function hurtPlayer(dmg, ang, attacker) {
const p = G.player;
if (p.invT > 0 || p.hp <= 0 || G.mode !== 'play') return;
// Iron Lung: the first hit each room is blocked
if (p.ironLungReady) {
p.ironLungReady = false;
p.invT = 0.5; p.hitT = 0.1;
spawnText(p.x, p.y - 20, 'IRON LUNG', '#9fb4bd');
addShake(2); Sfx.shieldUp();
return;
}
if (p.stats.armor > 0 && chance(armorBlockChance(p.stats.armor))) {
p.invT = 0.2;
p.hitT = 0.1;
@@ -201,6 +242,7 @@ function hurtPlayer(dmg, ang, attacker) {
p.hp -= reduced;
p.hitT = 0.18;
p.invT = 0.9 + p.stats.invBonus;
if (reduced > 0 && p.stats.painEngine > 0) p.painEngineT = 4;
G.flash = 0.35;
addShake(7);
Sfx.hurt();
+2
View File
@@ -67,6 +67,8 @@ function enterRoom(gx, gy) {
// spawn protection: brief invincibility walking into a fresh room
if (G.player) G.player.invT = Math.max(G.player.invT, 1.0);
// Iron Lung: re-arm the once-per-room hit block
if (G.player && G.player.stats.ironLung > 0) G.player.ironLungReady = true;
if (!room.visited) {
room.visited = true;
+3
View File
@@ -39,6 +39,9 @@ const SPRITE_MANIFEST = [
'i_chillgland', 'i_hookedsinew', 'i_gyroscopicribs', 'i_marrowpiston', 'i_splitcortex',
'i_gristlecord', 'i_renderedfat', 'i_whipcordtendon', 'i_rusteddiadem', 'i_gorgedtick',
'i_bonemealpowder', 'i_rimedfang', 'i_butcherstwine', 'i_cindersump',
'i_deadweight', 'i_cauterizedveins', 'i_hollowchoir', 'i_sawbonecoil', 'i_gluttonsgut',
'i_slaughterrhythm', 'i_painengine', 'i_thresherplate', 'i_bloodmoat', 'i_ironlung',
'i_meathook', 'i_blooddebt',
// full 64px/128px eight-direction action atlases
'player_sheet', 'player_legs_sheet',
'enemy_shambler_sheet', 'enemy_runner_sheet', 'enemy_spitter_sheet',
+3 -3
View File
@@ -43,7 +43,7 @@ function fireWeapon(p, w) {
const ang = p.aim;
const muzzle = w.muzzle || 35;
const bx = p.x + Math.cos(ang) * muzzle, by = p.y + Math.sin(ang) * muzzle;
const dmgMul = st.dmgMul;
const dmgMul = st.dmgMul * st.dmgLiveMul;
const spdMul = st.shotSpeedMul;
spawnMuzzleFx(bx, by, ang, w.behavior);
@@ -144,7 +144,7 @@ function sawTick(p, def, inst, dt) {
for (const e of G.enemies) {
if (dist2(cx, cy, e.x, e.y) < (radius + e.r) * (radius + e.r)) {
const tickScale = dt / Math.max(0.01, def.interval || 0.1);
damageEnemy(e, def.dmg * st.dmgMul * st.rateMul * inert * tickScale, a, false, {
damageEnemy(e, def.dmg * st.dmgMul * st.dmgLiveMul * st.rateMul * inert * tickScale, a, false, {
source: 'player', procScale: tickScale, procIntervalScale: tickScale,
});
hitAny = true;
@@ -166,7 +166,7 @@ function fireBeam(p, def, charge) {
for (let i = 1; i <= st.rear; i++) angles.push(p.aim + Math.PI + (i - (st.rear + 1) / 2) * 0.18);
const len = 900 * st.rangeMul;
const inert = 1 + (st.pierce + st.bounce + st.homing) * 0.06;
const dmg = def.dmg * st.dmgMul * (0.4 + 0.6 * charge) * inert;
const dmg = def.dmg * st.dmgMul * st.dmgLiveMul * (0.4 + 0.6 * charge) * inert;
const width = 14 * Math.sqrt(st.sizeMul);
for (const ang of angles) {
const x2 = p.x + Math.cos(ang) * len, y2 = p.y + Math.sin(ang) * len;
+51 -4
View File
@@ -318,9 +318,9 @@ step(30, 16);
console.log('== expanded perk and item rosters ==');
{
check('41 new items added (14 phase-2 passives)', Object.keys(ctx.ITEMS).length === 60);
check('41 new items added (14 phase-2 + 12 phase-3 passives)', Object.keys(ctx.ITEMS).length === 72);
const newPerks = ['critbone', 'critmeat', 'flensing', 'ember', 'frostbile', 'heavyhand', 'thickhide', 'secondwind', 'scrapfeed', 'boneknit', 'spiteflesh', 'carrion', 'sinew'];
const newItems = ['chainsinew', 'mortarbone', 'bloatrounds', 'marrowglut', 'hollowneedle', 'bloodshoteye', 'flayerkiss', 'emberjar', 'acidgland', 'hookrounds', 'sledgerounds', 'graftedtrigger', 'deadmanswitch', 'orbitcrown', 'tannedhide', 'deadmansclock', 'hollowbones', 'boneplate', 'wormgut', 'spinecage', 'secondstomach', 'spitewell', 'twinhearts', 'brassmagazine', 'crowbait', 'gorgingleech', 'rerollrib', 'chillgland', 'hookedsinew', 'gyroscopicribs', 'marrowpiston', 'splitcortex', 'gristlecord', 'renderedfat', 'whipcordtendon', 'rusteddiadem', 'gorgedtick', 'bonemealpowder', 'rimedfang', 'butcherstwine', 'cindersump'];
const newItems = ['chainsinew', 'mortarbone', 'bloatrounds', 'marrowglut', 'hollowneedle', 'bloodshoteye', 'flayerkiss', 'emberjar', 'acidgland', 'hookrounds', 'sledgerounds', 'graftedtrigger', 'deadmanswitch', 'orbitcrown', 'tannedhide', 'deadmansclock', 'hollowbones', 'boneplate', 'wormgut', 'spinecage', 'secondstomach', 'spitewell', 'twinhearts', 'brassmagazine', 'crowbait', 'gorgingleech', 'rerollrib', 'chillgland', 'hookedsinew', 'gyroscopicribs', 'marrowpiston', 'splitcortex', 'gristlecord', 'renderedfat', 'whipcordtendon', 'rusteddiadem', 'gorgedtick', 'bonemealpowder', 'rimedfang', 'butcherstwine', 'cindersump', 'deadweight', 'cauterizedveins', 'hollowchoir', 'sawbonecoil', 'gluttonsgut', 'slaughterrhythm', 'painengine', 'thresherplate', 'bloodmoat', 'ironlung', 'meathook', 'blooddebt'];
check('all new perks have manifest icons or fallbacks', newPerks.every(id => ctx.SPRITE_MANIFEST.includes('perk_' + id)));
check('all new items have manifest icons or fallbacks', newItems.every(id => ctx.SPRITE_MANIFEST.includes('i_' + id)));
check('new roster entries all have names, descriptions and effects',
@@ -498,6 +498,53 @@ console.log('== tier-scaled items + duplicate favoring ==');
check('item rolls favor owned items (' + ownedHits + '/200)', ownedHits >= 40);
}
console.log('== phase-3 hook passives ==');
{
const p = ctx.G.player;
const s = p.stats;
// Dead Weight: execute bonus on wounded enemies (isolate stats so no leftover items leak)
const savedStats = s;
p.stats = Object.assign({}, s, { executeBonus: 0.40, burnDamageBonus: 0, crit: 0, critMul: 2 });
const wounded = ctx.makeEnemy('shambler', 500, 300, 1, false); wounded.hp = wounded.maxHp = 100; wounded.hp = 20; // 20% of maxHp
const healthy = ctx.makeEnemy('shambler', 500, 300, 1, false); healthy.hp = healthy.maxHp = 100;
const wBefore = wounded.hp, hBefore = healthy.hp;
ctx.damageEnemy(wounded, 10, 0, false, { noCrit: true });
ctx.damageEnemy(healthy, 10, 0, false, { noCrit: true });
check('Dead Weight executes wounded enemies', Math.abs((wBefore - wounded.hp) - 14) < 0.01 && Math.abs((hBefore - healthy.hp) - 10) < 0.01);
p.stats = savedStats;
// Cauterized Veins: bonus damage to burning enemies
const savedBurn = s.burnDamageBonus;
s.burnDamageBonus = 0.25;
const burner = ctx.makeEnemy('shambler', 500, 300, 1, false); burner.burnT = 1;
const bBefore = burner.hp;
ctx.damageEnemy(burner, 10, 0, false, { noCrit: true });
check('Cauterized Veins boosts damage on burning enemies', Math.abs((bBefore - burner.hp) - 12.5) < 0.01);
s.burnDamageBonus = savedBurn;
ctx.G.enemies.length = 0;
// Pain Engine: dmgLiveMul rises after being hit
const savedPain = s.painEngine, savedArmor = s.armor, savedMax = s.maxHp;
s.painEngine = 0.30; s.armor = 0; s.maxHp = 999; p.hp = 999; p.invT = 0; p.painEngineT = 0; ctx.G.mode = 'play';
ctx.hurtPlayer(1, 0);
check('Pain Engine arms after being hit', p.painEngineT > 0);
s.painEngine = savedPain; s.armor = savedArmor; s.maxHp = savedMax; p.invT = 1; p.painEngineT = 0;
// Iron Lung: first hit in a room is blocked
const savedLung = s.ironLung;
s.ironLung = 1; p.ironLungReady = true; p.invT = 0; const hpBeforeLung = p.hp;
ctx.hurtPlayer(1, 0);
check('Iron Lung blocks the first room hit', p.hp === hpBeforeLung && p.ironLungReady === false);
s.ironLung = savedLung; p.invT = 1;
// Slaughter Rhythm: recent kills grant fire rate
const savedRhythm = s.slaughterRhythm;
s.slaughterRhythm = 0.04; p.killStamps = [ctx.G.time, ctx.G.time, ctx.G.time];
p.fireT = 0; ctx.updatePlayer(0.016);
check('Slaughter Rhythm grants live fire rate', (s.rhythmRateBonus || 0) > 0);
s.slaughterRhythm = savedRhythm; p.killStamps = [];
}
console.log('== all weapons fire without crashing ==');
for (const wid of Object.keys(ctx.WEAPONS)) {
ctx.G.player.weapon = { id: wid, ammo: 50 };
@@ -937,8 +984,8 @@ console.log('== item stickiness ramps with owned count ==');
console.log('== help manual grows with the implant roster ==');
{
check('9 help pages (3 implant pages for 60 items)', ctx.HELP_PAGES.length === 9);
check('9 help renderers', ctx.HELP_RENDERERS.length === 9);
check('10 help pages (4 implant pages for 72 items)', ctx.HELP_PAGES.length === 10);
check('10 help renderers', ctx.HELP_RENDERERS.length === 10);
ctx.G.player = ctx.G.player || {};
const fctx = fakeCtx();
let renderOK = true;
+12
View File
@@ -279,6 +279,18 @@ ITEMS = {
"rimedfang": "a long predatory fang crusted with white frost and a steel collar",
"butcherstwine": "a spool of blood-stained butcher twine with a frayed loose end",
"cindersump": "a smouldering iron sump grate with orange cinders and green acid drips",
"deadweight": "a heavy iron butcher weight crushing a pale bone",
"cauterizedveins": "a forearm cross-section with glowing cauterized orange veins",
"hollowchoir": "a hollow bone pipe organ with three small singing mouths",
"sawbonecoil": "a coiled spring made of interlocking bone saw teeth",
"gluttonsgut": "a distended stomach with a second mouth and spilled half-digested hearts",
"slaughterrhythm": "a metronome with a pendulum made of a bloody cleaver",
"painengine": "a small iron engine with a pressure gauge and a screaming flesh piston",
"thresherplate": "a spiked iron threshing plate bristling with rotating barbs",
"bloodmoat": "a ring-shaped moat of dark blood with a bone drawbridge",
"ironlung": "a riveted iron lung with bellows and a steel windpipe",
"meathook": "a huge rusted slaughterhouse meat hook on a taut chain",
"blooddebt": "a parchment contract signed in blood with a heart seal",
}
for iid, subject in ITEMS.items():
SPECS["i_" + iid] = Spec(subject + ", isolated inventory object, strong top-left light, no medallion, no icon background", 32)