0.9.3
Restore 2D 0.9.2, ignore Meshy refs, double Spinal Tap, Marrow Draught beam ammo, Second Stomach hearts fill overflow shields.
This commit is contained in:
1 parent
c41ce5fdb0
commit
b475f59853
10 files changed
+68
-36
No files matched your search
@@ -7,3 +7,6 @@ dist/
|
||||
served-state.txt
|
||||
__pycache__/
|
||||
*.pyc
|
||||
assets/raw/models/
|
||||
assets/raw/meshy/
|
||||
assets/raw/meshy_manifest.jsonl
|
||||
+1
-1
@@ -119,7 +119,7 @@ Right-click is disabled (context menu suppressed). There is **no interact key**
|
||||
### Healing
|
||||
|
||||
- Hearts restore 2 HP (1 heart) each.
|
||||
- Healing past max HP normally goes to waste — unless you have the **Second Stomach** item, which converts overflow healing into shield HP. Hearts are not consumed at full HP (they stay on the floor for later, and the magnet won't drag them).
|
||||
- Healing past max HP normally goes to waste — unless you have the **Second Stomach** item, which converts overflow healing into shield HP. Hearts stay on the floor at full HP unless Second Stomach has unused overflow-shield room (then they magnetize, collect, and fill shields).
|
||||
- Other healing sources: Vampire Dentures (kill procs), Hemophage (hit procs), Worm Gut / Bone Knit (post-combat heals).
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -295,7 +295,7 @@ Point-blank slam — no projectile, an instant circular blast in front of you. B
|
||||
|
||||
| Stat | Value |
|
||||
|---|---|
|
||||
| Damage | 95 (× 0.4–1.0 by charge) |
|
||||
| Damage | 190 (× 0.4–1.0 by charge) |
|
||||
| Charge time | 0.85 s (minimum 40% charge = 0.34 s to fire) |
|
||||
| Beam length | 900 × rangeMul |
|
||||
| Beam width | 14 × √sizeMul |
|
||||
|
||||
+22
-22
@@ -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=58">
|
||||
<link rel="stylesheet" href="css/style.css?v=59">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<canvas id="game" width="960" height="640"></canvas>
|
||||
</div>
|
||||
<script src="js/utils.js?v=58"></script>
|
||||
<script src="js/state.js?v=58"></script>
|
||||
<script src="js/input.js?v=58"></script>
|
||||
<script src="js/sfx.js?v=58"></script>
|
||||
<script src="js/sfxbank.js?v=58"></script>
|
||||
<script src="js/audio.js?v=58"></script>
|
||||
<script src="js/sprites.js?v=58"></script>
|
||||
<script src="js/particles.js?v=58"></script>
|
||||
<script src="js/weapons.js?v=58"></script>
|
||||
<script src="js/bullets.js?v=58"></script>
|
||||
<script src="js/pickups.js?v=58"></script>
|
||||
<script src="js/items.js?v=58"></script>
|
||||
<script src="js/perks.js?v=58"></script>
|
||||
<script src="js/enemies.js?v=58"></script>
|
||||
<script src="js/bosses.js?v=58"></script>
|
||||
<script src="js/rooms.js?v=58"></script>
|
||||
<script src="js/player.js?v=58"></script>
|
||||
<script src="js/hud.js?v=58"></script>
|
||||
<script src="js/help.js?v=58"></script>
|
||||
<script src="js/debug.js?v=58"></script>
|
||||
<script src="js/main.js?v=58"></script>
|
||||
<script src="js/utils.js?v=59"></script>
|
||||
<script src="js/state.js?v=59"></script>
|
||||
<script src="js/input.js?v=59"></script>
|
||||
<script src="js/sfx.js?v=59"></script>
|
||||
<script src="js/sfxbank.js?v=59"></script>
|
||||
<script src="js/audio.js?v=59"></script>
|
||||
<script src="js/sprites.js?v=59"></script>
|
||||
<script src="js/particles.js?v=59"></script>
|
||||
<script src="js/weapons.js?v=59"></script>
|
||||
<script src="js/bullets.js?v=59"></script>
|
||||
<script src="js/pickups.js?v=59"></script>
|
||||
<script src="js/items.js?v=59"></script>
|
||||
<script src="js/perks.js?v=59"></script>
|
||||
<script src="js/enemies.js?v=59"></script>
|
||||
<script src="js/bosses.js?v=59"></script>
|
||||
<script src="js/rooms.js?v=59"></script>
|
||||
<script src="js/player.js?v=59"></script>
|
||||
<script src="js/hud.js?v=59"></script>
|
||||
<script src="js/help.js?v=59"></script>
|
||||
<script src="js/debug.js?v=59"></script>
|
||||
<script src="js/main.js?v=59"></script>
|
||||
</body>
|
||||
</html>
|
||||
+3
-4
@@ -53,7 +53,7 @@ function updatePickups(dt) {
|
||||
}
|
||||
|
||||
// gems & small pickups fly to the player inside magnet radius
|
||||
if ((k.type === 'gem' || k.type === 'ammo' || (k.type === 'heart' && p.hp < p.stats.maxHp)) && d < magR) {
|
||||
if ((k.type === 'gem' || k.type === 'ammo' || (k.type === 'heart' && (p.hp < p.stats.maxHp || (p.stats.overShield > 0 && p.shieldHp < (p.stats.shieldPerk || 0) + p.stats.overShield)))) && d < magR) {
|
||||
const a = angleTo(k.x, k.y, p.x, p.y);
|
||||
const sp = lerp(320, 60, d / magR) * p.stats.magnetPull;
|
||||
k.x += Math.cos(a) * sp * dt; k.y += Math.sin(a) * sp * dt;
|
||||
@@ -73,9 +73,8 @@ function collectPickup(k) {
|
||||
Sfx.gem();
|
||||
break;
|
||||
case 'heart': {
|
||||
// At full HP hearts stay on the floor for later; Glutton's Gut heals +1
|
||||
// extra per tier below that.
|
||||
if (p.hp >= p.stats.maxHp) return false;
|
||||
// At full HP hearts stay unless Second Stomach has unused overflow-shield room.
|
||||
if (p.hp >= p.stats.maxHp && !(p.stats.overShield > 0 && p.shieldHp < (p.stats.shieldPerk || 0) + p.stats.overShield)) return false;
|
||||
const healAmt = 2 + (p.stats.gluttonGut || 0);
|
||||
healPlayer(healAmt); spawnText(p.x, p.y, '+' + healAmt, '#d92038');
|
||||
Sfx.heart();
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ const SfxBank = {
|
||||
if (this.buffers.has(name) || this.pending.has(name) || this.failed.has(name)) return;
|
||||
const file = this.FILES[name];
|
||||
if (!file || !this.ctx || typeof fetch !== 'function') return;
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=58')
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=59')
|
||||
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
|
||||
.then(bytes => this.ctx.decodeAudioData(bytes))
|
||||
.then(buffer => this.buffers.set(name, this.trim(buffer)))
|
||||
|
||||
+2
-2
@@ -88,10 +88,10 @@ const Sprites = {
|
||||
const finish = () => { if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.onload = () => { this.imgs[name] = img; if (typeof img.decode === 'function') img.decode().then(finish, finish); else finish(); };
|
||||
img.onerror = () => {
|
||||
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=58'; return; }
|
||||
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=59'; return; }
|
||||
finish();
|
||||
};
|
||||
img.src = hd ? 'assets/hd/' + name + '.webp?v=58' : 'assets/' + name + '.png?v=58';
|
||||
img.src = hd ? 'assets/hd/' + name + '.webp?v=59' : 'assets/' + name + '.png?v=59';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ const WEAPONS = {
|
||||
trapqueen: { id: 'trapqueen', name: 'Trap Queen', held: 58, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 2, ammo: 26, refill: 13, interval: 0.55, dmg: 10, spd: 999, behavior: 'lob_trap', sprite: 'bullet_saw', sfx: 'lob', punch: 0.65, desc: 'Bear traps. For bears. And worse' },
|
||||
tenderizer: { id: 'tenderizer', name: 'The Tenderizer', held: 66, torsoW: 132, torsoFwd: 23, muzzle: 78, tier: 3, ammo: 10, refill: 5, interval: 0.58, dmg: 60, spd: 0, behavior: 'slam', sprite: 'bullet_bone', sfx: 'heavy', punch: 1.00, desc: 'A room-shaking close-range meat paste' },
|
||||
redhand: { id: 'redhand', name: 'Red Right Hand', held: 68, torsoW: 134, torsoFwd: 24, muzzle: 80, tier: 2, ammo: 150, refill: 72, drain: 18, interval: 0.10, dmg: 7, spd: 0, behavior: 'saw', sprite: 'bullet_saw', sfx: 'saw', punch: 0.38, desc: 'A chainsaw that liquefies anything in reach' },
|
||||
spinaltap: { id: 'spinaltap', name: 'Spinal Tap', held: 68, torsoW: 136, torsoFwd: 24, muzzle: 81, tier: 3, ammo: 7, refill: 4, interval: 0.30, dmg: 95, spd: 0, chargeTime: 0.85, behavior: 'beam', sprite: 'bullet_bone', sfx: 'beam', punch: 1.00, desc: 'Charge it. Delete a line of meat' },
|
||||
spinaltap: { id: 'spinaltap', name: 'Spinal Tap', held: 68, torsoW: 136, torsoFwd: 24, muzzle: 81, tier: 3, ammo: 7, refill: 4, interval: 0.30, dmg: 190, spd: 0, chargeTime: 0.85, behavior: 'beam', sprite: 'bullet_bone', sfx: 'beam', punch: 1.00, desc: 'Charge it. Delete a line of meat' },
|
||||
swarmjar: { id: 'swarmjar', name: 'Swarm Jar', held: 62, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 3, ammo: 12, refill: 6, interval: 0.80, dmg: 9, spd: 999, behavior: 'lob_swarm', sprite: 'bullet_gore', sfx: 'lob', punch: 0.70, desc: 'A jar of friends. They are hungry' },
|
||||
};
|
||||
|
||||
@@ -215,7 +215,7 @@ function fireBeam(p, def, charge) {
|
||||
p.actionT = 0;
|
||||
addShake(8);
|
||||
Sfx.shoot(def);
|
||||
p.weapon.ammo -= 1 / st.ammoEff;
|
||||
if (!(p.marrowDraughtT > 0)) p.weapon.ammo -= 1 / st.ammoEff;
|
||||
}
|
||||
|
||||
// damage every enemy in a radius (slams, explosions)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meatslicer",
|
||||
"productName": "MeatSlicer",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"private": true,
|
||||
"description": "A Butcher's Descent - clear, harvest, descend.",
|
||||
"main": "desktop/main.js",
|
||||
|
||||
+32
-2
@@ -216,7 +216,7 @@ check('HD loader loads all images (fallbacks)', ctx.G.imagesLoaded === true);
|
||||
let hdFallbackOk = 0;
|
||||
for (const name of ctx.SPRITE_MANIFEST) {
|
||||
const urls = attemptedUrls[name];
|
||||
const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=58' && urls[1] === 'assets/' + name + '.png?v=58';
|
||||
const ok = urls && urls.length === 2 && urls[0] === 'assets/hd/' + name + '.webp?v=59' && urls[1] === 'assets/' + name + '.png?v=59';
|
||||
if (ok) hdFallbackOk++;
|
||||
}
|
||||
check('HD loader tries WebP then PNG per sprite', hdFallbackOk === ctx.SPRITE_MANIFEST.length);
|
||||
@@ -241,7 +241,7 @@ check('SD loader loads all images', ctx.G.imagesLoaded === true);
|
||||
let sdUrlOk = 0;
|
||||
for (const name of ctx.SPRITE_MANIFEST) {
|
||||
const urls = attemptedUrls[name];
|
||||
const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=58';
|
||||
const ok = urls && urls.length === 1 && urls[0] === 'assets/' + name + '.png?v=59';
|
||||
if (ok) sdUrlOk++;
|
||||
}
|
||||
check('SD loader requests one PNG per sprite', sdUrlOk === ctx.SPRITE_MANIFEST.length);
|
||||
@@ -1127,6 +1127,7 @@ console.log('== generic ammo drops ==');
|
||||
ctx.WEAPONS.repeater.refill > ctx.WEAPONS.spinaltap.refill * 10);
|
||||
check('stream weapons are time-denominated (drain, not per-shot)',
|
||||
ctx.WEAPONS.bile.drain > 0 && ctx.WEAPONS.cauterizer.drain > 0 && ctx.WEAPONS.redhand.drain > 0);
|
||||
check('Spinal Tap base damage is 190', ctx.WEAPONS.spinaltap.dmg === 190);
|
||||
|
||||
const repeaterCap = ctx.WEAPONS.repeater.ammo * 1.5;
|
||||
p.weapon = { id: 'repeater', ammo: repeaterCap - 1 };
|
||||
@@ -1197,6 +1198,28 @@ console.log('== hearts stay on the floor at full HP ==');
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== Second Stomach hearts fill overflow shields ==');
|
||||
{
|
||||
ctx.startRun(); step(3, 16);
|
||||
const p = ctx.G.player;
|
||||
ctx.giveItem('secondstomach');
|
||||
p.hp = p.stats.maxHp;
|
||||
p.shieldHp = 0;
|
||||
const cap = (p.stats.shieldPerk || 0) + p.stats.overShield;
|
||||
ctx.G.pickups.length = 0;
|
||||
ctx.G.enemies.length = 0;
|
||||
ctx.spawnPickup('heart', p.x, p.y);
|
||||
step(2, 16);
|
||||
check('Second Stomach collects heart at full HP', ctx.G.pickups.length === 0);
|
||||
check('heart overflow fills shields', p.hp === p.stats.maxHp && p.shieldHp === Math.min(cap, 2));
|
||||
p.hp = p.stats.maxHp;
|
||||
p.shieldHp = cap;
|
||||
ctx.spawnPickup('heart', p.x, p.y);
|
||||
step(2, 16);
|
||||
check('heart stays when Second Stomach shields are full', ctx.G.pickups.length === 1);
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== dash: burst, slow, cooldown ==');
|
||||
{
|
||||
ctx.startRun(); step(3, 16);
|
||||
@@ -1297,6 +1320,13 @@ console.log('== muzzle origins match projectile origins ==');
|
||||
const beamParts = ctx.G.parts.filter(pt => pt.type === 'beam');
|
||||
const newest = beamParts[beamParts.length - 1];
|
||||
check('beam particle starts at spinaltap muzzle (81px)', newest && Math.abs(newest.x - (p.x + 81)) < 0.01 && Math.abs(newest.y - (p.y)) < 0.01);
|
||||
p.weapon = { id: 'spinaltap', ammo: 7 };
|
||||
p.marrowDraughtT = 5;
|
||||
ctx.fireBeam(p, ctx.WEAPONS.spinaltap, 1);
|
||||
check('Marrow Draught fireBeam spends no ammo', p.weapon.ammo === 7);
|
||||
p.marrowDraughtT = 0;
|
||||
ctx.fireBeam(p, ctx.WEAPONS.spinaltap, 1);
|
||||
check('fireBeam spends ammo without Marrow Draught', p.weapon.ammo === 6);
|
||||
ctx.G.bullets.length = 0;
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user