feat: crimson metronome lends ½ heart, repaid on a clean room clear

This commit is contained in:
Quintine committed 2026-08-09 14:22:26 +10:00
1 parent 39d89fbc53
commit f77b4fd402
5 files changed
+68 -25

No files matched your search

+22 -22
View File
@@ -6,32 +6,32 @@
<meta name="theme-color" content="#0a0305">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><text y=%2252%22 x=%2250%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-size=%2252%22>🔪</text></svg>">
<title>MEATSLICER</title>
<link rel="stylesheet" href="css/style.css?v=52">
<link rel="stylesheet" href="css/style.css?v=53">
</head>
<body>
<div id="wrap">
<canvas id="game" width="960" height="640"></canvas>
</div>
<script src="js/utils.js?v=52"></script>
<script src="js/state.js?v=52"></script>
<script src="js/input.js?v=52"></script>
<script src="js/sfx.js?v=52"></script>
<script src="js/sfxbank.js?v=52"></script>
<script src="js/audio.js?v=52"></script>
<script src="js/sprites.js?v=52"></script>
<script src="js/particles.js?v=52"></script>
<script src="js/weapons.js?v=52"></script>
<script src="js/bullets.js?v=52"></script>
<script src="js/pickups.js?v=52"></script>
<script src="js/items.js?v=52"></script>
<script src="js/perks.js?v=52"></script>
<script src="js/enemies.js?v=52"></script>
<script src="js/bosses.js?v=52"></script>
<script src="js/rooms.js?v=52"></script>
<script src="js/player.js?v=52"></script>
<script src="js/hud.js?v=52"></script>
<script src="js/help.js?v=52"></script>
<script src="js/debug.js?v=52"></script>
<script src="js/main.js?v=52"></script>
<script src="js/utils.js?v=53"></script>
<script src="js/state.js?v=53"></script>
<script src="js/input.js?v=53"></script>
<script src="js/sfx.js?v=53"></script>
<script src="js/sfxbank.js?v=53"></script>
<script src="js/audio.js?v=53"></script>
<script src="js/sprites.js?v=53"></script>
<script src="js/particles.js?v=53"></script>
<script src="js/weapons.js?v=53"></script>
<script src="js/bullets.js?v=53"></script>
<script src="js/pickups.js?v=53"></script>
<script src="js/items.js?v=53"></script>
<script src="js/perks.js?v=53"></script>
<script src="js/enemies.js?v=53"></script>
<script src="js/bosses.js?v=53"></script>
<script src="js/rooms.js?v=53"></script>
<script src="js/player.js?v=53"></script>
<script src="js/hud.js?v=53"></script>
<script src="js/help.js?v=53"></script>
<script src="js/debug.js?v=53"></script>
<script src="js/main.js?v=53"></script>
</body>
</html>
+1 -1
View File
@@ -94,7 +94,7 @@ const ITEMS = {
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; } },
crimsonmetronome:{ name: 'Crimson Metronome', desc: '+60% fire rate; every 8th shot lends ½ heart, repaid if you clear the room clean', 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; } },
+9 -2
View File
@@ -31,6 +31,7 @@ function initPlayer() {
animT: 0, attackT: 0, actionT: 0, hitT: 0, deathT: 0,
weapon: { id: 'bonepopper', ammo: Infinity },
holstered: null, // a special weapon set aside while the pistol is out
metronomeTmp: 0, // ½-hearts lent by Crimson Metronome, repaid on a clean clear
level: 1, xp: 0,
items: {}, // iid -> quality tier (1..3)
perks: [], // perk ids drafted this run, in grant order
@@ -155,10 +156,16 @@ function updatePlayer(dt) {
p.choirCount = (p.choirCount || 0) + 1;
if (p.choirCount >= 4) { p.choirCount = 0; fireWeapon(p, w); }
}
// Crimson Metronome: every 8th shot costs ½ heart
// Crimson Metronome: every 8th shot lends ½ heart, repaid when the room clears clean
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'); }
if (p.metronomeCount >= 8) {
p.metronomeCount = 0;
const loss = Math.min(1, Math.max(0, p.hp - 0.5));
p.hp = Math.max(0.5, p.hp - 1);
p.metronomeTmp += loss; // on loan: restored by a clean room clear
spawnText(p.x, p.y - 14, '-½', '#e2472f');
}
}
p.recoil = Math.min(1, p.recoil + (w.behavior === 'slam' ? 1 : 0.45));
p.muzzleT = 0.07;
+8
View File
@@ -88,6 +88,7 @@ function enterRoom(gx, gy) {
G.roomLayerKey = '';
G.roomDamaged = false;
G.roomEnterT = G.time;
if (G.player) G.player.metronomeTmp = 0; // fresh per-room Crimson-Metronome loan ledger
const room = G.rooms[roomKey(gx, gy)];
G.cur = room;
@@ -221,6 +222,13 @@ function recordRoomClear(room) {
}
// active items charge on room clears: +1 per combat room, +2 per boss
const p = G.player;
// Crimson Metronome: a ½-heart loan is repaid when the room clears clean
if (!G.roomDamaged && p.metronomeTmp > 0) {
const back = Math.min(p.metronomeTmp, p.stats.maxHp - p.hp);
p.hp += back;
p.metronomeTmp = 0;
if (back > 0) spawnText(p.x, p.y - 26, 'LOAN REPAID +' + back, '#d98991');
}
if (p && p.active) {
const a = ACTIVES[p.active.iid];
if (a) {
+28
View File
@@ -1810,5 +1810,33 @@ check('debug console closes back to play', ctx.G.mode === 'play');
ctx.G.mode = 'play';
})();
console.log('== crimson metronome heart loan ==');
{
ctx.startRun(); step(3, 16); // self-contained: fresh player, empty arena
ctx.G.enemies.length = 0;
const p = ctx.G.player;
ctx.giveItem('crimsonmetronome');
check('metronome grants the loan flag', p.stats.crimsonMetronome === 1);
p.weapon = { id: 'repeater', ammo: 500 }; p.holstered = null; // per-shot trigger
p.hp = 6; p.metronomeCount = 0; p.metronomeTmp = 0;
const hpBefore = p.hp;
ctx.Input.mdown = true;
let guard = 0;
while (p.metronomeTmp === 0 && guard++ < 4000) step(1, 16);
ctx.Input.mdown = false;
check('8th shot lends ½ heart (not lost)', p.hp === hpBefore - 1 && p.metronomeTmp === 1);
ctx.G.roomDamaged = false;
ctx.recordRoomClear({ type: 'combat' });
check('clean room clear repays the loan', p.hp === hpBefore && p.metronomeTmp === 0);
ctx.Input.mdown = true;
guard = 0;
while (p.metronomeTmp === 0 && guard++ < 4000) step(1, 16);
ctx.Input.mdown = false;
check('loan accrues again', p.hp === hpBefore - 1 && p.metronomeTmp === 1);
ctx.G.roomDamaged = true;
ctx.recordRoomClear({ type: 'combat' });
check('hit room forfeits the loan', p.hp === hpBefore - 1 && p.metronomeTmp === 1);
}
console.log(failures === 0 ? '\nALL CHECKS PASSED' : '\n' + failures + ' CHECKS FAILED');
process.exit(failures === 0 ? 0 : 1);