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

213 lines
8.2 KiB
JavaScript
Raw Normal View History

/* ISLE SLINGERS -- physics/step/index.js (submodule: step)
* Modularization pass (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: step, stepPhysics, spendBird.
*/
(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.step = function(g, dt) {
g.time += dt;
if (g.status === 'won' || g.status === 'lost') {
// S-5: the level is over -- landed bodies no longer exist (their window
// was "while another bird was active"), so remove them; the rest of the
// world still settles a beat for the win/lose screen.
for (var lb = 0; lb < g.birds.length; lb++) {
if (g.birds[lb].state === 'landed') {
g.birds[lb].state = 'spent';
g.events.push({ type: 'bird_gone', x: g.birds[lb].x, y: g.birds[lb].y });
}
}
P.stepPhysics(g, dt);
P.updatePopups(g, dt);
P.decayCamera(g, dt);
return;
}
P.stepPhysics(g, dt);
P.updatePopups(g, dt);
P.decayCamera(g, dt);
P.updateCameraTarget(g);
P.checkEndStates(g);
}
P.stepPhysics = function(g, dt) {
var i, b;
// active bird flight
b = P.activeBird(g);
if (b && b.state === 'flying') {
b.age += dt;
var nvy = b.vy + P.GRAVITY * dt;
b.x += b.vx * dt;
b.y += (b.vy + nvy) * 0.5 * dt;
b.vx *= P.AIR_DRAG;
b.vy = nvy;
g.stats.distance = Math.max(g.stats.distance, b.x);
P.collideBirdWorld(g, b);
P.collideBirdBlocks(g, b);
P.collideBirdEnemies(g, b);
// settle / spend detection (low speed anywhere = spent, whether on
// the ground or perched on a beam; jitter-tolerant accumulation)
var sp = Math.hypot(b.vx, b.vy);
if (sp < P.REST_SPEED) b.settleTimer += dt;
else b.settleTimer = Math.max(0, b.settleTimer - dt * 4);
if (b.age > P.BIRD_MAX_AGE || b.x > P.WORLD.w + 200 || b.x < -200) {
// gone for good (off the island or aged out) -> spent, next bird loads
P.spendBird(g, b);
} else if (b.settleTimer > P.SETTLE_TIME) {
// Landed-bird gravity behavior: the "another bird is already active" window.
// A bird that settles while MORE birds are in play (queued or active)
// is NOT a corpse: it becomes a LANDED body -- real continuous
// gravity, no lifetime timer. It falls if its support breaks,
// chain-damages blocks it lands on, and can kill enemies, for as long
// as the level runs. The level-end rule (last bird, no window left)
// keeps the pre-S-5 behaviour exactly: the bird is spent there, so
// win/lose resolve unchanged.
// ANOTHER bird must be in play (queued, or flying/aiming): the settled
// bird lingers only during the "another bird is already active" window.
// It never counts itself -- for the LAST bird this stays false and the
// pre-S-5 spend path runs exactly as before (win/lose unchanged).
var moreBirds = P.birdsRemaining(g) > 0;
if (!moreBirds) {
for (var ob = 0; ob < g.birds.length; ob++) {
if (ob === g.birds.indexOf(b)) continue;
if (g.birds[ob].state === 'flying' || g.birds[ob].state === 'landed' || g.birds[ob].state === 'aiming') { moreBirds = true; break; }
}
}
if (moreBirds) {
b.state = 'landed';
b.vx = 0; b.vy = 0;
g.events.push({ type: 'bird_landed', x: b.x, y: b.y, kind: b.kind });
P.advanceToNextBird(g);
} else {
P.spendBird(g, b);
}
}
}
// S-5: landed birds stay physical bodies -- real gravity, fall when their
// support breaks, hit blocks (chain damage) and enemies (kills). No
// lifetime timer: they live until the level resolves.
for (i = 0; i < g.birds.length; i++) {
var lb = g.birds[i];
if (lb.state !== 'landed') continue;
var lvy = lb.vy + P.GRAVITY * dt;
lb.x += lb.vx * dt;
lb.y += (lb.vy + lvy) * 0.5 * dt;
lb.vy = lvy;
lb.vx *= 0.999; // a landed body keeps (almost) all of its momentum
P.collideBirdWorld(g, lb);
P.collideBirdBlocks(g, lb);
P.collideBirdEnemies(g, lb);
}
// blocks: gravity + integration + collisions
for (i = 0; i < g.blocks.length; i++) {
var bl = g.blocks[i];
if (bl.hp <= 0) continue;
if (bl.pivoting) continue; // rotate about its pivot; integrated by toppleBlocks
bl.vy = Math.min(bl.vy + P.GRAVITY * dt, P.MATERIALS[bl.mat].maxVel);
bl.vx *= 0.995;
bl.x += bl.vx * dt;
bl.y += bl.vy * dt;
// ground
var halfH = bl.h / 2;
if (bl.y + halfH > P.WORLD.ground) {
var impact = bl.vy;
bl.y = P.WORLD.ground - halfH;
if (impact > 90) {
bl.vy = -impact * P.MATERIALS[bl.mat].restitution;
bl.vx *= 0.8;
if (impact > 240) {
P.damageBlock(g, bl, (impact - 240) * 0.45, 'ground');
g.events.push({ type: 'block_impact', x: bl.x, y: P.WORLD.ground, mat: bl.mat, strength: impact });
}
bl.rest = 0;
} else {
bl.vy = 0;
bl.vx *= 0.82; // rolling friction
if (Math.abs(bl.vx) < 4) { bl.vx = 0; bl.rest = 1; }
}
}
// walls (keep the mess on the island)
if (bl.x - bl.w / 2 < 0) { bl.x = bl.w / 2; bl.vx = Math.abs(bl.vx) * 0.2; }
if (bl.x + bl.w / 2 > P.WORLD.w) { bl.x = P.WORLD.w - bl.w / 2; bl.vx = -Math.abs(bl.vx) * 0.2; }
}
// block vs block (positional + 1D impulse, 2 relaxation passes)
var pass, j, a, c;
for (pass = 0; pass < 2; pass++) {
for (i = 0; i < g.blocks.length; i++) {
a = g.blocks[i];
if (a.hp <= 0 || a.pivoting) continue;
for (j = i + 1; j < g.blocks.length; j++) {
c = g.blocks[j];
if (c.hp <= 0 || c.pivoting) continue;
P.resolveBoxPair(g, a, c);
}
}
}
// block tip/topple (S-3): rotate about a support-edge pivot, land, impact
P.toppleBlocks(g, dt);
// block vs bird (crush) -- S-5: the active bird AND any landed bodies
for (i = 0; i < g.birds.length; i++) {
var cb = g.birds[i];
if (cb.state !== 'flying' && cb.state !== 'landed') continue;
for (var ci = 0; ci < g.blocks.length; ci++) {
var blk = g.blocks[ci];
if (blk.hp <= 0 || blk.pivoting) continue;
P.crushCircleVsBox(g, blk, cb, false);
}
}
// enemies: settle on ground/blocks, take crush damage
for (i = 0; i < g.enemies.length; i++) {
var e = g.enemies[i];
if (e.dead) { e.deathAge += dt; continue; }
e.vy = Math.min(e.vy + P.GRAVITY * dt, 700);
e.x += e.vx * dt;
e.y += e.vy * dt;
e.vx *= 0.9;
if (e.y + e.r > P.WORLD.ground) {
if (e.vy > 420) { e.vy = -e.vy * 0.25; e.vx *= 0.6; }
else { e.vy = 0; e.y = P.WORLD.ground - e.r; e.vx *= 0.8; }
}
for (j = 0; j < g.blocks.length; j++) {
var bb = g.blocks[j];
if (bb.hp <= 0 || bb.pivoting) continue;
P.crushCircleVsBox(g, bb, e, true);
}
}
}
// next bird onto the sling (shared by spendBird and the S-5 landed
// transition -- the same "another bird is already active" moment)
P.advanceToNextBird = function(g) {
var next = -1;
for (var i = g.activeBird + 1; i < g.birds.length; i++) {
if (g.birds[i].state === 'queued') { next = i; break; }
}
g.activeBird = next;
if (next >= 0) {
var nb = g.birds[next];
nb.state = 'aiming';
nb.x = g.level.sling.x; nb.y = g.level.sling.y;
g.status = 'ready';
g.events.push({ type: 'bird_load', x: nb.x, y: nb.y, kind: nb.kind });
}
}
P.spendBird = function(g, b) {
b.state = 'spent';
b.vx = 0; b.vy = 0;
g.events.push({ type: 'bird_gone', x: b.x, y: b.y });
// next bird onto the sling
P.advanceToNextBird(g);
}
};
});