224 lines
9.8 KiB
JavaScript
224 lines
9.8 KiB
JavaScript
|
|
/* ISLE SLINGERS -- physics/topple/index.js (submodule: topple)
|
||
|
|
* RC-375D84 S-3. Boards tip and topple end-over-end (Commander Q1: FULL
|
||
|
|
* CONTINUOUS ROTATION). A resting block whose centre of mass lies OUTSIDE the
|
||
|
|
* x-span of whatever supports it (ground, or the top face of a block below)
|
||
|
|
* begins pivoting about the support-edge corner on the side the COM leans to.
|
||
|
|
* The pivot is a fixed point; the board rotates about it under gravity
|
||
|
|
* (rigid body, point-mass COM) -- a smooth continuous swing, no snap. When the
|
||
|
|
* far end strikes the ground it lands: impact speed = angVel * r drives damage
|
||
|
|
* to the board itself and to anything in the swept path (blocks take impact,
|
||
|
|
* enemies can be killed). The board then rests at its landed angle.
|
||
|
|
*
|
||
|
|
* Adds a single rotational degree of freedom (ang, angVel, pivot) to blocks.
|
||
|
|
* Blocks that never overhang keep ang=0 forever, so pre-S-3 behaviour is
|
||
|
|
* exactly preserved for supported stacks (proven by topple.test arm 3).
|
||
|
|
*
|
||
|
|
* Installs into the shared physics scope P (same single-scope semantics as the
|
||
|
|
* monolith). Must load before js/physics/index.js. Surface installed:
|
||
|
|
* P.toppleBlocks, P.blockSupportSpan
|
||
|
|
*/
|
||
|
|
(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) {
|
||
|
|
|
||
|
|
// The x-span [lo, hi] that supports the block, built from the ground and the
|
||
|
|
// top faces of live blocks whose top is at (or just below) this block's
|
||
|
|
// bottom and which overlap it in x. Returns null if the block is free-
|
||
|
|
// falling (nothing directly under it).
|
||
|
|
P.blockSupportSpan = function(g, bl) {
|
||
|
|
var bottom = bl.y + bl.h / 2;
|
||
|
|
var lo = null, hi = null;
|
||
|
|
var i, o, oTop, oBot, oLo, oHi;
|
||
|
|
// ground support: if the block's bottom sits on the ground line
|
||
|
|
if (Math.abs(bottom - P.WORLD.ground) < 2 && Math.abs(bl.vy) < P.REST_SPEED) {
|
||
|
|
lo = bl.x - bl.w / 2; hi = bl.x + bl.w / 2;
|
||
|
|
}
|
||
|
|
for (i = 0; i < g.blocks.length; i++) {
|
||
|
|
o = g.blocks[i];
|
||
|
|
if (o === bl || o.hp <= 0) continue;
|
||
|
|
oTop = o.y - o.h / 2; // top face of the support block (unrotated support)
|
||
|
|
oBot = o.y + o.h / 2;
|
||
|
|
oLo = o.x - o.w / 2; oHi = o.x + o.w / 2;
|
||
|
|
// o supports bl when o's top face is within a hair below bl's bottom
|
||
|
|
if (bottom - oTop > 2 || oTop - bottom > 4) continue; // not touching
|
||
|
|
if (oBot < bottom - 4) continue;
|
||
|
|
// x-overlap between bl and o
|
||
|
|
var ovlLo = Math.max(bl.x - bl.w / 2, oLo);
|
||
|
|
var ovlHi = Math.min(bl.x + bl.w / 2, oHi);
|
||
|
|
if (ovlLo >= ovlHi) continue;
|
||
|
|
lo = (lo === null) ? ovlLo : Math.min(lo, ovlLo);
|
||
|
|
hi = (hi === null) ? ovlHi : Math.max(hi, ovlHi);
|
||
|
|
}
|
||
|
|
if (lo === null || hi === null) return null;
|
||
|
|
return { lo: lo, hi: hi };
|
||
|
|
}
|
||
|
|
|
||
|
|
P.toppleBlocks = function(g, dt) {
|
||
|
|
var i, bl, sp, margin = 2;
|
||
|
|
for (i = 0; i < g.blocks.length; i++) {
|
||
|
|
bl = g.blocks[i];
|
||
|
|
if (bl.hp <= 0) continue;
|
||
|
|
if (bl.pivoting) {
|
||
|
|
// support destroyed or block removed mid-swing -> free fall, keep angle
|
||
|
|
if (!P.supportStillAlive(g, bl)) { bl.pivoting = false; bl.angVel = 0; continue; }
|
||
|
|
P.integrateTopple(g, bl, dt);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
// candidate for tipping: must be (nearly) at rest to evaluate support
|
||
|
|
if (Math.abs(bl.vy) > P.REST_SPEED * 2 || Math.abs(bl.vx) > P.REST_SPEED * 2) continue;
|
||
|
|
if (bl.toppled) {
|
||
|
|
// anti-livelock: a swung-and-settled board rests until a REAL knock
|
||
|
|
// (displacement > 6px from its settled spot, or a damaging hit, which
|
||
|
|
// clears the flag in damageBlock). Gravity settling never moves it
|
||
|
|
// that far, so the knife-edge re-tip loop is closed.
|
||
|
|
if (Math.abs(bl.x - (bl.restX || bl.x)) + Math.abs(bl.y - (bl.restY || bl.y)) >= 6) {
|
||
|
|
bl.toppled = false;
|
||
|
|
} else {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sp = P.blockSupportSpan(g, bl);
|
||
|
|
if (!sp) continue; // free-falling or in the air
|
||
|
|
var comX = bl.x;
|
||
|
|
if (comX > sp.hi + margin) {
|
||
|
|
// COM leans right of the support span -> pivot about the support's
|
||
|
|
// top-right corner (the edge of what's underneath, not the board edge)
|
||
|
|
P.beginTopple(g, bl, sp.hi, bl.y + bl.h / 2);
|
||
|
|
} else if (comX < sp.lo - margin) {
|
||
|
|
P.beginTopple(g, bl, sp.lo, bl.y + bl.h / 2);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
P.beginTopple = function(g, bl, px, py) {
|
||
|
|
bl.pivoting = true;
|
||
|
|
bl.restX = bl.x; bl.restY = bl.y; // where it sits if the swing settles back
|
||
|
|
bl.pivotX = px; bl.pivotY = py;
|
||
|
|
// vector from pivot to COM at the start of the swing
|
||
|
|
bl.pvx = bl.x - px; bl.pvy = bl.y - py;
|
||
|
|
bl.ang = 0; bl.angVel = 0;
|
||
|
|
bl.rest = 0;
|
||
|
|
g.events.push({ type: 'topple_start', x: bl.x, y: bl.y, mat: bl.mat });
|
||
|
|
}
|
||
|
|
|
||
|
|
P.integrateTopple = function(g, bl, dt) {
|
||
|
|
var r2 = bl.pvx * bl.pvx + bl.pvy * bl.pvy;
|
||
|
|
if (r2 < 1e-6) { bl.pivoting = false; return; }
|
||
|
|
// rotate the pivot->COM vector by the current angle to get the live COM
|
||
|
|
var c = Math.cos(bl.ang), s = Math.sin(bl.ang);
|
||
|
|
var comX = bl.pivotX + bl.pvx * c - bl.pvy * s;
|
||
|
|
var comY = bl.pivotY + bl.pvx * s + bl.pvy * c;
|
||
|
|
// torque per mass about the pivot: g * (COMx - pivotX); I/m = r2
|
||
|
|
var alpha = P.GRAVITY * (comX - bl.pivotX) / r2;
|
||
|
|
bl.angVel += alpha * dt;
|
||
|
|
bl.ang += bl.angVel * dt;
|
||
|
|
bl.x = comX; bl.y = comY;
|
||
|
|
// landing: the far (non-pivot) corners hit the ground line
|
||
|
|
var hw = bl.w / 2, hh = bl.h / 2;
|
||
|
|
var cx = [ -hw, hw, hw, -hw ], cy = [ -hh, -hh, hh, hh ];
|
||
|
|
var landed = false, k, rx0, ry0, rx, ry;
|
||
|
|
for (k = 0; k < 4; k++) {
|
||
|
|
// corner vector = (pivot->COM) + (COM->corner), rotated by ang
|
||
|
|
rx0 = bl.pvx + cx[k]; ry0 = bl.pvy + cy[k];
|
||
|
|
rx = rx0 * c - ry0 * s;
|
||
|
|
ry = rx0 * s + ry0 * c;
|
||
|
|
if (bl.pivotY + ry >= P.WORLD.ground - 0.5) { landed = true; break; }
|
||
|
|
}
|
||
|
|
if (landed) { P.landTopple(g, bl); return; }
|
||
|
|
// settle-back: the pivot is too high for either end to reach the ground,
|
||
|
|
// so the board is a damped-free pendulum -- when it swings back through
|
||
|
|
// the flat angle, it lands flat on its support (the physical resolution)
|
||
|
|
if (bl.ang < 0 && bl.angVel > 0 && bl.ang > -0.06) P.settleBack(g, bl);
|
||
|
|
else if (bl.ang > 0 && bl.angVel < 0 && bl.ang < 0.06) P.settleBack(g, bl);
|
||
|
|
}
|
||
|
|
|
||
|
|
// true while the block the pivot sits on is still alive (or the pivot is on
|
||
|
|
// the ground line). Used to detect a support destroyed mid-swing.
|
||
|
|
P.supportStillAlive = function(g, bl) {
|
||
|
|
if (Math.abs(bl.pivotY - P.WORLD.ground) < 2) return true;
|
||
|
|
var i, o, oTop;
|
||
|
|
for (i = 0; i < g.blocks.length; i++) {
|
||
|
|
o = g.blocks[i];
|
||
|
|
if (o === bl || o.hp <= 0) continue;
|
||
|
|
oTop = o.y - o.h / 2;
|
||
|
|
if (Math.abs(oTop - bl.pivotY) < 3) {
|
||
|
|
if (bl.pivotX >= o.x - o.w / 2 - 1 && bl.pivotX <= o.x + o.w / 2 + 1) return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// the board swung back to flat on its support: stop the rotation, rest it
|
||
|
|
// flat (COM returns to its exact start point, so no position drift)
|
||
|
|
P.settleBack = function(g, bl) {
|
||
|
|
bl.angVel = 0;
|
||
|
|
bl.ang = 0;
|
||
|
|
bl.pivoting = false;
|
||
|
|
bl.x = bl.restX; bl.y = bl.restY; // damped pendulum: it sits back where it was
|
||
|
|
bl.toppled = true; // anti-livelock: don't re-tip until knocked or damaged
|
||
|
|
bl.vx = 0; bl.vy = 0; bl.rest = 1;
|
||
|
|
g.events.push({ type: 'topple_settle', x: bl.x, y: bl.y, mat: bl.mat });
|
||
|
|
}
|
||
|
|
|
||
|
|
P.landTopple = function(g, bl) {
|
||
|
|
// impact speed of the COM = tangential speed about the pivot
|
||
|
|
var r = Math.hypot(bl.pvx, bl.pvy);
|
||
|
|
var impact = Math.abs(bl.angVel) * r;
|
||
|
|
// settle: stop the swing, keep the landed angle (board rests propped/flat)
|
||
|
|
bl.angVel = 0;
|
||
|
|
bl.pivoting = false;
|
||
|
|
bl.vx = 0; bl.vy = 0; bl.rest = 1;
|
||
|
|
// clamp COM onto the ground line if it dipped below
|
||
|
|
if (bl.y + bl.h / 2 > P.WORLD.ground) bl.y = P.WORLD.ground - bl.h / 2;
|
||
|
|
if (impact > 60) {
|
||
|
|
// the board itself takes impact damage from the slam
|
||
|
|
P.damageBlock(g, bl, (impact - 60) * 0.35, 'topple');
|
||
|
|
g.events.push({ type: 'topple_land', x: bl.x, y: bl.y, mat: bl.mat, strength: impact });
|
||
|
|
g.camera.shake = Math.max(g.camera.shake, Math.min(14, impact / 60));
|
||
|
|
// anything in the swept path takes it too: check the board's current
|
||
|
|
// rotated extent against live blocks and enemies
|
||
|
|
P.topplePathDamage(g, bl, impact);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
P.topplePathDamage = function(g, bl, impact) {
|
||
|
|
// rotated corners of the landed board
|
||
|
|
var c = Math.cos(bl.ang), s = Math.sin(bl.ang);
|
||
|
|
var hw = bl.w / 2, hh = bl.h / 2;
|
||
|
|
var cx = [ -hw, hw, hw, -hw ], cy = [ -hh, -hh, hh, hh ];
|
||
|
|
var xs = [], ys = [];
|
||
|
|
for (var k = 0; k < 4; k++) {
|
||
|
|
xs.push(bl.x + cx[k] * c - cy[k] * s);
|
||
|
|
ys.push(bl.y + cx[k] * s + cy[k] * c);
|
||
|
|
}
|
||
|
|
var minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
|
||
|
|
var minY = Math.min.apply(null, ys), maxY = Math.max.apply(null, ys);
|
||
|
|
var i, o, spec;
|
||
|
|
// blocks in the landed footprint
|
||
|
|
for (i = 0; i < g.blocks.length; i++) {
|
||
|
|
o = g.blocks[i];
|
||
|
|
if (o === bl || o.hp <= 0) continue;
|
||
|
|
if (o.x + o.w / 2 < minX || o.x - o.w / 2 > maxX) continue;
|
||
|
|
if (o.y + o.h / 2 < minY || o.y - o.h / 2 > maxY) continue;
|
||
|
|
P.damageBlock(g, o, impact * 0.4 * P.MATERIALS[o.mat].dmgFactor, 'topple');
|
||
|
|
}
|
||
|
|
// enemies in the swept footprint can be killed (objective b)
|
||
|
|
for (i = 0; i < g.enemies.length; i++) {
|
||
|
|
o = g.enemies[i];
|
||
|
|
if (o.dead) continue;
|
||
|
|
if (o.x + o.r < minX || o.x - o.r > maxX) continue;
|
||
|
|
if (o.y + o.r < minY || o.y - o.r > maxY) continue;
|
||
|
|
spec = P.ENEMY_TYPES[o.kind];
|
||
|
|
if (impact > spec.crushVel) {
|
||
|
|
P.damageEnemy(g, o, (impact - spec.crushVel) * 0.004 * spec.crushDmg + 0.35);
|
||
|
|
g.events.push({ type: 'crush', x: o.x, y: o.y });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
});
|