Cleaver whirl, HD boss first-draw, elite Splitter minis.
This commit is contained in:
Quintine committed 2026-08-15 00:46:35 +10:00
1 parent 8991bde943
commit faa58b5678
14 files changed
+111 -45

No files matched your search

+1 -1
View File
@@ -378,7 +378,7 @@ function killEnemy(e, ang) {
}
// splitter splits
if (ENEMY_TYPES[e.type].splits && !e.elite) {
if (ENEMY_TYPES[e.type].splits) {
Sfx.split(e);
for (let i = 0; i < 2 && G.enemies.length < MAX_ENEMIES; i++) {
const m = makeEnemy('mini', e.x + rand(-18, 18), e.y + rand(-18, 18), G.floor, false);
+11
View File
@@ -385,6 +385,17 @@ function draw() {
}
function drawBossLoad(ctx) {
if (G.cur) {
drawFloor(ctx, G.cur);
drawWalls(ctx, G.cur);
}
const sheet = G.bossLoadSheet && Sprites.get(G.bossLoadSheet);
if (sheet) {
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(sheet, 0, 0);
ctx.restore();
}
ctx.fillStyle = '#0a0506';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
+11 -2
View File
@@ -113,11 +113,20 @@ function spawnDashBurst(x, y, ang) {
function spawnSweepFx(x, y, ang, reach, arc) {
addParticle({ type: 'sweep', x, y, ang, arc, r: 20, maxR: reach, life: 0.18, t: 0 }, true);
addParticle({ type: 'sweep', x, y, ang, arc, r: 20, maxR: reach, life: 0.22, t: 0 }, true);
addParticle({ type: 'sweep', x, y, ang, arc: arc * 0.72, r: 12, maxR: reach * 0.82, life: 0.14, t: 0 }, true);
spawnShockwave(x, y, reach * 0.55, '#8b1020', 0.45);
for (let i = 0; i < 10; i++) {
const a = ang - arc / 2 + (i / 9) * arc;
const d = reach * rand(0.35, 1);
const px = x + Math.cos(a) * d, py = y + Math.sin(a) * d;
spawnBlood(px, py, a, 2, i % 3 === 0);
if (i % 2 === 0) spawnSpark(px, py, a);
}
}
function spawnMuzzleFx(x, y, ang, behavior) {
if (behavior === 'slam') return;
if (behavior === 'slam' || behavior === 'sweep') return;
const back = ang + Math.PI;
if (behavior === 'flame') {
for (let i = 0; i < 3; i++) {
+9 -5
View File
@@ -444,18 +444,22 @@ function drawPlayer(ctx) {
}
const w = WEAPONS[p.weapon.id];
let swingFacing = p.aim;
if (w.id === 'cleaver' && p.attackT > 0) {
swingFacing = p.aim + (1 - clamp(p.attackT / 0.4, 0, 1)) * TAU * 2.5;
}
const kick = p.recoil * 3;
Sprites.shadow(ctx, p.x, p.y + 17, 27 + p.moveBlend * 2, 9, 0.46);
if (p.hp <= 0) {
Sprites.strip(ctx, 'player_death_sheet', p.x, p.y, p.deathT || 0, 12, 14, 128, 144, true);
} else {
Sprites.legs(ctx, p.x, p.y + 4, p.bodyFacing, p.step, 124);
Sprites.legs(ctx, p.x, p.y + 4, w.id === 'cleaver' && p.attackT > 0 ? swingFacing : p.bodyFacing, p.step, 124);
const torsoOffset = (w.torsoFwd || 18) - kick;
const torsoX = p.x + Math.cos(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);
const torsoX = p.x + Math.cos(swingFacing) * torsoOffset;
const torsoY = p.y + Math.sin(swingFacing) * torsoOffset;
Sprites.draw(ctx, 'pt_' + p.weapon.id, torsoX, torsoY, swingFacing, w.torsoW || 116, p.hitT > 0);
}
if (p.hp > 0 && p.muzzleT > 0 && w.behavior !== 'slam' && w.behavior !== 'saw') {
if (p.hp > 0 && p.muzzleT > 0 && w.behavior !== 'slam' && w.behavior !== 'saw' && w.behavior !== 'sweep') {
const muzzleDist = (w.muzzle || 40) - kick;
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; }
+1 -1
View File
@@ -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=60')
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=61')
.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
View File
@@ -88,11 +88,11 @@ 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=60'; return; }
if (hd && !img._sdRetry) { img._sdRetry = true; img.src = 'assets/' + name + '.png?v=61'; return; }
this.imgs[name] = null;
finish();
};
img.src = hd ? 'assets/hd/' + name + '.webp?v=60' : 'assets/' + name + '.png?v=60';
img.src = hd ? 'assets/hd/' + name + '.webp?v=61' : 'assets/' + name + '.png?v=61';
}
},
+5 -2
View File
@@ -11,7 +11,7 @@ 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: 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.55, dmg: 34, behavior: 'sweep', sprite: 'bullet_cleaver', sfx: 'heavy', punch: 0.95, sweepRange: 90, sweepArc: 2.4, desc: 'A wide, dramatic melee sweep' },
cleaver: { id: 'cleaver', name: 'Cleaver Cadence', held: 60, torsoW: 124, torsoFwd: 21, muzzle: 73, tier: 1, ammo: 26, refill: 10, interval: 0.55, dmg: 42, behavior: 'sweep', sprite: 'bullet_cleaver', sfx: 'heavy', punch: 0.95, sweepRange: 110, sweepArc: 2.85, desc: 'A wider, gory whirl of steel' },
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' },
@@ -141,9 +141,12 @@ function fireWeapon(p, w) {
const a = angleTo(p.x, p.y, e.x, e.y);
if (Math.abs(angleDelta(a, ang)) > half) continue;
damageEnemy(e, w.dmg * dmgMul, a, true, { source: 'player' });
spawnBlood(e.x, e.y, a, 8, true);
spawnGibs(e.x, e.y, 3);
addShake(2);
}
spawnSweepFx(p.x, p.y, ang, reach, w.sweepArc || 2.4);
addShake(4);
addShake(6);
break;
}
default: { // bullet, bounce and homing projectiles