0.9.4
feat: large-room blood-pool solids, dash gore burst, GLB weapon refs
This commit is contained in:
1 parent
988e04b2d4
commit
2cf8dc89fe
9 files changed
+325
-13
No files matched your search
+6
-2
@@ -100,6 +100,8 @@ function spawnPosAwayFromPlayer() {
|
||||
const y = rand(a.y0 + 40, a.y1 - 40);
|
||||
if (dist(x, y, p.x, p.y) <= safeDist) continue;
|
||||
if (nearAnyDoor(x, y)) continue;
|
||||
const obstacles = G.cur && G.cur.obstacles;
|
||||
if (obstacles && obstacles.some(o => dist(x, y, o.x, o.y) < 16 + o.r)) continue;
|
||||
return { x, y };
|
||||
}
|
||||
// fallback: a bounded central region, valid even in the thin hall variants
|
||||
@@ -458,8 +460,8 @@ function updateEnemies(dt) {
|
||||
e.x += px; e.y += py; o.x -= px; o.y -= py;
|
||||
}
|
||||
}
|
||||
e.x = clamp(e.x, G.arena.x0 + e.r, G.arena.x1 - e.r);
|
||||
e.y = clamp(e.y, G.arena.y0 + e.r, G.arena.y1 - e.r);
|
||||
const pos = resolveSolids(e.x, e.y, e.r);
|
||||
e.x = pos.x; e.y = pos.y;
|
||||
|
||||
// contact damage
|
||||
if (e.hitT > 0) e.hitT -= dt;
|
||||
@@ -522,6 +524,8 @@ function updateEnemyAI(e, dt) {
|
||||
const behind = p.aim + Math.PI, range = rand(90, 125);
|
||||
e.x = clamp(p.x + Math.cos(behind) * range, G.arena.x0 + e.r, G.arena.x1 - e.r);
|
||||
e.y = clamp(p.y + Math.sin(behind) * range, G.arena.y0 + e.r, G.arena.y1 - e.r);
|
||||
const landed = resolveSolids(e.x, e.y, e.r);
|
||||
e.x = landed.x; e.y = landed.y;
|
||||
e.phased = false; e.alpha = 1; e.ambushState = 'lunge'; e.lungeT = 0.48;
|
||||
e.faceDir = angleTo(e.x, e.y, p.x, p.y); e.attackT = 0.48; e.actionT = 0;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,14 @@ function spawnShockwave(x, y, radius, color, alpha, important) {
|
||||
}, important);
|
||||
}
|
||||
|
||||
function spawnDashBurst(x, y, ang) {
|
||||
addParticle({ type: 'bloodball', x, y, vx: 0, vy: 0, life: 0.18, t: 0, r: 10, maxR: 56 }, true);
|
||||
spawnShockwave(x, y, 52, '#8b1020', 0.75);
|
||||
spawnBlood(x, y, ang, 16, true);
|
||||
spawnGibs(x, y, 7, false);
|
||||
}
|
||||
|
||||
|
||||
function spawnSweepFx(x, y, ang, reach, arc) {
|
||||
addParticle({ type: 'sweep', x, y, ang, arc, r: 20, maxR: reach, life: 0.18, t: 0 }, true);
|
||||
}
|
||||
@@ -249,6 +257,15 @@ function drawParticles(ctx) {
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, radius, radius * 0.72, 0, 0, TAU); ctx.stroke();
|
||||
ctx.globalAlpha = k * 0.18;
|
||||
ctx.fillStyle = p.color; ctx.beginPath(); ctx.ellipse(p.x, p.y, radius * 0.86, radius * 0.58, 0, 0, TAU); ctx.fill();
|
||||
} else if (p.type === 'bloodball') {
|
||||
const progress = 1 - k;
|
||||
const radius = lerp(p.r, p.maxR, progress);
|
||||
ctx.globalAlpha = k * 0.82;
|
||||
ctx.fillStyle = '#6a0b14';
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, radius, radius * 0.72, 0, 0, TAU); ctx.fill();
|
||||
ctx.globalAlpha = k * 0.55;
|
||||
ctx.fillStyle = '#b41828';
|
||||
ctx.beginPath(); ctx.ellipse(p.x, p.y, radius * 0.55, radius * 0.55 * 0.72, 0, 0, TAU); ctx.fill();
|
||||
} else if (p.type === 'beam') {
|
||||
ctx.globalAlpha = k;
|
||||
ctx.strokeStyle = '#d9f6ff'; ctx.lineWidth = 13 * k + 3;
|
||||
|
||||
+11
-6
@@ -87,11 +87,16 @@ function updatePlayer(dt) {
|
||||
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;
|
||||
const sx = p.x, sy = p.y;
|
||||
const stepD = dashD / 8;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
p.x += (mx / len) * stepD;
|
||||
p.y += (my / len) * stepD;
|
||||
const pos = resolveSolids(p.x, p.y, p.r);
|
||||
p.x = pos.x; p.y = pos.y;
|
||||
}
|
||||
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);
|
||||
spawnDashBurst(sx, sy, Math.atan2(my, mx));
|
||||
Sfx.duck();
|
||||
}
|
||||
const spd = 178 * st.speedMul * firing * (p.dashSlowT > 0 ? 0.5 : 1);
|
||||
@@ -107,8 +112,8 @@ function updatePlayer(dt) {
|
||||
p.moveBlend = lerp(p.moveBlend, mx || my ? 1 : 0, clamp(dt * 12, 0, 1));
|
||||
p.recoil = Math.max(0, p.recoil - dt * 7);
|
||||
p.muzzleT = Math.max(0, p.muzzleT - dt);
|
||||
p.x = clamp(p.x, G.arena.x0 + p.r, G.arena.x1 - p.r);
|
||||
p.y = clamp(p.y, G.arena.y0 + p.r, G.arena.y1 - p.r);
|
||||
const pos = resolveSolids(p.x, p.y, p.r);
|
||||
p.x = pos.x; p.y = pos.y;
|
||||
|
||||
// aim
|
||||
p.aim = angleTo(p.x, p.y, mxW(), myW());
|
||||
|
||||
+62
-3
@@ -2,6 +2,9 @@
|
||||
|
||||
const DIRS = { n: [0, -1], s: [0, 1], e: [1, 0], w: [-1, 0] };
|
||||
const OPP = { n: 's', s: 'n', e: 'w', w: 'e' };
|
||||
const OBSTACLE_COUNTS = { grand_hall: 3, deep_hall: 3, meat_hall: 5 };
|
||||
const OBSTACLE_R = 42;
|
||||
|
||||
const ROOM_SHAPES = {
|
||||
hall: { x0: 48, y0: 48, x1: 912, y1: 592 }, // unchanged — start & boss rooms
|
||||
wide_hall: { x0: 48, y0: 176, x1: 912, y1: 464 }, // h 224 -> 288 ("a little wider"; cy stays 320, min dim 288 < 300 keeps censer/broodsac excluded)
|
||||
@@ -64,16 +67,67 @@ function genFloor(floor) {
|
||||
for (const [dir, [dx, dy]] of Object.entries(DIRS)) {
|
||||
r.doors[dir] = !!G.rooms[(r.gx + dx) + ',' + (r.gy + dy)];
|
||||
}
|
||||
placeRoomObstacles(r);
|
||||
}
|
||||
}
|
||||
|
||||
function makeRoom(gx, gy, type) {
|
||||
return { gx, gy, type, shape: 'hall', theme: 'abattoir', doors: {}, cleared: type === 'start',
|
||||
visited: false, decals: [], wavesLeft: 0, spawnT: 0, bossSpawned: false, pickups: [] };
|
||||
visited: false, decals: [], obstacles: [], wavesLeft: 0, spawnT: 0, bossSpawned: false, pickups: [] };
|
||||
}
|
||||
|
||||
function roomKey(x, y) { return x + ',' + y; }
|
||||
|
||||
function nearRoomDoor(room, x, y) {
|
||||
const a = roomBounds(room);
|
||||
const M = Math.min(130, Math.min(a.w, a.h) * 0.32);
|
||||
if (room.doors.n && Math.abs(x - a.cx) < DOOR_HALF + 30 && y < a.y0 + M) return true;
|
||||
if (room.doors.s && Math.abs(x - a.cx) < DOOR_HALF + 30 && y > a.y1 - M) return true;
|
||||
if (room.doors.e && Math.abs(y - a.cy) < DOOR_HALF + 30 && x > a.x1 - M) return true;
|
||||
if (room.doors.w && Math.abs(y - a.cy) < DOOR_HALF + 30 && x < a.x0 + M) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function placeRoomObstacles(room) {
|
||||
room.obstacles = [];
|
||||
const count = OBSTACLE_COUNTS[room.shape];
|
||||
if (!count) return;
|
||||
const a = roomBounds(room);
|
||||
for (let tries = 0; tries < 80 && room.obstacles.length < count; tries++) {
|
||||
const x = rand(a.x0 + 80, a.x1 - 80);
|
||||
const y = rand(a.y0 + 80, a.y1 - 80);
|
||||
if (nearRoomDoor(room, x, y)) continue;
|
||||
if (dist(x, y, a.cx, a.cy) < 110) continue;
|
||||
if (room.obstacles.some(o => dist(x, y, o.x, o.y) < OBSTACLE_R * 2 + 24)) continue;
|
||||
room.obstacles.push({ x, y, r: OBSTACLE_R, kind: 'bloodpool' });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSolids(x, y, r) {
|
||||
const obstacles = G.cur && G.cur.obstacles;
|
||||
if (obstacles && obstacles.length) {
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
for (const o of obstacles) {
|
||||
const d = dist(x, y, o.x, o.y);
|
||||
if (d < r + o.r) {
|
||||
if (d > 0.01) {
|
||||
const n = (r + o.r) / d;
|
||||
x = o.x + (x - o.x) * n;
|
||||
y = o.y + (y - o.y) * n;
|
||||
} else {
|
||||
x = o.x + r + o.r;
|
||||
y = o.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
x = clamp(x, G.arena.x0 + r, G.arena.x1 - r);
|
||||
y = clamp(y, G.arena.y0 + r, G.arena.y1 - r);
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
|
||||
function wavesFor(floor) {
|
||||
const base = 3 + Math.ceil(floor * 1.4);
|
||||
const waves = [base];
|
||||
@@ -110,8 +164,9 @@ function enterRoom(gx, gy) {
|
||||
// spawn protection: brief invincibility walking into a fresh room
|
||||
if (G.player) {
|
||||
G.player.invT = Math.max(G.player.invT, 1.0);
|
||||
G.player.x = clamp(G.player.x, a.x0 + G.player.r, a.x1 - G.player.r);
|
||||
G.player.y = clamp(G.player.y, a.y0 + G.player.r, a.y1 - G.player.r);
|
||||
const pos = resolveSolids(G.player.x, G.player.y, G.player.r);
|
||||
G.player.x = pos.x;
|
||||
G.player.y = pos.y;
|
||||
}
|
||||
// Iron Lung: re-arm the once-per-room hit block
|
||||
if (G.player && G.player.stats.ironLung > 0) G.player.ironLungReady = true;
|
||||
@@ -420,6 +475,10 @@ function drawRoom(ctx) {
|
||||
for (const d of r.decals) {
|
||||
Sprites.draw(ctx, d.img, d.x, d.y, d.rot, 48 * d.s, false, 0.55);
|
||||
}
|
||||
for (const o of r.obstacles) {
|
||||
Sprites.draw(ctx, 'decal_blood1', o.x, o.y, 0, o.r * 2.4, false, 0.92);
|
||||
Sprites.draw(ctx, 'decal_blood3', o.x, o.y, 0.7, o.r * 1.7, false, 0.75);
|
||||
}
|
||||
// hazards
|
||||
for (const h of G.hazards) {
|
||||
const k = 1 - h.t / h.life;
|
||||
|
||||
Reference in new issue
Block a user