feat: room-entry reaction buffer, 1.5s perk-selection lock, nearby-item toasts, Sine Weave stagger perk
This commit is contained in:
1 parent
97ab316304
commit
5d5eb3d265
8 files changed
+122
-16
No files matched your search
+11
-3
@@ -108,7 +108,7 @@ function spawnPosAwayFromPlayer() {
|
||||
return { x: rand(a.cx - rx, a.cx + rx), y: rand(a.cy - ry, a.cy + ry) };
|
||||
}
|
||||
|
||||
function spawnWave(count, floor) {
|
||||
function spawnWave(count, floor, warn) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (G.enemies.length >= MAX_ENEMIES) break;
|
||||
const type = pickWaveEnemyType(floor);
|
||||
@@ -116,7 +116,7 @@ function spawnWave(count, floor) {
|
||||
const canElite = type !== 'bulwark' && type !== 'broodsac';
|
||||
const elite = canElite && chance(Math.min(0.03 + floor * 0.02, 0.2));
|
||||
const e = makeEnemy(type, pos.x, pos.y, floor, elite);
|
||||
e.warmT = SPAWN_WARN; // telegraph the incoming spawn so the player isn't blindsided
|
||||
e.warmT = warn || SPAWN_WARN; // telegraph the incoming spawn so the player isn't blindsided
|
||||
G.enemies.push(e);
|
||||
spawnBlood(pos.x, pos.y, rand(0, TAU), 4);
|
||||
Sfx.spawn(e);
|
||||
@@ -195,6 +195,11 @@ function damageEnemy(e, dmg, ang, knockback, opts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Diminishing (armor-style) conversion of a raw stun rating into a stagger chance.
|
||||
function stunChance(raw) {
|
||||
return Math.min(0.50, Math.max(0, raw) / (1 + Math.max(0, raw)));
|
||||
}
|
||||
|
||||
// Shared player-hit payload. Every weapon primitive routes through damageEnemy,
|
||||
// so items remain useful for bullets, beams, slams, saws, traps and orbitals.
|
||||
function procOnHit(e, dmg, ang, opts) {
|
||||
@@ -243,7 +248,10 @@ function procOnHit(e, dmg, ang, opts) {
|
||||
e.burnDps = Math.max(e.burnDps || 0, 5 * s.dmgMul);
|
||||
}
|
||||
if (s.slowOnHit > 0 && chance(clamp(s.slowOnHit * chanceScale, 0, 0.9))) e.slowT = Math.max(e.slowT || 0, 1.5);
|
||||
if (!e.boss && e.type !== 'bulwark' && s.stunOnHit > 0 && chance(clamp(s.stunOnHit * chanceScale, 0, 0.75))) e.stunT = Math.max(e.stunT || 0, 0.35);
|
||||
if (!e.boss && e.type !== 'bulwark') {
|
||||
const stunCh = clamp((s.stunOnHit || 0) * chanceScale, 0, 0.75) + stunChance(s.stunRaw || 0);
|
||||
if (stunCh > 0 && chance(clamp(stunCh, 0, 0.75))) e.stunT = Math.max(e.stunT || 0, 0.35);
|
||||
}
|
||||
if (s.pullOnHit > 0 && !e.boss && chance(clamp(s.pullOnHit * chanceScale, 0, 0.8))) {
|
||||
const toward = angleTo(e.x, e.y, p.x, p.y);
|
||||
e.vx += Math.cos(toward) * 170; e.vy += Math.sin(toward) * 170;
|
||||
|
||||
+1
-1
@@ -220,7 +220,7 @@ function update(dt) {
|
||||
drawToastsUpdate(dt);
|
||||
break;
|
||||
case 'levelup':
|
||||
updateLevelup();
|
||||
updateLevelup(dt);
|
||||
updateParticles(dt);
|
||||
break;
|
||||
case 'debug':
|
||||
|
||||
+10
-7
@@ -23,7 +23,7 @@ const PERKS = [
|
||||
{ id: 'boneknit', name: 'Bone Knit', desc: '3% chance to heal ½ heart after combat rooms', apply: s => { s.roomHealChance += 0.03; } },
|
||||
{ id: 'spiteflesh',name: 'Spite Flesh', desc: 'Contact attackers take damage', apply: s => { s.thorns += 4; } },
|
||||
{ id: 'carrion', name: 'Carrion Sense', desc: '+8% pickup radius and pull speed', apply: s => { s.magnet *= 1.08; s.magnetPull *= 1.08; } },
|
||||
{ id: 'sinew', name: 'Sinew Weave', desc: '+3% move speed and range', apply: s => { s.speedMul *= 1.03; s.rangeMul *= 1.03; } },
|
||||
{ id: 'sinew', name: 'Sinew Weave', desc: 'Hits may stagger enemies (starts 3%, up to 50%)', apply: s => { s.stunRaw = (s.stunRaw || 0) + STUN_UNIT; } },
|
||||
];
|
||||
|
||||
const PERK_CARD_W = 170, PERK_CARD_H = 230, PERK_CARD_GAP = 18;
|
||||
@@ -61,6 +61,7 @@ function openPerkDraft() {
|
||||
}
|
||||
const opts = shuffle([...PERKS]).slice(0, 3);
|
||||
G.perkChoices = opts;
|
||||
if (G.mode !== 'levelup') G.selectionLock = 1.5;
|
||||
G.mode = 'levelup';
|
||||
Sfx.levelup();
|
||||
}
|
||||
@@ -82,11 +83,13 @@ function choosePerk(i) {
|
||||
else G.mode = 'play';
|
||||
}
|
||||
|
||||
function updateLevelup() {
|
||||
if (keyPressed('1')) choosePerk(0);
|
||||
else if (keyPressed('2')) choosePerk(1);
|
||||
else if (keyPressed('3')) choosePerk(2);
|
||||
else if (keyPressed('4', ' ')) choosePerk(irand(0, G.perkChoices.length - 1));
|
||||
function updateLevelup(dt) {
|
||||
G.selectionLock = Math.max(0, (G.selectionLock || 0) - dt);
|
||||
const unlocked = G.selectionLock <= 0;
|
||||
if (unlocked && keyPressed('1')) choosePerk(0);
|
||||
else if (unlocked && keyPressed('2')) choosePerk(1);
|
||||
else if (unlocked && keyPressed('3')) choosePerk(2);
|
||||
else if (unlocked && keyPressed('4', ' ')) choosePerk(irand(0, G.perkChoices.length - 1));
|
||||
else if (keyPressed('t')) {
|
||||
setAutoPerk(true);
|
||||
G.mode = 'play';
|
||||
@@ -97,7 +100,7 @@ function updateLevelup() {
|
||||
G.perkChoices = shuffle([...PERKS]).slice(0, 3);
|
||||
Sfx.menu();
|
||||
}
|
||||
else if (Input.mpressed && G.perkChoices) {
|
||||
else if (unlocked && Input.mpressed && G.perkChoices) {
|
||||
// click a card
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cx = perkCardX(i);
|
||||
|
||||
@@ -40,6 +40,18 @@ function updatePickups(dt) {
|
||||
k.vx *= 0.9; k.vy *= 0.9;
|
||||
const d = dist(k.x, k.y, p.x, p.y);
|
||||
|
||||
// nearby pickup: toast the description once per approach, re-arm on leaving
|
||||
if (k.type === 'item' || k.type === 'active' || k.type === 'weapon') {
|
||||
if (d < 90 && !k.toastNear) {
|
||||
k.toastNear = true;
|
||||
if (k.type === 'item') addToast(ITEMS[k.iid].name, ITEMS[k.iid].desc, 3.5);
|
||||
else if (k.type === 'active') addToast(ACTIVES[k.aid].name, ACTIVES[k.aid].desc, 3.5);
|
||||
else addToast(WEAPONS[k.wid].name, WEAPONS[k.wid].desc, 3.5);
|
||||
} else if (d >= 90 && k.toastNear) {
|
||||
k.toastNear = false; // re-arm once the player leaves the 90px radius
|
||||
}
|
||||
}
|
||||
|
||||
// gems & small pickups fly to the player inside magnet radius
|
||||
if ((k.type === 'gem' || k.type === 'heart' || k.type === 'ammo') && d < magR) {
|
||||
const a = angleTo(k.x, k.y, p.x, p.y);
|
||||
|
||||
@@ -6,6 +6,7 @@ function defaultPlayerStats() {
|
||||
pierce: 0, bounce: 0, homing: 0, split: 0, fan: 0, rear: 0, splinter: 0,
|
||||
orbitals: 0, lifestealChance: 0, explodeOnKill: 0,
|
||||
armor: 0, luck: 0, magnet: 1, magnetPull: 1, xpMul: 1, crit: 0.05, critMul: 2, bloodlust: 0,
|
||||
stunRaw: 0,
|
||||
bleed: 0, igniteChance: 0, slowOnHit: 0, stunOnHit: 0, acidOnHit: 0, pullOnHit: 0,
|
||||
chain: 0, knockbackMul: 1, invBonus: 0, ammoEff: 1, ammoPickupMul: 1,
|
||||
roomHeal: 0, roomHealChance: 0, thorns: 0, retaliate: 0, sizeMul: 1, mortar: 0, frenzy: 0,
|
||||
|
||||
+21
-1
@@ -16,6 +16,9 @@ const ROOM_THEMES = {
|
||||
flesh: { floor: ['tile_floor8', 'tile_floor8', 'tile_floor1'], wall: ['tile_wall4', 'tile_wall2'] },
|
||||
};
|
||||
|
||||
const ENTRY_WARN = 0.75; // first-entry wave telegraph seconds (~750ms reaction)
|
||||
const ENTRY_BUFFER = 170; // min px between player and a freshly-entered enemy
|
||||
|
||||
function roomBounds(room) {
|
||||
const def = ROOM_SHAPES[(room && room.shape) || 'hall'] || ROOM_SHAPES.hall;
|
||||
return { x0: def.x0, y0: def.y0, x1: def.x1, y1: def.y1,
|
||||
@@ -88,6 +91,7 @@ function enterRoom(gx, gy) {
|
||||
G.roomLayerKey = '';
|
||||
G.roomDamaged = false;
|
||||
G.roomEnterT = G.time;
|
||||
G.entryFresh = false;
|
||||
|
||||
const room = G.rooms[roomKey(gx, gy)];
|
||||
G.cur = room;
|
||||
@@ -110,7 +114,8 @@ function enterRoom(gx, gy) {
|
||||
const w = wavesFor(G.floor);
|
||||
room.wavesLeft = w.length;
|
||||
room.pendingWave = w;
|
||||
spawnWave(w[0], G.floor);
|
||||
G.entryFresh = true;
|
||||
spawnWave(w[0], G.floor, ENTRY_WARN);
|
||||
room.wavesLeft--;
|
||||
room.spawnT = 0;
|
||||
} else if (room.type === 'boss') {
|
||||
@@ -138,6 +143,20 @@ function enterRoom(gx, gy) {
|
||||
}
|
||||
}
|
||||
|
||||
function separateEntryWave() {
|
||||
const p = G.player;
|
||||
const a = G.arena;
|
||||
const minD = Math.min(ENTRY_BUFFER, Math.min(a.w, a.h) * 0.42);
|
||||
const minD2 = minD * minD;
|
||||
for (const e of G.enemies) {
|
||||
if (e.boss || e.hp <= 0) continue;
|
||||
if (dist2(e.x, e.y, p.x, p.y) < minD2) {
|
||||
const pos = spawnPosAwayFromPlayer();
|
||||
e.x = pos.x; e.y = pos.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function roomLocked() {
|
||||
const r = G.cur;
|
||||
if (!r) return false;
|
||||
@@ -206,6 +225,7 @@ function updateRoom(dt) {
|
||||
if (dir === 'w') p.x = next.x1 - p.r - 8;
|
||||
p.x = clamp(p.x, next.x0 + p.r, next.x1 - p.r);
|
||||
p.y = clamp(p.y, next.y0 + p.r, next.y1 - p.r);
|
||||
if (G.entryFresh) { separateEntryWave(); G.entryFresh = false; }
|
||||
G.transition = 0.3;
|
||||
Sfx.door();
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ const G = {
|
||||
const PERK_POWER_WEIGHT = 0.5;
|
||||
// ammo economy: global dial for drop frequency (refill size is per-weapon)
|
||||
const AMMO_DROP_SCALE = 0.75;
|
||||
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_UNIT = 0.01; // one "point" on the dial scale
|
||||
const PRESSURE_GAIN = PRESSURE_UNIT; // retained: dial 0 == 1 point
|
||||
|
||||
Reference in new issue
Block a user