/* ISLE SLINGERS -- physics/step/index.js (submodule: step) * 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: 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) { // S-5 (RC-375D84, Q2): 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); } }; });