Normalize ammo economy: per-weapon refill, stream drain, behavior fixes

- Every special weapon gets an explicit 'refill' (units per ammo pickup,
  tuned so one pickup funds ~10 kills) replacing the ammoRefillFraction
  log2 heuristic and ammoWeight overrides
- Bile Blunderbuss + The Cauterizer join Red Right Hand as time-denominated
  stream weapons ('drain' per second) instead of per-shot cost
- Sawblades ricochet through enemies (bounce fix), traps catch 3 victims,
  lobbed projectiles arc overhead and always trigger at the target point
  (Flesh Masher / Trap Queen / Swarm Jar payloads now actually deploy)
- Boss rooms stock one ammo on entry and drop one guaranteed on death
- Cauterizer burn trimmed (2.2s x2 -> 1.5s x0.8), bile acid pools trimmed
  (3.5s x1.5 -> 2.5s x0.8), Marrow Draught now covers stream drain
- New tools/ammo_sim.js calibrates every weapon into the 8-13 kills/refill
  band (all 15 pass); smoke test guards the ammo structure
- v0.3.0, cache bump v=44
This commit is contained in:
MeatSlicer Developer committed 2026-07-30 22:06:10 +10:00
1 parent 7d33e36c1e
commit d9eae34df4
18 files changed
+380 -101

No files matched your search

+13 -6
View File
@@ -87,7 +87,7 @@ function updateBullets(dt) {
dead = true;
// bile congeals into acid pools
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 });
G.hazards.push({ kind: 'acid', x: b.x, y: b.y, r: 24 * (b.sizeMul || 1), life: 2.5, t: 0, dps: b.dmg * 0.8 });
}
// 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') {
@@ -111,8 +111,9 @@ function updateBullets(dt) {
lobTrigger(b);
}
// enemy collision
if (!dead) {
// enemy collision (lobbed projectiles arc overhead and only trigger at
// their target point — or on a wall — never mid-flight)
if (!dead && !b.lobbed) {
for (const e of G.enemies) {
if (e.hp <= 0 || e.phased || (b.hit && b.hit.has(e))) continue;
if (e === b.dragTarget) continue;
@@ -122,10 +123,10 @@ function updateBullets(dt) {
if (!b.hit) b.hit = new Set();
b.hit.add(e);
spawnBlood(b.x, b.y, a, 3);
if (b.behavior === 'flame') { e.burnT = Math.max(e.burnT, 2.2); e.burnDps = Math.max(e.burnDps || 0, b.dmg * 2); }
if (b.behavior === 'flame') { e.burnT = Math.max(e.burnT, 1.5); e.burnDps = Math.max(e.burnDps || 0, b.dmg * 0.8); }
if (b.behavior === 'cone') {
e.slowT = Math.max(e.slowT, 1.6);
if (G.hazards.length < 80 && chance(0.45)) 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 });
if (G.hazards.length < 80 && chance(0.45)) G.hazards.push({ kind: 'acid', x: b.x, y: b.y, r: 24 * (b.sizeMul || 1), life: 2.5, t: 0, dps: b.dmg * 0.8 });
}
// Hemophage remains an on-hit drain, naturally limited by its fire interval.
if (b.lifesteal && p.hp < p.stats.maxHp && chance(b.lifesteal * 0.25)) lifestealPlayer(1);
@@ -134,6 +135,12 @@ function updateBullets(dt) {
}
if (b.pierce > 0) { b.pierce--; }
else if (b.behavior === 'boomerang' || b.behavior === 'pierce_drag') { /* pass through */ }
// Sawblades ricochet through meat the same way they ricochet off
// walls: each enemy hit spends one bounce, shared with wall bounces.
else if (b.behavior === 'bounce' && ((b.bounces || 0) + (b.bounce || 0)) > 0) {
if (b.bounces > 0) b.bounces--; else b.bounce--;
spawnSpark(b.x, b.y, a + Math.PI);
}
else {
dead = true; break;
}
@@ -217,7 +224,7 @@ function lobTrigger(b) {
spawnBlood(b.x, b.y, 0, 16, true);
Sfx.explode({ x: b.x, y: b.y });
} else if (b.behavior === 'lob_trap') { // Trap Queen
G.hazards.push({ kind: 'trap', x: b.x, y: b.y, r: 16 * (b.sizeMul || 1), life: 12 * (b.rangeMul || 1), t: 0, dmg: b.dmg });
G.hazards.push({ kind: 'trap', x: b.x, y: b.y, r: 16 * (b.sizeMul || 1), life: 12 * (b.rangeMul || 1), t: 0, dmg: b.dmg, charges: 3 });
Sfx.trapSet({ x: b.x, y: b.y });
} else if (b.behavior === 'lob_swarm') { // Swarm Jar: homing maggots
const st = G.player.stats;