Isle-Slingers/js/physics/collision/index.js

213 lines
9.1 KiB
JavaScript
Raw Normal View History

/* ISLE SLINGERS -- physics/collision/index.js (submodule: collision)
* RC-375D84 S-2 modularization (behavior-neutral split of the original
* js/core.js monolith). Installs its units into ONE shared physics scope `P`
* that every physics submodule sees -- the same single-scope semantics the
* monolith had. Must load before js/physics/index.js (the aggregator that
* attaches the public ISLE_CORE API). Surface installed: crushCircleVsBox, collideBirdWorld, collideBirdBlocks, collideBirdEnemies, REST_VEL_CLAMP, resolveBoxPair.
*/
(function (root, factory) {
var part = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = part;
else (root.ISLE_PHYSICS_PARTS = root.ISLE_PHYSICS_PARTS || []).push(part);
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
return function (P) {
P.crushCircleVsBox = function(g, box, ent, isEnemy) {
var cx = Math.max(box.x - box.w / 2, Math.min(ent.x, box.x + box.w / 2));
var cy = Math.max(box.y - box.h / 2, Math.min(ent.y, box.y + box.h / 2));
var dx = ent.x - cx, dy = ent.y - cy;
var d2 = dx * dx + dy * dy;
if (d2 >= ent.r * ent.r) return;
var d = Math.sqrt(d2) || 0.001;
var nx = dx / d, ny = dy / d;
var overlap = ent.r - d;
ent.x += nx * overlap * 0.9;
ent.y += ny * overlap * 0.9;
// impulse: relative velocity along normal
var rvx = (ent.vx || 0) - (box.vx || 0), rvy = (ent.vy || 0) - (box.vy || 0);
var rel = rvx * nx + rvy * ny;
if (rel < -30) {
var relSpeed = -rel;
ent.vx -= nx * rel * 1.4;
ent.vy -= ny * rel * 1.4;
if (isEnemy) {
// crush damage: a fast box hurts what it lands on.
// S-4 (RC-375D84): the direction gate is velocity-aware -- the BOX
// must be the body moving into the enemy (boxAlong > 30). The old gate
// (ny < -0.3, enemy above the box) only let SIDE plows crush: a block
// dropped STRAIGHT DOWN onto an enemy never registered ("falling
// objects can't kill"). Vertical drops and side plows share this path.
// Two sub-fixes, both watched in test/fallingkill.test.mjs:
// (a) per-enemy crush cooldown (0.12s, same idea as the bird-hit
// gate): a box resting on an enemy must not machine-gun damage
// every frame; one landing = one crush instance.
// (b) inelastic transfer: the box sheds its approach velocity into
// the enemy (mass-scaled), so a block does not phase through
// the enemy at terminal velocity after the hit.
var spec = P.ENEMY_TYPES[ent.kind];
var boxSpeed = Math.hypot(box.vx || 0, box.vy || 0);
var boxAlong = (box.vx || 0) * nx + (box.vy || 0) * ny; // box velocity toward the enemy
if (boxSpeed > spec.crushVel && boxAlong > 30) {
if (g.time - (ent.lastCrushT || -9) > 0.12) {
P.damageEnemy(g, ent, (boxSpeed - spec.crushVel) * 0.004 * spec.crushDmg + 0.35);
g.events.push({ type: 'crush', x: ent.x, y: ent.y });
ent.lastCrushT = g.time;
// inelastic: box loses a mass-scaled share of its normal velocity
var entM = ent.r / 24; // mogg~1, grum~1.3, king~1.6
var share = entM / ((box.mass || 1) + entM);
box.vx -= nx * boxAlong * share;
box.vy -= ny * boxAlong * share;
}
}
} else {
// bird: transfer momentum into the box
var dmg = relSpeed * 0.32 * (ent.mass || 3) * 0.25;
if (dmg > 25) P.damageBlock(g, box, dmg, 'bird');
box.vx += nx * rel * 0.05 * (ent.mass || 3);
box.vy += ny * rel * 0.05 * (ent.mass || 3);
}
} else if (rel < 0) {
// slow resting contact: remove the normal approach velocity so a
// perched enemy/box pair stops oscillating under gravity
ent.vx -= nx * rel;
ent.vy -= ny * rel;
}
}
P.collideBirdWorld = function(g, b) {
if (b.y + b.r > P.WORLD.ground) {
var impact = -b.vy;
b.y = P.WORLD.ground - b.r;
if (b.vy > 0) b.vy = -b.vy * P.BIRD_BOUNCE;
b.vx *= P.BIRD_GROUND_FRICTION;
if (impact > 250) {
g.events.push({ type: 'bird_ground', x: b.x, y: P.WORLD.ground, strength: impact });
g.camera.shake = Math.max(g.camera.shake, 5);
}
if (Math.abs(b.vy) < 30) b.vy = 0;
}
if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx) * 0.3; }
}
P.collideBirdBlocks = function(g, b) {
var i;
for (i = 0; i < g.blocks.length; i++) {
var bl = g.blocks[i];
if (bl.hp <= 0) continue;
var cx = Math.max(bl.x - bl.w / 2, Math.min(b.x, bl.x + bl.w / 2));
var cy = Math.max(bl.y - bl.h / 2, Math.min(b.y, bl.y + bl.h / 2));
var dx = b.x - cx, dy = b.y - cy;
var d2 = dx * dx + dy * dy;
if (d2 >= b.r * b.r) continue;
var d = Math.sqrt(d2) || 0.001;
var nx = dx / d, ny = dy / d;
var overlap = b.r - d;
b.x += nx * overlap;
b.y += ny * overlap;
var rvx = b.vx - bl.vx, rvy = b.vy - bl.vy;
var rel = rvx * nx + rvy * ny;
if (rel < 0) {
var relSpeed = -rel;
// impulse: j = -(1+e)*rel / (1/m1 + 1/m2)
var invM = 1 / b.mass + 1 / bl.mass;
var j = -(1 + P.BIRD_BOUNCE) * rel / invM;
b.vx += (j / b.mass) * nx;
b.vy += (j / b.mass) * ny;
bl.vx -= (j / bl.mass) * nx;
bl.vy -= (j / bl.mass) * ny;
g.stats.impacts++;
var dmg = relSpeed * 0.55 * (b.mass / 3) * P.MATERIALS[bl.mat].dmgFactor;
if (dmg > 30) {
P.damageBlock(g, bl, dmg, 'bird');
g.events.push({ type: 'hit', x: cx, y: cy, mat: bl.mat, strength: relSpeed });
g.camera.shake = Math.max(g.camera.shake, Math.min(12, relSpeed / 90));
}
b.vx *= 0.985; b.vy *= 0.985;
}
}
}
P.collideBirdEnemies = function(g, b) {
var i;
for (i = 0; i < g.enemies.length; i++) {
var e = g.enemies[i];
if (e.dead) continue;
var dx = e.x - b.x, dy = e.y - b.y;
var rr = e.r + b.r;
var d2 = dx * dx + dy * dy;
if (d2 >= rr * rr) continue;
var d = Math.sqrt(d2) || 0.001;
var nx = dx / d, ny = dy / d;
var rel = (b.vx - e.vx) * nx + (b.vy - e.vy) * ny;
if (rel > 0) {
e.vx += nx * rel * 0.8;
e.vy += ny * rel * 0.8 - 120;
// per-target hit cooldown so one pass-through = one damage instance
if (g.time - (e.lastHit || -9) > 0.12) {
var speed = Math.hypot(b.vx, b.vy);
// retuned 2026-09-28 (RC-0D193E, Commander bug #3): the old curve
// gave ~0.9 dmg to a soft wren lob — a mogg (hp1) could not be
// killed without a perfect full-power hit. Bumped so a decent
// medium shot connects; the king (hp3) still survives one
// full-power wren hit (pinned in test/sim.test.mjs).
var dmg = 0.35 + speed / 800 * (b.mass / 3);
P.damageEnemy(g, e, dmg);
e.lastHit = g.time;
g.events.push({ type: 'bird_hit_enemy', x: e.x, y: e.y, strength: speed });
}
b.vx *= 0.92; b.vy *= 0.92;
}
}
}
// Resting-contact clamp: inelastic contacts + zeroing below this speed,
// so stacked blocks settle to exactly still instead of oscillating under
// gravity re-injection.
P.REST_VEL_CLAMP = 30; // |v| below this on a resting contact is zeroed
P.resolveBoxPair = function(g, a, c) {
var ox = (a.w / 2 + c.w / 2) - Math.abs(a.x - c.x);
var oy = (a.h / 2 + c.h / 2) - Math.abs(a.y - c.y);
if (ox <= 0 || oy <= 0) return;
if (ox < oy) {
// side contact: push apart along x
var n = c.x >= a.x ? 1 : -1; // direction from a toward c
a.x -= n * ox * 0.5; c.x += n * ox * 0.5;
var relv = (c.vx - a.vx) * n;
if (relv < 0) { // approaching
var avg = (a.vx + c.vx) / 2;
a.vx = avg; c.vx = avg;
}
a.vx *= 0.9; c.vx *= 0.9;
} else {
// stacked contact: push apart along y, then kill resting velocity
var n2 = c.y >= a.y ? 1 : -1;
a.y -= n2 * oy * 0.5; c.y += n2 * oy * 0.5;
var relv2 = (c.vy - a.vy) * n2;
if (relv2 < -P.REST_VEL_CLAMP) {
// real impact: inelastic common velocity.
// S-4 (RC-375D84): mass-WEIGHTED (was a plain (a.vy+c.vy)/2), so a
// heavy block landing on a light plank transfers its real momentum
// through the stack (chain-crush). A 22.6-mass stone at v over a
// 7.2-mass plank now drives the plank to ~0.76v instead of 0.5v.
var mA2 = a.mass || 1, mC2 = c.mass || 1;
var avgY = (a.vy * mA2 + c.vy * mC2) / (mA2 + mC2);
a.vy = avgY; c.vy = avgY;
} else if (relv2 < 0) {
// resting: top takes the bottom's velocity (kills the relative
// velocity), then zero both below the clamp so stacks stop
// oscillating under gravity re-injection
var top = n2 === 1 ? a : c;
var bot = n2 === 1 ? c : a;
top.vy = bot.vy;
if (Math.abs(top.vy) < P.REST_VEL_CLAMP) { top.vy = 0; bot.vy = 0; }
} else {
// separating / at rest: clamp near-zero velocities
if (Math.abs(a.vy) < 2) a.vy = 0;
if (Math.abs(c.vy) < 2) c.vy = 0;
}
}
}
};
});