Phase 4: 10 boss-exclusive legendaries + ammo/HP balance fixes

Legendaries (cap 1, boss-only): Butcher's Oath, Second Skin, Twin Sidearm,
Crimson Metronome, Abattoir Engine, Gore Crown, Thousand Teeth, Hollow Father,
The Last Cut, Meat Grinder. Once-per-floor flags reset in nextFloor.

Balance fixes: Ghoul Heart now +4 HP (2 full hearts); Brass Magazine ammo
efficiency uses the same diminishing additive formula as Scrap Feed.
This commit is contained in:
MeatSlicer Developer committed 2026-07-30 16:06:20 +10:00
1 parent a93802ebd9
commit f9552326c2
19 files changed
+150 -11

No files matched your search

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: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+12 -2
View File
@@ -109,7 +109,7 @@ All of these trigger from your weapon hits (orbital knives proc at half chance).
| Item | Description | Exact effect per tier |
|---|---|---|
| **Iron Stomach** | +½ heart container, heal ½ heart | `maxHp +1`, heal 1 |
| **Ghoul Heart** | +2 max HP, heal 2 | `maxHp +2`, heal 2 |
| **Ghoul Heart** | +4 max HP, heal 4 | `maxHp +4`, heal 4 — a full 2 hearts |
| **Twin Hearts** | +1 max heart, heal to full | `maxHp +2`, `hp = maxHp` — the premium Rare HP item |
| **Rendered Fat** | +½ heart, +4% armor | `maxHp +1`, `armor +0.04`, heal 1 — clean hybrid HP/armor |
| **Gristle Cord** | +0.12s hurt immunity, +4% armor | `invBonus +0.12`, `armor +0.04` |
@@ -139,7 +139,7 @@ All of these trigger from your weapon hits (orbital knives proc at half chance).
| **Magnet Maw** | +60% pickup radius | `magnet ×1.6` — magnet radius 46 × magnet px |
| **Gorging Leech** | +20% XP gained | `xpMul ×1.20` |
| **Reroll Rib** | +1 perk reroll each level | `rerollPerLevel +1` and +1 reroll immediately |
| **Brass Magazine** | 15% less ammo use, +20% ammo found | `ammoEff ×1.15`, `ammoPickupMul ×1.20` (the ×1.15 stays multiplicative; the **Scrap Feed** perk's additive stacks sit underneath it) |
| **Brass Magazine** | +10% ammo efficiency, +20% ammo found | `ammoEff +0.10` (same diminishing additive formula as **Scrap Feed**, so the saving never reaches 50%), `ammoPickupMul ×1.20` |
| **Hollow Bones** | +14% move speed | `speedMul ×1.14` (178 → ~203 px/s at tier I) |
| **Gorged Tick** | +15% XP, +10% luck | `xpMul ×1.15`, `luck +0.10` |
| **Hooked Sinew** | +30% pickup pull speed, +10% radius | `magnetPull ×1.30`, `magnet ×1.10` (activates the `magnetPull` stat) |
@@ -224,6 +224,16 @@ All of these trigger from your weapon hits (orbital knives proc at half chance).
| 70 | Sawbone Coil | Uncommon | V | Shot modifier |
| 71 | Slaughter Rhythm | Rare | III | Fire rate |
| 72 | Thresher Plate | Uncommon | V | Survivability |
| 73 | Abattoir Engine | Legendary | I | Utility |
| 74 | Butcher's Oath | Legendary | I | Damage |
| 75 | Crimson Metronome | Legendary | I | Fire rate |
| 76 | Gore Crown | Legendary | I | On-kill |
| 77 | Hollow Father | Legendary | I | Utility |
| 78 | Meat Grinder | Legendary | I | Survivability |
| 79 | Second Skin | Legendary | I | Survivability |
| 80 | The Last Cut | Legendary | I | Damage |
| 81 | Thousand Teeth | Legendary | I | Shot modifier |
| 82 | Twin Sidearm | Legendary | I | Shot modifier |
## Synergy notes
+23
View File
@@ -148,6 +148,23 @@ function procOnHit(e, dmg, ang, opts) {
const magnitudeScale = opts.procMagnitudeScale === undefined ? 1 : opts.procMagnitudeScale;
const intervalScale = opts.procIntervalScale === undefined ? 1 : opts.procIntervalScale;
const procDamage = dmg / Math.max(0.05, intervalScale) * magnitudeScale;
// Thousand Teeth: bullets shatter into 6 extra shards that can crit.
// Shards inherit the shard flag so they don't recurse. The −40% base bullet
// damage is applied as a global dmgMul penalty in the item's apply().
if (s.thousandTeeth > 0 && !(opts.bullet && opts.bullet.shard) && chance(clamp(chanceScale, 0, 1))) {
for (let i = 0; i < 6; i++) {
const sa = rand(0, TAU);
const shardCrit = chance(s.crit || 0);
G.bullets.push({
x: e.x, y: e.y, ang: sa,
vx: Math.cos(sa) * 300 * s.shotSpeedMul, vy: Math.sin(sa) * 300 * s.shotSpeedMul,
r: 3 * s.sizeMul, dmg: procDamage * 0.4 * (shardCrit ? (s.critMul || 2) : 1), pierce: 0, bounce: s.bounce,
life: 0.35 * s.rangeMul, t: 0, behavior: 'bullet', sprite: 'bullet_bone',
homing: s.homing ? 1.6 + s.homing * 0.7 : 0, shard: true, sizeMul: s.sizeMul,
});
}
spawnSpark(e.x, e.y, ang);
}
if (s.splinter > 0 && !(opts.bullet && opts.bullet.shard) && chance(clamp(chanceScale, 0, 1))) {
const count = Math.min(18, Math.floor(s.splinter));
for (let i = 0; i < count; i++) {
@@ -241,6 +258,12 @@ function killEnemy(e, ang) {
if (e.boss) { onBossDeath(e); return; }
// Gore Crown: free nova on every kill
if (p.stats.goreCrown > 0) {
areaDamage(e.x, e.y, 70 * Math.sqrt(p.stats.sizeMul), 8 * p.stats.dmgMul * p.stats.dmgLiveMul, true, { noProc: true, noCrit: true });
spawnExplosionFx(e.x, e.y, 60);
}
// 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 });
+14 -2
View File
@@ -21,7 +21,7 @@ const ITEMS = {
magnetmaw: { name: 'Magnet Maw', desc: '+60% pickup radius', rarity: 'common', cap: 3, apply: s => { s.magnet *= 1.6; } },
gorgingleech: { name: 'Gorging Leech', desc: '+20% XP gained', rarity: 'common', cap: 4, apply: s => { s.xpMul *= 1.20; } },
hollowbones: { name: 'Hollow Bones', desc: '+14% move speed', rarity: 'common', cap: 6, apply: s => { s.speedMul *= 1.14; } },
brassmagazine: { name: 'Brass Magazine', desc: '15% less ammo use, +20% ammo found', rarity: 'common', cap: 5, apply: s => { s.ammoEff *= 1.15; s.ammoPickupMul *= 1.20; } },
brassmagazine: { name: 'Brass Magazine', desc: '+10% ammo efficiency, +20% ammo found', rarity: 'common', cap: 5, apply: s => { s.ammoEff += 0.10; s.ammoPickupMul *= 1.20; } },
ironstomach: { name: 'Iron Stomach', desc: '+½ heart container, heal ½ heart', rarity: 'common', cap: 8, apply: (s, p) => { s.maxHp += 1; p.hp = Math.min(s.maxHp, p.hp + 1); } },
tannedhide: { name: 'Tanned Hide', desc: '+8% chance to ignore damage', rarity: 'common', cap: 6, apply: s => { s.armor += 0.08 / 0.92; } },
bloodlust: { name: 'Bloodlust', desc: 'Kills may drop bonus XP', rarity: 'common', cap: 5, apply: s => { s.bloodlust = (s.bloodlust || 0) + 0.12; } },
@@ -43,7 +43,7 @@ const ITEMS = {
spinecage: { name: 'Spine Cage', desc: 'Contact attackers take damage', rarity: 'uncommon', cap: 5, apply: s => { s.thorns += 8; } },
spitewell: { name: 'Spite Well', desc: 'Taking damage releases a blood nova', rarity: 'uncommon', cap: 5, apply: s => { s.retaliate += 10; } },
orbitcrown: { name: 'Orbit Crown', desc: '+25% orbital speed and damage', rarity: 'uncommon', cap: 5, apply: s => { s.orbSpeedMul *= 1.25; s.orbDmgMul *= 1.25; } },
ghoulheart: { name: 'Ghoul Heart', desc: '+2 max HP, heal 2', rarity: 'uncommon', cap: 5, apply: (s, p) => { s.maxHp += 2; p.hp = Math.min(s.maxHp, p.hp + 2); } },
ghoulheart: { name: 'Ghoul Heart', desc: '+4 max HP, heal 4', rarity: 'uncommon', cap: 5, apply: (s, p) => { s.maxHp += 4; p.hp = Math.min(s.maxHp, p.hp + 4); } },
// ---- rare ----
splittongue: { name: 'Split Tongue', desc: 'Twin parallel shot', rarity: 'rare', cap: 3, apply: s => { s.split += 1; } },
@@ -89,6 +89,18 @@ const ITEMS = {
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); } },
// ---- phase 4: boss-exclusive legendaries (cap 1, curses allowed) ----
butchersoath: { name: "Butcher's Oath", desc: '+80% damage, but max HP set to 2', rarity: 'legendary', cap: 1, apply: (s, p) => { s.dmgMul *= 1.8; s.maxHp = Math.min(s.maxHp, 2); p.hp = Math.min(p.hp, s.maxHp); } },
secondskin: { name: 'Second Skin', desc: 'Revive once per floor at ½ heart', rarity: 'legendary', cap: 1, apply: s => { s.secondSkin += 1; } },
twinsidearm: { name: 'Twin Sidearm', desc: 'Bone Popper double-taps; specials burn 2× ammo', rarity: 'legendary', cap: 1, apply: s => { s.twinSidearm += 1; } },
crimsonmetronome:{ name: 'Crimson Metronome', desc: '+60% fire rate; every 8th shot costs ½ heart', rarity: 'legendary', cap: 1, apply: s => { s.rateMul *= 1.6; s.crimsonMetronome += 1; } },
abattoirengine:{ name: 'Abattoir Engine', desc: 'Pressure rises 2× faster; score and luck ×1.5', rarity: 'legendary', cap: 1, apply: s => { s.abattoirEngine += 1; s.luck += 0.5; } },
gorecrown: { name: 'Gore Crown', desc: 'Free nova on every kill, −25% damage', rarity: 'legendary', cap: 1, apply: s => { s.goreCrown += 1; s.dmgMul *= 0.75; } },
thousandteeth: { name: 'Thousand Teeth', desc: '+6 shards that can crit, −40% base bullet damage', rarity: 'legendary', cap: 1, apply: s => { s.thousandTeeth += 1; s.dmgMul *= 0.6; } },
hollowfather: { name: 'Hollow Father', desc: '3 damage-scaling orbitals, −25% fire rate', rarity: 'legendary', cap: 1, apply: s => { s.orbitals += 3; s.hollowFather += 1; s.rateMul *= 0.75; } },
thelastcut: { name: 'The Last Cut', desc: 'At ½ heart: ×3 damage, +1s immunity', rarity: 'legendary', cap: 1, apply: s => { s.theLastCut += 1; } },
meatgrinder: { name: 'Meat Grinder', desc: '12 dps aura within 90px, +1 damage taken per hit', rarity: 'legendary', cap: 1, apply: s => { s.meatGrinder += 1; } },
};
const ITEM_LEVEL_CAP = 9;
+50 -1
View File
@@ -26,6 +26,9 @@ function initPlayer() {
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,
// phase-4 legendaries
secondSkin: 0, twinSidearm: 0, crimsonMetronome: 0, abattoirEngine: 0,
goreCrown: 0, thousandTeeth: 0, hollowFather: 0, theLastCut: 0, meatGrinder: 0,
maxHp: 6,
},
};
@@ -127,16 +130,23 @@ function updatePlayer(dt) {
} else {
if (Input.mdown && p.fireT <= 0 && p.weapon.ammo > 0) {
fireWeapon(p, w);
// Twin Sidearm: the Bone Popper double-taps
if (st.twinSidearm > 0 && p.weapon.id === 'bonepopper') 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); }
}
// Crimson Metronome: every 8th shot costs ½ heart
if (st.crimsonMetronome > 0) {
p.metronomeCount = (p.metronomeCount || 0) + 1;
if (p.metronomeCount >= 8) { p.metronomeCount = 0; p.hp = Math.max(0.5, p.hp - 1); spawnText(p.x, p.y - 14, '-½', '#e2472f'); }
}
p.recoil = Math.min(1, p.recoil + (w.behavior === 'slam' ? 1 : 0.45));
p.muzzleT = 0.07;
p.attackT = 0.4; p.actionT = 0;
p.fireT = rate;
if (p.weapon.ammo !== Infinity) p.weapon.ammo -= 1 / st.ammoEff;
if (p.weapon.ammo !== Infinity) p.weapon.ammo -= (st.twinSidearm > 0 ? 2 : 1) / st.ammoEff;
Sfx.shoot(w);
if (w.behavior === 'slam') addShake(5);
}
@@ -182,6 +192,23 @@ function updatePlayer(dt) {
st.rhythmRateBonus = rhythm;
if (p.painEngineT > 0) p.painEngineT -= dt;
st.dmgLiveMul = p.painEngineT > 0 ? 1 + st.painEngine : 1;
// The Last Cut: while at ½ heart, ×3 damage
if (st.theLastCut > 0 && p.lastCutActive && p.hp <= 1) st.dmgLiveMul *= 3;
// Meat Grinder: passive 12 dps aura within 90px
if (st.meatGrinder > 0) {
p.meatGrinderTick = (p.meatGrinderTick || 0) - dt;
if (p.meatGrinderTick <= 0) {
p.meatGrinderTick = 0.5;
for (const e of G.enemies) {
if (e.hp <= 0) continue;
if (dist2(e.x, e.y, p.x, p.y) < (90 + e.r) * (90 + e.r)) {
tickEnemyDamage(e, 6 * st.dmgMul * st.dmgLiveMul);
if (chance(0.3)) spawnBlood(e.x, e.y, angleTo(p.x, p.y, e.x, e.y), 2);
}
}
}
}
// Thresher Plate: passive contact-damage aura (no need to be hit)
if (st.thresherPlate > 0) {
@@ -232,6 +259,8 @@ function hurtPlayer(dmg, ang, attacker) {
return;
}
let reduced = Math.max(1, Math.round(dmg));
// Meat Grinder: aura damages you for +1 per hit taken
if (p.stats.meatGrinder > 0) reduced += p.stats.meatGrinder;
// shield hearts absorb first (from the Shield Heart perk)
if (p.shieldHp > 0) {
const absorbed = Math.min(p.shieldHp, reduced);
@@ -262,7 +291,27 @@ function hurtPlayer(dmg, ang, attacker) {
areaDamage(p.x, p.y, 85 * Math.sqrt(p.stats.sizeMul), p.stats.retaliate * p.stats.dmgMul, true, { noProc: true });
spawnExplosionFx(p.x, p.y, 70);
}
// The Last Cut: at the brink, gain ×3 damage and +1s immunity
if (p.stats.theLastCut > 0 && p.hp > 0 && p.hp <= 1 && !p.lastCutActive) {
p.lastCutActive = true;
p.invT = Math.max(p.invT, 1.0 + p.stats.invBonus);
spawnText(p.x, p.y - 20, 'THE LAST CUT', '#e2472f');
addToast('THE LAST CUT', '×3 damage while at ½ heart');
Sfx.shieldUp();
}
if (p.hp <= 0) {
// Second Skin: revive once per floor at ½ heart
if (p.stats.secondSkin > 0 && !p.secondSkinUsed) {
p.secondSkinUsed = true;
p.hp = 1;
p.invT = 1.5;
G.flash = 0.6;
addShake(10);
spawnText(p.x, p.y - 20, 'SECOND SKIN', '#e2472f');
addToast('SECOND SKIN', 'you refuse to die — once per floor');
Sfx.shieldUp();
return;
}
p.hp = 0;
p.deathT = 0;
gameOver();
+3
View File
@@ -200,6 +200,9 @@ function nextFloor() {
// Shield Heart perk: fresh shield hearts each floor
G.player.shieldHp = G.player.stats.shieldPerk || 0;
if (G.player.shieldHp > 0) { spawnText(G.player.x, G.player.y - 20, 'SHIELD UP', '#3bc9e0'); Sfx.shieldUp(); }
// legendary once-per-floor flags reset
G.player.secondSkinUsed = false;
G.player.lastCutActive = false;
G.player.x = W / 2; G.player.y = H / 2;
addToast('FLOOR ' + G.floor, 'the meat runs deeper here');
Music.requestFloorMusic();
+2
View File
@@ -42,6 +42,8 @@ const SPRITE_MANIFEST = [
'i_deadweight', 'i_cauterizedveins', 'i_hollowchoir', 'i_sawbonecoil', 'i_gluttonsgut',
'i_slaughterrhythm', 'i_painengine', 'i_thresherplate', 'i_bloodmoat', 'i_ironlung',
'i_meathook', 'i_blooddebt',
'i_butchersoath', 'i_secondskin', 'i_twinsidearm', 'i_crimsonmetronome', 'i_abattoirengine',
'i_gorecrown', 'i_thousandteeth', 'i_hollowfather', 'i_thelastcut', 'i_meatgrinder',
// full 64px/128px eight-direction action atlases
'player_sheet', 'player_legs_sheet',
'enemy_shambler_sheet', 'enemy_runner_sheet', 'enemy_spitter_sheet',
+1 -1
View File
@@ -55,7 +55,7 @@ const PRESSURE_DIAL_MIN = -5, PRESSURE_DIAL_MAX = 5;
function pressureGainUnits(dial) { const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); return d <= 0 ? 1 + d * 0.2 : 1 + d * 0.8; }
// -5 -> 5 units, 0 -> 3 units, +5 -> 0 units (piecewise linear)
function pressureDropUnits(dial) { const d = clamp(dial, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX); return d <= 0 ? 3 - d * 0.4 : 3 - d * 0.6; }
function pressureGain() { return pressureGainUnits(G.pressureDial) * PRESSURE_UNIT; }
function pressureGain() { const base = pressureGainUnits(G.pressureDial) * PRESSURE_UNIT; return base * (G.player && G.player.stats.abattoirEngine > 0 ? 2 : 1); }
function pressureDropScale() { return pressureDropUnits(G.pressureDial) / PRESSURE_DROP_BASE; }
// total player power: perks drafted (level - 1) + item tiers owned, weighted by rarity.
+35 -5
View File
@@ -318,9 +318,9 @@ step(30, 16);
console.log('== expanded perk and item rosters ==');
{
check('41 new items added (14 phase-2 + 12 phase-3 passives)', Object.keys(ctx.ITEMS).length === 72);
check('41 new items added (14 phase-2 + 12 phase-3 + 10 legendaries)', Object.keys(ctx.ITEMS).length === 82);
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', 'deadweight', 'cauterizedveins', 'hollowchoir', 'sawbonecoil', 'gluttonsgut', 'slaughterrhythm', 'painengine', 'thresherplate', 'bloodmoat', 'ironlung', 'meathook', 'blooddebt'];
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', 'butchersoath', 'secondskin', 'twinsidearm', 'crimsonmetronome', 'abattoirengine', 'gorecrown', 'thousandteeth', 'hollowfather', 'thelastcut', 'meatgrinder'];
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',
@@ -367,10 +367,13 @@ console.log('== scrap feed has diminishing returns (like thick hide) ==');
check('first stack ~5% saving', Math.abs(s1 - (1 - 1 / 1.05)) < 0.0001);
check('each stack helps less', d2 < d1 && d3 < d2);
check('saving never reaches 100%', save() < 1);
// Brass Magazine still multiplies on top (unchanged)
// Brass Magazine uses the same diminishing additive formula as Scrap Feed
const s2x = { ammoEff: 1, ammoPickupMul: 1 };
ctx.ITEMS.brassmagazine.apply(s2x, {});
check('brass magazine stays multiplicative', Math.abs(s2x.ammoEff - 1.15) < 0.0001);
check('brass magazine is additive-diminishing (not multiplicative)', Math.abs(s2x.ammoEff - 1.10) < 0.0001);
const s2y = { ammoEff: 1, ammoPickupMul: 1 };
for (let i = 0; i < 5; i++) ctx.ITEMS.brassmagazine.apply(s2y, {});
check('brass magazine never reaches 50% saving', 1 - 1 / s2y.ammoEff < 0.5);
}
console.log('== close-range weapon impact pass ==');
@@ -498,6 +501,33 @@ console.log('== tier-scaled items + duplicate favoring ==');
check('item rolls favor owned items (' + ownedHits + '/200)', ownedHits >= 40);
}
console.log('== phase-4 boss-exclusive legendaries ==');
{
const p = ctx.G.player;
p.items = {};
// boss pool is the only source that can roll legendaries
let bossLegendaries = 0;
for (let i = 0; i < 400; i++) if (ctx.ITEMS[ctx.rollItemId('boss', 5)].rarity === 'legendary') bossLegendaries++;
check('boss pool can roll legendaries (' + bossLegendaries + '/400)', bossLegendaries > 10);
// Butcher's Oath: big damage, HP clamped to 2
const savedMax = p.stats.maxHp, savedHp = p.hp;
p.stats.maxHp = 10; p.hp = 10;
ctx.giveItem('butchersoath');
check('Butcher\'s Oath clamps max HP to 2', p.stats.maxHp === 2 && p.hp === 2);
p.stats.maxHp = savedMax; p.hp = savedHp; delete p.items.butchersoath;
// Second Skin: revive once per floor at ½ heart
p.stats.secondSkin = 0; p.secondSkinUsed = false;
ctx.giveItem('secondskin');
p.hp = 1; p.invT = 0; ctx.G.mode = 'play';
ctx.hurtPlayer(9999, 0);
check('Second Skin revives at ½ heart', p.hp === 1 && p.secondSkinUsed === true && ctx.G.mode === 'play');
// second death same floor is real
p.invT = 0;
ctx.hurtPlayer(9999, 0);
check('Second Skin only works once per floor', ctx.G.mode === 'gameover');
ctx.startRun(); step(3, 16);
}
console.log('== phase-3 hook passives ==');
{
const p = ctx.G.player;
@@ -984,7 +1014,7 @@ console.log('== item stickiness ramps with owned count ==');
console.log('== help manual grows with the implant roster ==');
{
check('10 help pages (4 implant pages for 72 items)', ctx.HELP_PAGES.length === 10);
check('10 help pages (4 implant pages for 82 items)', ctx.HELP_PAGES.length === 10);
check('10 help renderers', ctx.HELP_RENDERERS.length === 10);
ctx.G.player = ctx.G.player || {};
const fctx = fakeCtx();
+10
View File
@@ -291,6 +291,16 @@ ITEMS = {
"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",
"butchersoath": "a blood-soaked butcher cleaver crossed with a black oath scroll",
"secondskin": "a pale translucent second skin peeling away to reveal muscle",
"twinsidearm": "two crossed bone pistols fused at the grip with sinew",
"crimsonmetronome": "a crimson metronome with a heart-shaped pendulum and blood drips",
"abattoirengine": "a roaring industrial abattoir engine with pressure gauges and meat grinders",
"gorecrown": "a jagged crown of congealed blood and bone spikes radiating a red aura",
"thousandteeth": "a gaping maw with hundreds of tiny sharp teeth in concentric rings",
"hollowfather": "a gaunt hollow patriarch figure with three orbiting bone halos",
"thelastcut": "a single gleaming cleaver edge with a desperate red gleam",
"meatgrinder": "an industrial meat grinder with spinning blades and gore pouring through",
}
for iid, subject in ITEMS.items():
SPECS["i_" + iid] = Spec(subject + ", isolated inventory object, strong top-left light, no medallion, no icon background", 32)