Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
/* ISLE SLINGERS -- physics/step/index.js (submodule: step)
|
2026-09-29 09:36:52 -05:00
|
|
|
* Modularization pass (behavior-neutral split of the original
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
* 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) {
|
2026-09-29 09:36:52 -05:00
|
|
|
// Landed-bird gravity behavior: the "another bird is already active" window.
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|