Removes internal process references so the repo is presentable to an outside audience: dropped the internal build-retrospective doc (process log with internal terms, not useful to an outside reader), and reworded remaining internal references in code comments and module docs (agents.md cards, architecture.md, MODULES.md, index.html, package.json) to generic descriptions of what changed and when, with no behavior change -- full 47/47 suite re-run green after the scrub, every edit verified comment/doc-only via diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
212 lines
8.2 KiB
JavaScript
212 lines
8.2 KiB
JavaScript
/* 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);
|
|
}
|
|
};
|
|
});
|