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
No files matched your search
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 948 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||