feat: dash ability; ammo pressure scaling, full-HP hearts, muzzle alignment, holstered debug ammo
- Shift dash: 0.45*v*0.6 instant burst then 0.6s of 50% slow (net 95% of walking distance); slow doubles as cooldown, no dash while idle/slowed. - Generic ammo refills scale with the live pressure dial at 80% of the HP multiplier via ammoPressureMul(). - Hearts at full HP stay on the floor (magnet ignores them) instead of converting to score. - Muzzle audit: flame FX and pellets share one right-hand origin, Spinal Tap beam starts at the muzzle, saw flash suppressed (blade sparks are the effect). - Debug REFILL/INF AMMO and the ammo row now cover a spent special holstered behind the Bone Popper. - Sfx.duck() defaults its args; a no-arg call crashed the loop with a non-finite AudioParam value (found via the dash E2E).
This commit is contained in:
1 parent
684a54f5ad
commit
4b9e4a1e97
6 files changed
+49
-24
No files matched your search
+10
-3
@@ -211,6 +211,13 @@ function dbgRenderPlayer(ctx, b) {
|
|||||||
dbgText(ctx, 'Mutating this tab taints the run — best score will not be saved.', b.x, y, '#7d5a58', hfont(8));
|
dbgText(ctx, 'Mutating this tab taints the run — best score will not be saved.', b.x, y, '#7d5a58', hfont(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debugRefillAmmo() {
|
||||||
|
const p = G.player;
|
||||||
|
for (const inst of [p.weapon, p.holstered]) {
|
||||||
|
if (inst && inst.ammo !== Infinity) inst.ammo = WEAPONS[inst.id].ammo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dbgRenderWeapons(ctx, b) {
|
function dbgRenderWeapons(ctx, b) {
|
||||||
const p = G.player;
|
const p = G.player;
|
||||||
const lw = 320;
|
const lw = 320;
|
||||||
@@ -232,12 +239,12 @@ function dbgRenderWeapons(ctx, b) {
|
|||||||
p.charge = 0; p.fireT = 0;
|
p.charge = 0; p.fireT = 0;
|
||||||
addToast('DEBUG', WEAPONS[wid].name);
|
addToast('DEBUG', WEAPONS[wid].name);
|
||||||
});
|
});
|
||||||
act('REFILL AMMO', () => { if (p.weapon.ammo !== Infinity) p.weapon.ammo = WEAPONS[p.weapon.id].ammo; });
|
act('REFILL AMMO', () => { debugRefillAmmo(); });
|
||||||
act('HOLSTER CURRENT', () => { if (p.weapon.id !== 'bonepopper') { p.holstered = p.weapon; p.weapon = { id: 'bonepopper', ammo: Infinity }; } });
|
act('HOLSTER CURRENT', () => { if (p.weapon.id !== 'bonepopper') { p.holstered = p.weapon; p.weapon = { id: 'bonepopper', ammo: Infinity }; } });
|
||||||
act('DROP AT CURSOR', () => { const c = dbgCursor(); spawnPickup('weapon', c.x, c.y, { wid }); });
|
act('DROP AT CURSOR', () => { const c = dbgCursor(); spawnPickup('weapon', c.x, c.y, { wid }); });
|
||||||
act('SPAWN ITEM PEDESTAL', () => { const c = dbgCursor(); spawnItemPedestal(c.x, c.y, null, 'room'); });
|
act('SPAWN ITEM PEDESTAL', () => { const c = dbgCursor(); spawnItemPedestal(c.x, c.y, null, 'room'); });
|
||||||
ay += 4;
|
ay += 4;
|
||||||
dbgNumRow(ctx, 'ammo', ax, ay, b.w - lw - 20, () => (p.weapon.ammo === Infinity ? 999 : p.weapon.ammo), v => { debugTaint(); if (p.weapon.ammo !== Infinity) p.weapon.ammo = Math.max(0, v); }, 10, 0, 999);
|
dbgNumRow(ctx, 'ammo', ax, ay, b.w - lw - 20, () => (p.weapon.ammo !== Infinity ? p.weapon.ammo : (p.holstered && p.holstered.ammo !== Infinity ? p.holstered.ammo : 999)), v => { debugTaint(); const t = p.weapon.ammo !== Infinity ? p.weapon : (p.holstered && p.holstered.ammo !== Infinity ? p.holstered : null); if (t) t.ammo = Math.max(0, v); }, 10, 0, 999);
|
||||||
dbgText(ctx, 'Equip swaps the live weapon; the old one goes to the holster unless it was the Bone Popper.', ax, ay + 32, '#6f5c58', hfont(8));
|
dbgText(ctx, 'Equip swaps the live weapon; the old one goes to the holster unless it was the Bone Popper.', ax, ay + 32, '#6f5c58', hfont(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,7 +535,7 @@ const DEBUG_PAGES = [
|
|||||||
// ---- top-level update / draw ----
|
// ---- top-level update / draw ----
|
||||||
function updateDebug() {
|
function updateDebug() {
|
||||||
// infinite-ammo upkeep (also runs while pinned so live play benefits)
|
// infinite-ammo upkeep (also runs while pinned so live play benefits)
|
||||||
if (G.debugFlags.infAmmo && G.player && G.player.weapon.ammo !== Infinity) G.player.weapon.ammo = WEAPONS[G.player.weapon.id].ammo;
|
if (G.debugFlags.infAmmo && G.player) for (const inst of [G.player.weapon, G.player.holstered]) if (inst && inst.ammo !== Infinity) inst.ammo = WEAPONS[inst.id].ammo;
|
||||||
|
|
||||||
// wall-clock fps counter for the stats tab (independent of timescale/freeze)
|
// wall-clock fps counter for the stats tab (independent of timescale/freeze)
|
||||||
DBG.fpsFrames++;
|
DBG.fpsFrames++;
|
||||||
|
|||||||
+9
-8
@@ -53,15 +53,14 @@ function updatePickups(dt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// gems & small pickups fly to the player inside magnet radius
|
// gems & small pickups fly to the player inside magnet radius
|
||||||
if ((k.type === 'gem' || k.type === 'heart' || k.type === 'ammo') && d < magR) {
|
if ((k.type === 'gem' || k.type === 'ammo' || (k.type === 'heart' && p.hp < p.stats.maxHp)) && d < magR) {
|
||||||
const a = angleTo(k.x, k.y, p.x, p.y);
|
const a = angleTo(k.x, k.y, p.x, p.y);
|
||||||
const sp = lerp(320, 60, d / magR) * p.stats.magnetPull;
|
const sp = lerp(320, 60, d / magR) * p.stats.magnetPull;
|
||||||
k.x += Math.cos(a) * sp * dt; k.y += Math.sin(a) * sp * dt;
|
k.x += Math.cos(a) * sp * dt; k.y += Math.sin(a) * sp * dt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d < p.r + 12 && !(k.delay > 0) && k.type !== 'itemspot') {
|
if (d < p.r + 12 && !(k.delay > 0) && k.type !== 'itemspot') {
|
||||||
collectPickup(k);
|
if (collectPickup(k) !== false) G.pickups.splice(i, 1);
|
||||||
G.pickups.splice(i, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,10 +73,11 @@ function collectPickup(k) {
|
|||||||
Sfx.gem();
|
Sfx.gem();
|
||||||
break;
|
break;
|
||||||
case 'heart': {
|
case 'heart': {
|
||||||
// Glutton's Gut: hearts heal +1 extra per tier; overheal becomes score
|
// 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;
|
||||||
const healAmt = 2 + (p.stats.gluttonGut || 0);
|
const healAmt = 2 + (p.stats.gluttonGut || 0);
|
||||||
if (p.hp < p.stats.maxHp) { healPlayer(healAmt); spawnText(p.x, p.y, '+' + healAmt, '#d92038'); }
|
healPlayer(healAmt); spawnText(p.x, p.y, '+' + healAmt, '#d92038');
|
||||||
else addScore(25 + (p.stats.gluttonGut || 0) * 10);
|
|
||||||
Sfx.heart();
|
Sfx.heart();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ function collectPickup(k) {
|
|||||||
for (const inst of carried) {
|
for (const inst of carried) {
|
||||||
const def = WEAPONS[inst.id];
|
const def = WEAPONS[inst.id];
|
||||||
const cap = def.ammo * 1.5;
|
const cap = def.ammo * 1.5;
|
||||||
const wanted = Math.max(1, Math.ceil((def.refill || 1) * p.stats.ammoPickupMul));
|
const wanted = Math.max(1, Math.ceil((def.refill || 1) * p.stats.ammoPickupMul * ammoPressureMul()));
|
||||||
const add = Math.max(0, Math.min(wanted, cap - inst.ammo));
|
const add = Math.max(0, Math.min(wanted, cap - inst.ammo));
|
||||||
inst.ammo += add;
|
inst.ammo += add;
|
||||||
total += add;
|
total += add;
|
||||||
@@ -115,7 +115,7 @@ function collectPickup(k) {
|
|||||||
spawnText(p.x, p.y, 'AMMO FULL', '#d0b060');
|
spawnText(p.x, p.y, 'AMMO FULL', '#d0b060');
|
||||||
} else {
|
} else {
|
||||||
const cap = w.ammo * 1.5;
|
const cap = w.ammo * 1.5;
|
||||||
const wanted = Math.max(1, Math.ceil((w.refill || 1) * p.stats.ammoPickupMul));
|
const wanted = Math.max(1, Math.ceil((w.refill || 1) * p.stats.ammoPickupMul * ammoPressureMul()));
|
||||||
const add = Math.max(0, Math.min(wanted, cap - match.ammo));
|
const add = Math.max(0, Math.min(wanted, cap - match.ammo));
|
||||||
match.ammo += add;
|
match.ammo += add;
|
||||||
if (wanted > add) addScore(Math.round((wanted - add) * 2));
|
if (wanted > add) addScore(Math.round((wanted - add) * 2));
|
||||||
@@ -161,6 +161,7 @@ function collectPickup(k) {
|
|||||||
nextFloor();
|
nextFloor();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// readable nameplate shown when the player walks up to a pickup
|
// readable nameplate shown when the player walks up to a pickup
|
||||||
|
|||||||
+18
-4
@@ -1,5 +1,7 @@
|
|||||||
// ---- the butcher (player) ----
|
// ---- the butcher (player) ----
|
||||||
|
|
||||||
|
const DASH_SLOW_T = 0.6; // seconds of 50% move speed after a dash; doubles as the dash cooldown
|
||||||
|
|
||||||
function defaultPlayerStats() {
|
function defaultPlayerStats() {
|
||||||
return {
|
return {
|
||||||
dmgMul: 1, rateMul: 1, speedMul: 1, shotSpeedMul: 1, rangeMul: 1,
|
dmgMul: 1, rateMul: 1, speedMul: 1, shotSpeedMul: 1, rangeMul: 1,
|
||||||
@@ -28,7 +30,7 @@ function initPlayer() {
|
|||||||
x: W / 2, y: H / 2, r: 20,
|
x: W / 2, y: H / 2, r: 20,
|
||||||
hp: 6, shieldHp: 0,
|
hp: 6, shieldHp: 0,
|
||||||
aim: 0, bodyFacing: 0, fireT: 0, charge: 0, invT: 0, frenzyT: 0,
|
aim: 0, bodyFacing: 0, fireT: 0, charge: 0, invT: 0, frenzyT: 0,
|
||||||
step: 0, moveBlend: 0, recoil: 0, muzzleT: 0,
|
step: 0, moveBlend: 0, recoil: 0, muzzleT: 0, dashSlowT: 0,
|
||||||
animT: 0, attackT: 0, actionT: 0, hitT: 0, deathT: 0,
|
animT: 0, attackT: 0, actionT: 0, hitT: 0, deathT: 0,
|
||||||
weapon: { id: 'bonepopper', ammo: Infinity },
|
weapon: { id: 'bonepopper', ammo: Infinity },
|
||||||
holstered: null, // a special weapon set aside while the pistol is out
|
holstered: null, // a special weapon set aside while the pistol is out
|
||||||
@@ -79,9 +81,20 @@ function updatePlayer(dt) {
|
|||||||
if (keyDown('s', 'arrowdown')) my += 1;
|
if (keyDown('s', 'arrowdown')) my += 1;
|
||||||
if (keyDown('a', 'arrowleft')) mx -= 1;
|
if (keyDown('a', 'arrowleft')) mx -= 1;
|
||||||
if (keyDown('d', 'arrowright')) mx += 1;
|
if (keyDown('d', 'arrowright')) mx += 1;
|
||||||
|
if (p.dashSlowT > 0) p.dashSlowT -= dt;
|
||||||
if (mx || my) {
|
if (mx || my) {
|
||||||
const len = Math.hypot(mx, my);
|
const len = Math.hypot(mx, my);
|
||||||
const spd = 178 * st.speedMul * (held ? (w.id === 'bonepopper' ? 0.95 : 0.85) : 1);
|
const firing = held ? (w.id === 'bonepopper' ? 0.95 : 0.85) : 1;
|
||||||
|
if (keyPressed('shift') && p.dashSlowT <= 0) {
|
||||||
|
const dashD = 0.45 * (178 * st.speedMul * firing) * DASH_SLOW_T;
|
||||||
|
p.x += (mx / len) * dashD;
|
||||||
|
p.y += (my / len) * dashD;
|
||||||
|
p.dashSlowT = DASH_SLOW_T;
|
||||||
|
spawnSmoke(p.x, p.y, Math.atan2(-my, -mx), 6, '#9b8d84');
|
||||||
|
spawnShockwave(p.x, p.y, 26, '#d8c9a8', 0.35);
|
||||||
|
Sfx.duck();
|
||||||
|
}
|
||||||
|
const spd = 178 * st.speedMul * firing * (p.dashSlowT > 0 ? 0.5 : 1);
|
||||||
p.x += (mx / len) * spd * dt;
|
p.x += (mx / len) * spd * dt;
|
||||||
p.y += (my / len) * spd * dt;
|
p.y += (my / len) * spd * dt;
|
||||||
p.step += dt * 9 * Math.min(st.speedMul, 1.8);
|
p.step += dt * 9 * Math.min(st.speedMul, 1.8);
|
||||||
@@ -436,9 +449,10 @@ function drawPlayer(ctx) {
|
|||||||
const torsoY = p.y + Math.sin(p.aim) * torsoOffset;
|
const torsoY = p.y + Math.sin(p.aim) * torsoOffset;
|
||||||
Sprites.draw(ctx, 'pt_' + p.weapon.id, torsoX, torsoY, p.aim, w.torsoW || 116, p.hitT > 0);
|
Sprites.draw(ctx, 'pt_' + p.weapon.id, torsoX, torsoY, p.aim, w.torsoW || 116, p.hitT > 0);
|
||||||
}
|
}
|
||||||
if (p.hp > 0 && p.muzzleT > 0 && w.behavior !== 'slam') {
|
if (p.hp > 0 && p.muzzleT > 0 && w.behavior !== 'slam' && w.behavior !== 'saw') {
|
||||||
const muzzleDist = (w.muzzle || 40) - kick;
|
const muzzleDist = (w.muzzle || 40) - kick;
|
||||||
const mx = p.x + Math.cos(p.aim) * muzzleDist, my = p.y + Math.sin(p.aim) * muzzleDist;
|
let mx = p.x + Math.cos(p.aim) * muzzleDist, my = p.y + Math.sin(p.aim) * muzzleDist;
|
||||||
|
if (w.behavior === 'flame') { const rh = p.aim + Math.PI / 2; mx += Math.cos(rh) * 26; my += Math.sin(rh) * 26; }
|
||||||
ctx.save(); ctx.translate(mx, my); ctx.rotate(p.aim);
|
ctx.save(); ctx.translate(mx, my); ctx.rotate(p.aim);
|
||||||
ctx.globalAlpha = p.muzzleT / 0.07;
|
ctx.globalAlpha = p.muzzleT / 0.07;
|
||||||
ctx.fillStyle = '#ffe6a0';
|
ctx.fillStyle = '#ffe6a0';
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const Sfx = {
|
|||||||
try { audio._sfxSource.disconnect(); } catch (e) {}
|
try { audio._sfxSource.disconnect(); } catch (e) {}
|
||||||
// The owning Music entry is discarded immediately; never re-wrap this element.
|
// The owning Music entry is discarded immediately; never re-wrap this element.
|
||||||
},
|
},
|
||||||
duck(amount, duration) {
|
duck(amount = 0.3, duration = 0.15) {
|
||||||
if (!this.ctx || !this.duckGain || G.muted) return;
|
if (!this.ctx || !this.duckGain || G.muted) return;
|
||||||
const t = this.ctx.currentTime, gain = this.duckGain.gain;
|
const t = this.ctx.currentTime, gain = this.duckGain.gain;
|
||||||
if (gain.cancelAndHoldAtTime) gain.cancelAndHoldAtTime(t);
|
if (gain.cancelAndHoldAtTime) gain.cancelAndHoldAtTime(t);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ const G = {
|
|||||||
const PERK_POWER_WEIGHT = 0.5;
|
const PERK_POWER_WEIGHT = 0.5;
|
||||||
// ammo economy: global dial for drop frequency (refill size is per-weapon)
|
// ammo economy: global dial for drop frequency (refill size is per-weapon)
|
||||||
const AMMO_DROP_SCALE = 0.75;
|
const AMMO_DROP_SCALE = 0.75;
|
||||||
|
// Generic ammo refills track the live pressure dial at 80% of the HP scaling.
|
||||||
|
function ammoPressureMul() { return 1 + (G.pressure - 1) * 0.8; }
|
||||||
const STUN_UNIT = 0.03 / 0.97; // Sine Weave raw rating per stack, so the first stack = 3% stagger chance
|
const STUN_UNIT = 0.03 / 0.97; // Sine Weave raw rating per stack, so the first stack = 3% stagger chance
|
||||||
const PRESSURE_MIN = 0.60, PRESSURE_MAX = 2.00;
|
const PRESSURE_MIN = 0.60, PRESSURE_MAX = 2.00;
|
||||||
// seconds a player-dropped weapon stays un-pickable (prevents instant swap loops)
|
// seconds a player-dropped weapon stays un-pickable (prevents instant swap loops)
|
||||||
|
|||||||
+9
-8
@@ -48,6 +48,10 @@ function fireWeapon(p, w) {
|
|||||||
const ang = p.aim;
|
const ang = p.aim;
|
||||||
const muzzle = w.muzzle || 35;
|
const muzzle = w.muzzle || 35;
|
||||||
let bx = p.x + Math.cos(ang) * muzzle, by = p.y + Math.sin(ang) * muzzle;
|
let bx = p.x + Math.cos(ang) * muzzle, by = p.y + Math.sin(ang) * muzzle;
|
||||||
|
if (w.behavior === 'flame') {
|
||||||
|
const rh = ang + Math.PI / 2;
|
||||||
|
bx += Math.cos(rh) * 26; by += Math.sin(rh) * 26; // right-hand hold: FX and bullets share one origin
|
||||||
|
}
|
||||||
const dmgMul = st.dmgMul * st.dmgLiveMul;
|
const dmgMul = st.dmgMul * st.dmgLiveMul;
|
||||||
const spdMul = st.shotSpeedMul;
|
const spdMul = st.shotSpeedMul;
|
||||||
spawnMuzzleFx(bx, by, ang, w.behavior);
|
spawnMuzzleFx(bx, by, ang, w.behavior);
|
||||||
@@ -86,10 +90,6 @@ function fireWeapon(p, w) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'cone': case 'flame': {
|
case 'cone': case 'flame': {
|
||||||
if (w.behavior === 'flame') {
|
|
||||||
const rh = ang + Math.PI / 2;
|
|
||||||
bx += Math.cos(rh) * 26; by += Math.sin(rh) * 26; // right-hand hold: origin right of the body
|
|
||||||
}
|
|
||||||
for (const center of volley) {
|
for (const center of volley) {
|
||||||
for (let i = 0; i < 2; i++) mk(center + rand(-w.spread, w.spread), { pierce: 2 + st.pierce, r: 6 * st.sizeMul });
|
for (let i = 0; i < 2; i++) mk(center + rand(-w.spread, w.spread), { pierce: 2 + st.pierce, r: 6 * st.sizeMul });
|
||||||
}
|
}
|
||||||
@@ -173,6 +173,7 @@ function sawTick(p, def, inst, dt) {
|
|||||||
// fire the Spinal Tap beam
|
// fire the Spinal Tap beam
|
||||||
function fireBeam(p, def, charge) {
|
function fireBeam(p, def, charge) {
|
||||||
const st = p.stats;
|
const st = p.stats;
|
||||||
|
const muzzle = def.muzzle || 35;
|
||||||
const angles = [p.aim];
|
const angles = [p.aim];
|
||||||
for (let i = 1; i <= st.split; i++) angles.push(p.aim + (i % 2 ? 1 : -1) * 0.025);
|
for (let i = 1; i <= st.split; i++) angles.push(p.aim + (i % 2 ? 1 : -1) * 0.025);
|
||||||
for (let i = 1; i <= st.fan; i++) { angles.push(p.aim + 0.16 * i); angles.push(p.aim - 0.16 * i); }
|
for (let i = 1; i <= st.fan; i++) { angles.push(p.aim + 0.16 * i); angles.push(p.aim - 0.16 * i); }
|
||||||
@@ -182,16 +183,16 @@ function fireBeam(p, def, charge) {
|
|||||||
const dmg = def.dmg * st.dmgMul * st.dmgLiveMul * (0.4 + 0.6 * charge) * inert;
|
const dmg = def.dmg * st.dmgMul * st.dmgLiveMul * (0.4 + 0.6 * charge) * inert;
|
||||||
const width = 14 * Math.sqrt(st.sizeMul);
|
const width = 14 * Math.sqrt(st.sizeMul);
|
||||||
for (const ang of angles) {
|
for (const ang of angles) {
|
||||||
const x2 = p.x + Math.cos(ang) * len, y2 = p.y + Math.sin(ang) * len;
|
const x0 = p.x + Math.cos(ang) * muzzle, y0 = p.y + Math.sin(ang) * muzzle;
|
||||||
|
const x2 = x0 + Math.cos(ang) * len, y2 = y0 + Math.sin(ang) * len;
|
||||||
for (const e of G.enemies) {
|
for (const e of G.enemies) {
|
||||||
if (distToSegment(e.x, e.y, p.x, p.y, x2, y2) < e.r + width) {
|
if (distToSegment(e.x, e.y, x0, y0, x2, y2) < e.r + width) {
|
||||||
damageEnemy(e, dmg, ang, true, { source: 'player' });
|
damageEnemy(e, dmg, ang, true, { source: 'player' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
spawnBeam(p.x, p.y, ang, len);
|
spawnBeam(x0, y0, ang, len);
|
||||||
}
|
}
|
||||||
const ang = p.aim;
|
const ang = p.aim;
|
||||||
const muzzle = def.muzzle || 35;
|
|
||||||
spawnMuzzleFx(p.x + Math.cos(ang) * muzzle, p.y + Math.sin(ang) * muzzle, ang, 'beam');
|
spawnMuzzleFx(p.x + Math.cos(ang) * muzzle, p.y + Math.sin(ang) * muzzle, ang, 'beam');
|
||||||
p.recoil = 1;
|
p.recoil = 1;
|
||||||
p.muzzleT = 0.11;
|
p.muzzleT = 0.11;
|
||||||
|
|||||||
Reference in new issue
Block a user