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:
1 parent
7d33e36c1e
commit
d9eae34df4
18 files changed
+380
-101
No files matched your search
+2
-1
@@ -592,8 +592,9 @@ function onBossDeath(e) {
|
||||
}
|
||||
addScore(500 + G.floor * 100);
|
||||
addShake(12);
|
||||
// boss reward: item pedestal + a real weapon drop + stairs down
|
||||
// boss reward: item pedestal + a real weapon drop + ammo + stairs down
|
||||
spawnItemPedestal(W / 2 - 90, H / 2, null, 'boss');
|
||||
spawnPickup('ammo', W / 2, H / 2 - 40);
|
||||
// 30% chance of a bonus active pedestal (additive — never replaces the passive)
|
||||
if (chance(0.30)) spawnActivePedestal(W / 2 - 160, H / 2 + 60);
|
||||
const bw = rollWeaponDrop(G.floor + 2); // bosses drop the good stuff
|
||||
|
||||
+13
-6
@@ -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;
|
||||
|
||||
+3
-1
@@ -151,7 +151,9 @@ function drawHelpArsenal(ctx, px, py) {
|
||||
if (row % 2 === 0) { ctx.fillStyle = 'rgba(255,220,200,0.03)'; ctx.fillRect(x - 4, ty - 11, HELP_PANEL.w - 72, 15); }
|
||||
const dps = Math.round(w.dmg / Math.max(0.01, w.interval) * (w.pellets || 1));
|
||||
const vals = [w.name.toUpperCase(), w.tier < 0 ? 'SIDE' : 'T' + w.tier, String(w.dmg),
|
||||
w.interval.toFixed(2) + 's', w.ammo === Infinity ? '∞' : String(w.ammo), String(dps), w.behavior.toUpperCase()];
|
||||
w.interval.toFixed(2) + 's',
|
||||
w.ammo === Infinity ? '∞' : (w.drain ? w.ammo + '·' + w.drain + '/s' : String(w.ammo)),
|
||||
String(dps), w.behavior.toUpperCase()];
|
||||
ctx.fillStyle = '#c9b8ae';
|
||||
for (let c = 0; c < cols.length; c++) ctx.fillText(vals[c], x + cols[c], ty);
|
||||
ty += 15;
|
||||
|
||||
+4
-12
@@ -16,17 +16,9 @@ function spawnGems(x, y, value) {
|
||||
}
|
||||
|
||||
|
||||
// Ammo drops preserve useful trigger time: low-damage rapid weapons receive
|
||||
// a larger share, while heavy multi-pellet shots receive fewer rounds.
|
||||
function ammoRefillFraction(w) {
|
||||
const effectiveDamage = Math.max(1, (w.dmg || 1) * (w.pellets || 1));
|
||||
const interval = Math.max(0.05, w.interval ?? 0.3);
|
||||
const damageWeight = Math.log2(effectiveDamage / 8);
|
||||
const rateWeight = Math.log2(interval / 0.3);
|
||||
// Heavy, slow attacks push the fraction down; light, rapid attacks push it up.
|
||||
const heaviness = clamp((damageWeight + rateWeight) / 4, -1, 1);
|
||||
return clamp(0.62 - heaviness * 0.28, 0.30, 0.88) * (w.ammoWeight || 1) * AMMO_REFILL_SCALE;
|
||||
}
|
||||
// Ammo pickups hand back each weapon's authored `refill` (see WEAPONS), tuned
|
||||
// so one pickup funds roughly ten kills. Magazine size (`ammo`) is a separate
|
||||
// feel dial; the economy dial lives on `refill`.
|
||||
|
||||
// room-clear reward burst (weapons come from bosses / item rooms, not room clears)
|
||||
function spawnRoomReward(x, y, floor) {
|
||||
@@ -87,7 +79,7 @@ function collectPickup(k) {
|
||||
for (const inst of carried) {
|
||||
const def = WEAPONS[inst.id];
|
||||
const cap = def.ammo * 1.5;
|
||||
const wanted = Math.max(1, Math.ceil(def.ammo * ammoRefillFraction(def) * p.stats.ammoPickupMul));
|
||||
const wanted = Math.max(1, Math.ceil((def.refill || 1) * p.stats.ammoPickupMul));
|
||||
const add = Math.max(0, Math.min(wanted, cap - inst.ammo));
|
||||
inst.ammo += add;
|
||||
total += add;
|
||||
|
||||
@@ -111,11 +111,26 @@ function updatePlayer(dt) {
|
||||
if (w.behavior === 'saw') {
|
||||
if (Input.mdown && p.weapon.ammo > 0) {
|
||||
sawTick(p, w, p.weapon, dt);
|
||||
if (!(p.marrowDraughtT > 0)) p.weapon.ammo -= dt * (w.drain || 18) / st.ammoEff;
|
||||
p.muzzleT = 0.04; p.recoil = 0.18 + Math.sin(G.time * 45) * 0.05;
|
||||
if (p.attackT <= 0) p.actionT = 0;
|
||||
p.attackT = 0.2;
|
||||
}
|
||||
else p.fireT = 0;
|
||||
} else if (w.drain) {
|
||||
// cone / flame streams: projectiles are free, ammo is time-denominated
|
||||
if (Input.mdown && p.weapon.ammo > 0 && !(p.panicRoomT > 0)) {
|
||||
if (p.fireT <= 0) {
|
||||
fireWeapon(p, w);
|
||||
p.recoil = Math.min(1, p.recoil + 0.45);
|
||||
p.muzzleT = 0.07;
|
||||
p.attackT = 0.4; p.actionT = 0;
|
||||
p.fireT = rate;
|
||||
Sfx.shoot(w);
|
||||
}
|
||||
if (!(p.marrowDraughtT > 0)) p.weapon.ammo -= dt * w.drain / st.ammoEff;
|
||||
}
|
||||
else p.fireT = 0;
|
||||
} else if (w.behavior === 'beam') {
|
||||
if (Input.mdown && p.weapon.ammo > 0) {
|
||||
p.charge = Math.min(p.charge + dt * effectiveRate * Math.sqrt(st.shotSpeedMul), w.chargeTime);
|
||||
|
||||
+9
-3
@@ -82,6 +82,8 @@ function enterRoom(gx, gy) {
|
||||
} else if (room.type === 'boss') {
|
||||
const boss = spawnBoss(G.floor);
|
||||
room.bossSpawned = true;
|
||||
// boss fights are a pure ammo drain, so stock the room on the way in
|
||||
spawnPickup('ammo', W / 2, H / 2 + 140);
|
||||
Music.playBoss(boss.bossKind);
|
||||
} else if (room.type === 'item') {
|
||||
spawnItemPedestal(W / 2 - 45, H / 2, null, 'room');
|
||||
@@ -239,14 +241,18 @@ function updateHazards(dt) {
|
||||
}
|
||||
if (h.kind === 'trap') {
|
||||
for (const e of G.enemies) {
|
||||
if (e.boss) continue;
|
||||
if (e.boss || e.hp <= 0) continue;
|
||||
if (h.caught && h.caught.has(e)) continue; // each victim springs it once
|
||||
if (dist2(h.x, h.y, e.x, e.y) < (h.r + e.r) * (h.r + e.r)) {
|
||||
e.rootT = 2.2;
|
||||
damageEnemy(e, h.dmg, rand(0, TAU), false);
|
||||
spawnBlood(e.x, e.y, 0, 6);
|
||||
Sfx.trapSnap({ x: h.x, y: h.y });
|
||||
G.hazards.splice(i, 1);
|
||||
break;
|
||||
// a trap snaps shut on a few victims before it breaks
|
||||
if (!h.caught) h.caught = new Set();
|
||||
h.caught.add(e);
|
||||
h.charges = (h.charges === undefined ? 1 : h.charges) - 1;
|
||||
if (h.charges <= 0) { G.hazards.splice(i, 1); break; }
|
||||
}
|
||||
}
|
||||
} else if (!h.hostile || h.dps > 0) { // weapon pools hurt enemies; zero-DPS boss pools do not
|
||||
|
||||
+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=43')
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=44')
|
||||
.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)))
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ const Sprites = {
|
||||
const img = new Image();
|
||||
img.onload = () => { this.imgs[name] = img; if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.onerror = () => { if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.src = 'assets/' + name + '.png?v=43';
|
||||
img.src = 'assets/' + name + '.png?v=44';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
+1
-2
@@ -45,8 +45,7 @@ const G = {
|
||||
};
|
||||
|
||||
const PERK_POWER_WEIGHT = 0.5;
|
||||
// ammo economy: global dials for refill size and drop frequency
|
||||
const AMMO_REFILL_SCALE = 0.75;
|
||||
// ammo economy: global dial for drop frequency (refill size is per-weapon)
|
||||
const AMMO_DROP_SCALE = 0.75;
|
||||
const PRESSURE_MIN = 0.75, PRESSURE_MAX = 1.60;
|
||||
const PRESSURE_UNIT = 0.01; // one "point" on the dial scale
|
||||
|
||||
+23
-17
@@ -2,23 +2,28 @@
|
||||
// behavior: bullet | spread | boomerang | bounce | cone | flame | homing |
|
||||
// pierce_drag | lob | lob_trap | lob_swarm | slam | saw | beam
|
||||
|
||||
// Ammo economy: `ammo` is the magazine (trigger-time / burst feel), `refill`
|
||||
// is what one ammo pickup hands back (~10 kills' worth). Sustained-stream
|
||||
// weapons use `drain` (ammo per second while held) instead of per-shot cost.
|
||||
// Calibrated with tools/ammo_sim.js — every weapon should land ~8-13 kills
|
||||
// per refill at base stats.
|
||||
const WEAPONS = {
|
||||
bonepopper: { id: 'bonepopper', name: 'Bone Popper', held: 38, torsoW: 106, torsoFwd: 18, muzzle: 62, tier: -1, ammo: Infinity, interval: 0.60, dmg: 8, spd: 540, range: 0.45, behavior: 'bullet', sprite: 'bullet_bone', sfx: 'default', punch: 0.42, desc: 'Trusty bone-shard sidearm' },
|
||||
repeater: { id: 'repeater', name: 'Ribcage Repeater', held: 52, torsoW: 120, torsoFwd: 20, muzzle: 70, tier: 0, ammo: 140, interval: 0.09, dmg: 6, spd: 560, spread: 0.10, range: 0.42, behavior: 'bullet', sprite: 'bullet_bone', sfx: 'rapid', punch: 0.18, desc: 'Rattles off rib slivers' },
|
||||
marrow: { id: 'marrow', name: 'Marrow Scatter', held: 54, torsoW: 120, torsoFwd: 20, muzzle: 69, tier: 0, ammo: 42, interval: 0.58, dmg: 11, spd: 500, pellets: 6, spread: 0.55, range: 0.38, behavior: 'spread', sprite: 'bullet_bone', sfx: 'spread', punch: 0.92, desc: 'A devastating close blast of jagged bone' },
|
||||
cleaver: { id: 'cleaver', name: 'Cleaver Cadence', held: 60, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 1, ammo: 34, interval: 0.38, dmg: 15, spd: 400, range: 1.10, behavior: 'boomerang', sprite: 'bullet_cleaver', sfx: 'boomerang', punch: 0.42, desc: 'It always comes back' },
|
||||
saw: { id: 'saw', name: 'Sawblade Launcher',held: 52, torsoW: 118, torsoFwd: 20, muzzle: 70, tier: 1, ammo: 26, interval: 0.34, dmg: 13, spd: 360, bounces: 5, range: 0.60, behavior: 'bounce', sprite: 'bullet_saw', sfx: 'saw', punch: 0.45, desc: 'Bouncing, hungry steel' },
|
||||
bile: { id: 'bile', name: 'Bile Blunderbuss', held: 54, torsoW: 118, torsoFwd: 20, muzzle: 68, tier: 2, ammo: 70, interval: 0.12, dmg: 7, spd: 340, spread: 0.32, range: 0.38, behavior: 'cone', sprite: 'bullet_gore', sfx: 'flame', punch: 0.34, desc: 'A brutal corrosive torrent; pools linger' },
|
||||
hemophage: { id: 'hemophage', name: 'Hemophage', held: 50, torsoW: 116, torsoFwd: 19, muzzle: 67, tier: 1, ammo: 44, interval: 0.19, dmg: 8, spd: 480, lifesteal: 0.5, range: 0.45, behavior: 'bullet', sprite: 'bullet_syringe', sfx: 'default', punch: 0.35, desc: 'Drinks what it touches' },
|
||||
eye: { id: 'eye', name: 'Eye Ballista', held: 58, torsoW: 120, torsoFwd: 20, muzzle: 70, tier: 1, ammo: 32, interval: 0.30, dmg: 10, spd: 320, range: 0.62, behavior: 'homing', sprite: 'bullet_eye', sfx: 'default', punch: 0.40, desc: 'The eyes follow. They always follow' },
|
||||
guthook: { id: 'guthook', name: 'Gut Hook', held: 64, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 2, ammo: 14, interval: 0.85, dmg: 48, spd: 720, range: 0.60, behavior: 'pierce_drag', sprite: 'bullet_harpoon', sfx: 'heavy', punch: 0.95, desc: 'Impales and drags the catch' },
|
||||
cauterizer: { id: 'cauterizer', name: 'The Cauterizer', held: 58, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 3, ammo: 110, interval: 0.05, dmg: 4.5, spd: 310, spread: 0.34, range: 0.36, behavior: 'flame', sprite: 'bullet_gore', sfx: 'flame', punch: 0.32, desc: 'Incinerates anything brave enough to get close' },
|
||||
fleshmasher: { id: 'fleshmasher', name: 'Flesh Masher', held: 62, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 2, ammo: 14, interval: 0.75, dmg: 34, spd: 999, behavior: 'lob', sprite: 'bullet_gore', sfx: 'lob', punch: 0.90, desc: 'Lobs ground-meat bombs' },
|
||||
trapqueen: { id: 'trapqueen', name: 'Trap Queen', held: 58, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 2, ammo: 12, interval: 0.55, dmg: 10, spd: 999, behavior: 'lob_trap', sprite: 'bullet_saw', sfx: 'lob', punch: 0.65, ammoWeight: 0.75, desc: 'Bear traps. For bears. And worse' },
|
||||
tenderizer: { id: 'tenderizer', name: 'The Tenderizer', held: 66, torsoW: 132, torsoFwd: 23, muzzle: 78, tier: 3, ammo: 26, 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: 160, 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: 9, 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' },
|
||||
swarmjar: { id: 'swarmjar', name: 'Swarm Jar', held: 62, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 3, ammo: 7, interval: 0.80, dmg: 9, spd: 999, behavior: 'lob_swarm', sprite: 'bullet_gore', sfx: 'lob', punch: 0.70, ammoWeight: 0.8, desc: 'A jar of friends. They are hungry' },
|
||||
repeater: { id: 'repeater', name: 'Ribcage Repeater', held: 52, torsoW: 120, torsoFwd: 20, muzzle: 70, tier: 0, ammo: 84, refill: 42, interval: 0.09, dmg: 6, spd: 560, spread: 0.10, range: 0.42, behavior: 'bullet', sprite: 'bullet_bone', sfx: 'rapid', punch: 0.18, desc: 'Rattles off rib slivers' },
|
||||
marrow: { id: 'marrow', name: 'Marrow Scatter', held: 54, torsoW: 120, torsoFwd: 20, muzzle: 69, tier: 0, ammo: 20, refill: 6, interval: 0.58, dmg: 11, spd: 500, pellets: 6, spread: 0.55, range: 0.38, behavior: 'spread', sprite: 'bullet_bone', sfx: 'spread', punch: 0.92, desc: 'A devastating close blast of jagged bone' },
|
||||
cleaver: { id: 'cleaver', name: 'Cleaver Cadence', held: 60, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 1, ammo: 26, refill: 10, interval: 0.38, dmg: 15, spd: 400, range: 1.10, behavior: 'boomerang', sprite: 'bullet_cleaver', sfx: 'boomerang', punch: 0.42, desc: 'It always comes back' },
|
||||
saw: { id: 'saw', name: 'Sawblade Launcher',held: 52, torsoW: 118, torsoFwd: 20, muzzle: 70, tier: 1, ammo: 24, refill: 9, interval: 0.34, dmg: 13, spd: 360, bounces: 5, range: 0.60, behavior: 'bounce', sprite: 'bullet_saw', sfx: 'saw', punch: 0.45, desc: 'Bouncing, hungry steel' },
|
||||
bile: { id: 'bile', name: 'Bile Blunderbuss', held: 54, torsoW: 118, torsoFwd: 20, muzzle: 68, tier: 2, ammo: 80, refill: 21, drain: 8, interval: 0.12, dmg: 7, spd: 340, spread: 0.32, range: 0.38, behavior: 'cone', sprite: 'bullet_gore', sfx: 'flame', punch: 0.34, desc: 'A brutal corrosive torrent; pools linger' },
|
||||
hemophage: { id: 'hemophage', name: 'Hemophage', held: 50, torsoW: 116, torsoFwd: 19, muzzle: 67, tier: 1, ammo: 70, refill: 42, interval: 0.19, dmg: 8, spd: 480, lifesteal: 0.5, range: 0.45, behavior: 'bullet', sprite: 'bullet_syringe', sfx: 'default', punch: 0.35, desc: 'Drinks what it touches' },
|
||||
eye: { id: 'eye', name: 'Eye Ballista', held: 58, torsoW: 120, torsoFwd: 20, muzzle: 70, tier: 1, ammo: 42, refill: 21, interval: 0.30, dmg: 10, spd: 320, range: 0.62, behavior: 'homing', sprite: 'bullet_eye', sfx: 'default', punch: 0.40, desc: 'The eyes follow. They always follow' },
|
||||
guthook: { id: 'guthook', name: 'Gut Hook', held: 64, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 2, ammo: 11, refill: 5, interval: 0.85, dmg: 48, spd: 720, range: 0.60, behavior: 'pierce_drag', sprite: 'bullet_harpoon', sfx: 'heavy', punch: 0.95, desc: 'Impales and drags the catch' },
|
||||
cauterizer: { id: 'cauterizer', name: 'The Cauterizer', held: 58, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 3, ammo: 110, refill: 21, drain: 12, interval: 0.05, dmg: 4.5, spd: 310, spread: 0.34, range: 0.36, behavior: 'flame', sprite: 'bullet_gore', sfx: 'flame', punch: 0.32, desc: 'Incinerates anything brave enough to get close' },
|
||||
fleshmasher: { id: 'fleshmasher', name: 'Flesh Masher', held: 62, torsoW: 128, torsoFwd: 22, muzzle: 76, tier: 2, ammo: 14, refill: 4, interval: 0.75, dmg: 34, spd: 999, behavior: 'lob', sprite: 'bullet_gore', sfx: 'lob', punch: 0.90, desc: 'Lobs ground-meat bombs' },
|
||||
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' },
|
||||
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' },
|
||||
};
|
||||
|
||||
// rarity weights per tier, by floor
|
||||
@@ -128,7 +133,8 @@ function fireWeapon(p, w) {
|
||||
}
|
||||
}
|
||||
|
||||
// continuous chainsaw damage while held (def = weapon stats, inst = ammo holder)
|
||||
// continuous chainsaw damage while held (def = weapon stats; the caller owns
|
||||
// the time-denominated ammo drain, so inst is only kept for signature stability)
|
||||
function sawTick(p, def, inst, dt) {
|
||||
const st = p.stats;
|
||||
const angles = [p.aim];
|
||||
@@ -154,7 +160,7 @@ function sawTick(p, def, inst, dt) {
|
||||
if (hitAny) spawnBlood(cx, cy, a, 4);
|
||||
}
|
||||
if (hitAny) Sfx.sawHit({ x: p.x, y: p.y });
|
||||
inst.ammo -= dt * 18 / st.ammoEff;
|
||||
// ammo cost is time-denominated and handled by the caller (player.js)
|
||||
}
|
||||
|
||||
// fire the Spinal Tap beam
|
||||
|
||||
Reference in new issue
Block a user