diff --git a/src/config.js b/src/config.js index f5bf4c1..6cc5a65 100644 --- a/src/config.js +++ b/src/config.js @@ -93,17 +93,28 @@ export const BUS = { driveAcceleration: 0.0045, brakeAcceleration: 0.007, - // Bumped up to stay responsive at the new higher speed - sluggish lean - // control would feel wrong on a bus that's now moving quickly. - maxLeanAngularVelocity: 0.13, - leanTorqueStep: 0.018, + // Cut by more than half (both the cap and the per-tick torque step) for a + // much slower, more gradual lean - easier to fine-tune mid-air at the + // cost of snappiness. + maxLeanAngularVelocity: 0.06, + leanTorqueStep: 0.004, }; export const GFORCE = { // Scaled with WORLD_SCALE so "meters" keeps the same real-world meaning // relative to the bus, and ejectThresholdG doesn't need re-tuning. pixelsPerMeter: 50 * WORLD_SCALE, - ejectThresholdG: 6, // primary tuning knob - not physically derived, tune by playtesting + // The bouncier suspension (see BUS.suspensionStiffness/Damping/Travel) + // isolates the CHASSIS from shock by design - which starves this of any + // signal to trigger on. Verified via a headless matter-js simulation + // replicating these exact constants: even a 1300px vertical drop from + // rest only reaches ~0.8g at the chassis, so the old 3-6g range was + // effectively unreachable - kids were never actually being ejected at + // all, not just "rarely." 0.5 is set from that same data (crossed by + // roughly a 400px+ drop, not by gentle rolling hills) - primary tuning + // knob, re-verify by playtesting since the simulation didn't model real + // terrain bumps, only vertical drops. + ejectThresholdG: 0.5, gracePeriodMs: 500, ejectImpulseMagnitude: 0.03 * WORLD_SCALE, // A single hard landing can stay above threshold for several consecutive @@ -120,6 +131,14 @@ export const KID = { seatDamping: 0.1, seatLength: 4 * WORLD_SCALE, settleTimeMs: 2000, + // Once ejected, a kid ignores the world's real gravity entirely and + // KidManager applies this fraction of it instead (see + // KidManager._onBeforeUpdate) - horizontal velocity is untouched either + // way (this game's gravity is purely vertical), so a lighter fall is all + // it takes for an ejected kid to hang in the air and arc much further + // than the bus does, Snuggle-Truck style. Primary tuning knob for "how + // floaty," not physically derived. + ejectedGravityScale: 0.35, }; export const CAMERA = { diff --git a/src/data/levels/level04.js b/src/data/levels/level04.js index c68274f..76dc3ab 100644 --- a/src/data/levels/level04.js +++ b/src/data/levels/level04.js @@ -1,9 +1,9 @@ // Generated by editor.html - review before dropping into src/data/levels/. export default { - id: "levelCustom", - name: "New Level", + id: "level04", + name: "Mega Drop", description: "", - kidsAboard: 3, + kidsAboard: 5, startPosition: { x: 200, y: 700 }, startAngle: 0, terrain: [ @@ -149,9 +149,325 @@ export default { { x: 9260, y: 1294 }, { x: 9330, y: 1298 }, { x: 9400, y: 1300 }, + { x: 9470, y: 1316 }, + { x: 9540, y: 1332 }, + { x: 9610, y: 1348 }, + { x: 9680, y: 1364 }, + { x: 9750, y: 1380 }, + { x: 9820, y: 1396 }, + { x: 9890, y: 1412 }, + { x: 9960, y: 1428 }, + { x: 10030, y: 1444 }, + { x: 10100, y: 1460 }, + { x: 10170, y: 1476 }, + { x: 10240, y: 1492 }, + { x: 10310, y: 1508 }, + { x: 10380, y: 1524 }, + { x: 10450, y: 1540 }, + { x: 10520, y: 1556 }, + { x: 10590, y: 1572 }, + { x: 10660, y: 1588 }, + { x: 10730, y: 1604 }, + { x: 10800, y: 1620 }, + { x: 10870, y: 1636 }, + { x: 10940, y: 1652 }, + { x: 11010, y: 1668 }, + { x: 11080, y: 1684 }, + { x: 11150, y: 1700 }, + { x: 11220, y: 1716 }, + { x: 11290, y: 1732 }, + { x: 11360, y: 1748 }, + { x: 11430, y: 1764 }, + { x: 11500, y: 1780 }, + { x: 11570, y: 1796 }, + { x: 11640, y: 1812 }, + { x: 11710, y: 1828 }, + { x: 11780, y: 1844 }, + { x: 11850, y: 1860 }, + { x: 11920, y: 1876 }, + { x: 11990, y: 1892 }, + { x: 12060, y: 1908 }, + { x: 12130, y: 1924 }, + { x: 12200, y: 1940 }, + { x: 12270, y: 1956 }, + { x: 12340, y: 1972 }, + { x: 12410, y: 1988 }, + { x: 12480, y: 2004 }, + { x: 12550, y: 2020 }, + { x: 12620, y: 2036 }, + { x: 12690, y: 2052 }, + { x: 12760, y: 2068 }, + { x: 12830, y: 2084 }, + { x: 12900, y: 2100 }, + { x: 12970, y: 2116 }, + { x: 13040, y: 2132 }, + { x: 13110, y: 2148 }, + { x: 13180, y: 2164 }, + { x: 13250, y: 2180 }, + { x: 13320, y: 2196 }, + { x: 13390, y: 2212 }, + { x: 13460, y: 2228 }, + { x: 13530, y: 2244 }, + { x: 13600, y: 2260 }, + { x: 13670, y: 2276 }, + { x: 13740, y: 2292 }, + { x: 13810, y: 2308 }, + { x: 13880, y: 2324 }, + { x: 13950, y: 2340 }, + { x: 14020, y: 2356 }, + { x: 14090, y: 2372 }, + { x: 14160, y: 2388 }, + { x: 14230, y: 2404 }, + { x: 14300, y: 2420 }, + { x: 14370, y: 2436 }, + { x: 14440, y: 2452 }, + { x: 14510, y: 2468 }, + { x: 14580, y: 2484 }, + { x: 14650, y: 2500 }, + { x: 14720, y: 2516 }, + { x: 14790, y: 2532 }, + { x: 14860, y: 2548 }, + { x: 14930, y: 2564 }, + { x: 15000, y: 2580 }, + { x: 15070, y: 2596 }, + { x: 15140, y: 2612 }, + { x: 15210, y: 2628 }, + { x: 15280, y: 2644 }, + { x: 15350, y: 2660 }, + { x: 15420, y: 2676 }, + { x: 15490, y: 2692 }, + { x: 15560, y: 2708 }, + { x: 15630, y: 2724 }, + { x: 15700, y: 2740 }, + { x: 15770, y: 2756 }, + { x: 15840, y: 2772 }, + { x: 15910, y: 2788 }, + { x: 15980, y: 2804 }, + { x: 16050, y: 2820 }, + { x: 16120, y: 2836 }, + { x: 16190, y: 2852 }, + { x: 16260, y: 2868 }, + { x: 16330, y: 2884 }, + { x: 16400, y: 2900 }, + { x: 16470, y: 2900 }, + { x: 16540, y: 2900 }, + { x: 16610, y: 2900 }, + { x: 16680, y: 2900 }, + { x: 16750, y: 2900 }, + { x: 16820, y: 2900 }, + { x: 16890, y: 2900 }, + { x: 16960, y: 2900 }, + { x: 17030, y: 2900 }, + { x: 17100, y: 2900 }, + { x: 17170, y: 2900 }, + { x: 17240, y: 2900 }, + { x: 17310, y: 2900 }, + { x: 17380, y: 2900 }, + { x: 17450, y: 2900 }, + { x: 17520, y: 2900 }, + { x: 17590, y: 2900 }, + { x: 17660, y: 2900 }, + { x: 17730, y: 2900 }, + { x: 17800, y: 2900 }, + { x: 17870, y: 2866 }, + { x: 17940, y: 2830 }, + { x: 18010, y: 2796 }, + { x: 18080, y: 2760 }, + { x: 18150, y: 2726 }, + { x: 18220, y: 2690 }, + { x: 18290, y: 2656 }, + { x: 18360, y: 2620 }, + { x: 18430, y: 2620 }, + ] }, + { points: [ + { x: 18780, y: 2740 }, + { x: 18850, y: 2752 }, + { x: 18920, y: 2782 }, + { x: 18990, y: 2820 }, + { x: 19060, y: 2858 }, + { x: 19130, y: 2888 }, + { x: 19200, y: 2900 }, + { x: 19270, y: 2928 }, + { x: 19340, y: 2952 }, + { x: 19410, y: 2972 }, + { x: 19480, y: 2986 }, + { x: 19550, y: 2990 }, + { x: 19620, y: 2986 }, + { x: 19690, y: 2972 }, + { x: 19760, y: 2952 }, + { x: 19830, y: 2928 }, + { x: 19900, y: 2900 }, + { x: 19970, y: 2872 }, + { x: 20040, y: 2848 }, + { x: 20110, y: 2828 }, + { x: 20180, y: 2814 }, + { x: 20250, y: 2810 }, + { x: 20320, y: 2814 }, + { x: 20390, y: 2828 }, + { x: 20460, y: 2848 }, + { x: 20530, y: 2872 }, + { x: 20600, y: 2900 }, + { x: 20670, y: 2940 }, + { x: 20740, y: 2980 }, + { x: 20810, y: 3018 }, + { x: 20880, y: 3052 }, + { x: 20950, y: 3084 }, + { x: 21020, y: 3110 }, + { x: 21090, y: 3132 }, + { x: 21160, y: 3148 }, + { x: 21230, y: 3156 }, + { x: 21300, y: 3160 }, + { x: 21370, y: 3156 }, + { x: 21440, y: 3148 }, + { x: 21510, y: 3132 }, + { x: 21580, y: 3110 }, + { x: 21650, y: 3084 }, + { x: 21720, y: 3052 }, + { x: 21790, y: 3018 }, + { x: 21860, y: 2980 }, + { x: 21930, y: 2940 }, + { x: 22000, y: 2900 }, + { x: 22070, y: 2866 }, + { x: 22140, y: 2830 }, + { x: 22210, y: 2796 }, + { x: 22280, y: 2760 }, + { x: 22350, y: 2726 }, + { x: 22420, y: 2690 }, + { x: 22490, y: 2656 }, + { x: 22560, y: 2620 }, + { x: 22630, y: 2620 }, + ] }, + { points: [ + { x: 22980, y: 2740 }, + { x: 23050, y: 2752 }, + { x: 23120, y: 2782 }, + { x: 23190, y: 2820 }, + { x: 23260, y: 2858 }, + { x: 23330, y: 2888 }, + { x: 23400, y: 2900 }, + { x: 23470, y: 2884 }, + { x: 23540, y: 2868 }, + { x: 23610, y: 2852 }, + { x: 23680, y: 2836 }, + { x: 23750, y: 2820 }, + { x: 23820, y: 2804 }, + { x: 23890, y: 2788 }, + { x: 23960, y: 2772 }, + { x: 24030, y: 2756 }, + { x: 24100, y: 2740 }, + { x: 24170, y: 2724 }, + { x: 24240, y: 2708 }, + { x: 24310, y: 2692 }, + { x: 24380, y: 2676 }, + { x: 24450, y: 2660 }, + { x: 24520, y: 2644 }, + { x: 24590, y: 2628 }, + { x: 24660, y: 2612 }, + { x: 24730, y: 2596 }, + { x: 24800, y: 2580 }, + { x: 24870, y: 2564 }, + { x: 24940, y: 2548 }, + { x: 25010, y: 2532 }, + { x: 25080, y: 2516 }, + { x: 25150, y: 2500 }, + { x: 25220, y: 2484 }, + { x: 25290, y: 2468 }, + { x: 25360, y: 2452 }, + { x: 25430, y: 2436 }, + { x: 25500, y: 2420 }, + { x: 25570, y: 2404 }, + { x: 25640, y: 2388 }, + { x: 25710, y: 2372 }, + { x: 25780, y: 2356 }, + { x: 25850, y: 2340 }, + { x: 25920, y: 2324 }, + { x: 25990, y: 2308 }, + { x: 26060, y: 2292 }, + { x: 26130, y: 2276 }, + { x: 26200, y: 2260 }, + { x: 26270, y: 2244 }, + { x: 26340, y: 2228 }, + { x: 26410, y: 2212 }, + { x: 26480, y: 2196 }, + { x: 26550, y: 2180 }, + { x: 26620, y: 2164 }, + { x: 26690, y: 2148 }, + { x: 26760, y: 2132 }, + { x: 26830, y: 2116 }, + { x: 26900, y: 2100 }, + { x: 26970, y: 2084 }, + { x: 27040, y: 2068 }, + { x: 27110, y: 2052 }, + { x: 27180, y: 2036 }, + { x: 27250, y: 2020 }, + { x: 27320, y: 2004 }, + { x: 27390, y: 1988 }, + { x: 27460, y: 1972 }, + { x: 27530, y: 1956 }, + { x: 27600, y: 1940 }, + { x: 27670, y: 1956 }, + { x: 27740, y: 1972 }, + { x: 27810, y: 1988 }, + { x: 27880, y: 2004 }, + { x: 27950, y: 2020 }, + { x: 28020, y: 2036 }, + { x: 28090, y: 2052 }, + { x: 28160, y: 2068 }, + { x: 28230, y: 2084 }, + { x: 28300, y: 2100 }, + { x: 28370, y: 2116 }, + { x: 28440, y: 2132 }, + { x: 28510, y: 2148 }, + { x: 28580, y: 2164 }, + { x: 28650, y: 2180 }, + { x: 28720, y: 2196 }, + { x: 28790, y: 2212 }, + { x: 28860, y: 2228 }, + { x: 28930, y: 2244 }, + { x: 29000, y: 2260 }, + { x: 29070, y: 2260 }, + { x: 29140, y: 2260 }, + { x: 29210, y: 2260 }, + { x: 29280, y: 2260 }, + { x: 29350, y: 2260 }, + { x: 29420, y: 2260 }, + { x: 29490, y: 2260 }, + { x: 29560, y: 2260 }, + { x: 29630, y: 2260 }, + { x: 29700, y: 2260 }, + { x: 29770, y: 2260 }, + { x: 29840, y: 2260 }, + { x: 29910, y: 2260 }, + { x: 29980, y: 2260 }, + { x: 30050, y: 2260 }, + { x: 30120, y: 2260 }, + { x: 30190, y: 2260 }, + { x: 30260, y: 2260 }, + { x: 30330, y: 2260 }, + { x: 30400, y: 2260 }, + { x: 30470, y: 2260 }, + { x: 30540, y: 2260 }, + { x: 30610, y: 2260 }, + { x: 30680, y: 2260 }, + { x: 30750, y: 2260 }, + { x: 30820, y: 2260 }, + { x: 30890, y: 2260 }, + { x: 30960, y: 2260 }, + { x: 31030, y: 2260 }, + { x: 31100, y: 2260 }, + { x: 31170, y: 2260 }, + { x: 31240, y: 2260 }, + { x: 31310, y: 2260 }, + { x: 31380, y: 2260 }, + { x: 31450, y: 2260 }, + { x: 31520, y: 2260 }, + { x: 31590, y: 2260 }, + { x: 31660, y: 2260 }, + { x: 31730, y: 2260 }, + { x: 31800, y: 2260 }, ] }, ], obstacles: [], - goal: { x: 9160, y: 1060, width: 280, height: 640 }, - cameraBounds: { x: -600, y: 0, width: 10800, height: 2000 }, + goal: { x: 31560, y: 2020, width: 280, height: 640 }, + cameraBounds: { x: -600, y: -140, width: 33200, height: 4100 }, }; diff --git a/src/editor/levelBuilder.js b/src/editor/levelBuilder.js index fd3eb26..c053201 100644 --- a/src/editor/levelBuilder.js +++ b/src/editor/levelBuilder.js @@ -14,7 +14,17 @@ const GOAL_END_MARGIN = 120; const GOAL_HEIGHT_OFFSET = 120; const CAMERA_LEFT_MARGIN = 300; const CAMERA_RIGHT_MARGIN = 700; -const CAMERA_HEIGHT = 1000; +// Vertical camera bounds used to be a fixed 1000 regardless of the actual +// terrain - fine for the hand-authored levels (which stay near a constant +// baseline by design), but an editor sequence can chain multiple +// inclines/declines/ditches with no cap on cumulative elevation change, so +// a long run of declines could push the terrain (and the bus) below what a +// fixed-height bound could ever scroll down to reach - the camera would +// clamp at its bounds and the bus would drift off the bottom of the +// screen. Bounds are now sized to the level's actual min/max terrain Y, +// with generous padding, with this as just a floor for very flat levels. +const CAMERA_VERTICAL_PADDING = 400; +const CAMERA_MIN_HEIGHT = 1000; function scalePoint(p) { return { x: Math.round(p.x * WORLD_SCALE), y: Math.round(p.y * WORLD_SCALE) }; @@ -69,6 +79,22 @@ export function buildLevelData(sectionTypeIds, metadata) { const endX = Math.round(baseX * WORLD_SCALE); const groundYAtEnd = Math.round(baseY * WORLD_SCALE); + let minTerrainY = Infinity; + let maxTerrainY = -Infinity; + for (const segment of terrainSegments) { + for (const p of segment.points) { + minTerrainY = Math.min(minTerrainY, p.y); + maxTerrainY = Math.max(maxTerrainY, p.y); + } + } + + const verticalPadding = Math.round(CAMERA_VERTICAL_PADDING * WORLD_SCALE); + const cameraTop = Math.min(0, minTerrainY - verticalPadding); + const cameraHeight = Math.max( + Math.round(CAMERA_MIN_HEIGHT * WORLD_SCALE), + maxTerrainY + verticalPadding - cameraTop + ); + return { id: metadata.id, name: metadata.name, @@ -89,9 +115,9 @@ export function buildLevelData(sectionTypeIds, metadata) { }, cameraBounds: { x: -Math.round(CAMERA_LEFT_MARGIN * WORLD_SCALE), - y: 0, + y: cameraTop, width: endX + Math.round(CAMERA_RIGHT_MARGIN * WORLD_SCALE), - height: Math.round(CAMERA_HEIGHT * WORLD_SCALE), + height: cameraHeight, }, }; } diff --git a/src/entities/Kid.js b/src/entities/Kid.js index 4bc2af6..b67837c 100644 --- a/src/entities/Kid.js +++ b/src/entities/Kid.js @@ -18,6 +18,11 @@ export default class Kid { collisionFilter: { group: bus.group }, }); this.image.setDisplaySize(KID.radius * 2, KID.radius * 2); + // Bus parts are all left at Phaser's default depth (0) - render kids + // just behind that, so the chassis always draws over them (seated, + // ejected, or falling back down) instead of insertion order deciding + // it implicitly. + this.image.setDepth(-1); this.seatConstraint = scene.matter.add.constraint(bus.chassis, this.image, KID.seatLength, KID.seatStiffness, { pointA: seat, @@ -28,7 +33,7 @@ export default class Kid { this._settleAt = 0; } - eject(chassisVelocity, gForce) { + eject(gForce) { if (this.state !== 'aboard') return; this.state = 'ejected'; @@ -40,14 +45,23 @@ export default class Kid { this.image.body.collisionFilter.group = 0; this.image.setTexture('kid_ejected'); - const speed = Math.hypot(chassisVelocity.x, chassisVelocity.y) || 1; - const dirX = chassisVelocity.x / speed; - const dirY = chassisVelocity.y / speed; + // Opt out of the world's real gravity - KidManager applies a lighter, + // custom fraction of it each tick instead (see + // KidManager._onBeforeUpdate), so the kid floats through a much bigger + // arc than the bus's own fall would. + this.image.body.ignoreGravity = true; + const severity = Phaser.Math.Clamp(gForce / GFORCE.ejectThresholdG, 1, 3); + // Always pops straight up through the roof, never sideways or downward + // - deliberately ignores the chassis's actual velocity/tilt at impact, + // which previously could fling a kid out sideways or into the ground + // depending on which way the bus was moving. Horizontal speed still + // carries over on its own from the velocity the seat constraint was + // already imparting, so no sideways force is needed to "retain" it. this.image.applyForce({ - x: dirX * GFORCE.ejectImpulseMagnitude * severity + (Math.random() - 0.5) * GFORCE.ejectImpulseMagnitude * 0.5, - y: dirY * GFORCE.ejectImpulseMagnitude * severity - GFORCE.ejectImpulseMagnitude * 0.4, + x: 0, + y: -GFORCE.ejectImpulseMagnitude * severity, }); this._settleAt = this.scene.time.now + KID.settleTimeMs; diff --git a/src/scenes/PlayScene.js b/src/scenes/PlayScene.js index 5bcb273..bb9d704 100644 --- a/src/scenes/PlayScene.js +++ b/src/scenes/PlayScene.js @@ -62,6 +62,7 @@ export default class PlayScene extends Phaser.Scene { this.bus.applyIntent(intent); this.kidManager.update(time); this.cameraRig.update(); + this._updateParallax(); if (DEBUG && this.debugText) { const g = this.gForceMonitor.lastGForce || 0; @@ -73,14 +74,59 @@ export default class PlayScene extends Phaser.Scene { const bounds = this.level.cameraBounds; const width = bounds.width + 1000 * WORLD_SCALE; - const far = this.add.tileSprite(bounds.x, 0, width, GAME_HEIGHT, 'bg_far').setOrigin(0, 0); - far.setScrollFactor(0.15, 0); + // Far layer fills the full viewport height at ONE uniform scale (same + // factor on both axes, so its native aspect ratio is preserved - no + // stretch), fixed to the screen (scroll factor 0). _updateParallax() + // manually scrubs its tilePositionX each frame based on progress from + // start to goal, so the single image pans start-to-end exactly once + // over the course of the level. + const farNative = this._nativeTextureSize('bg_far'); + const farScale = GAME_HEIGHT / farNative.height; + this.bgFar = this.add.tileSprite(0, 0, GAME_WIDTH, GAME_HEIGHT, 'bg_far').setOrigin(0, 0); + this.bgFar.setScrollFactor(0, 0); + this.bgFar.tileScaleX = farScale; + this.bgFar.tileScaleY = farScale; + this._bgFarPanRange = Math.max(0, farNative.width - GAME_WIDTH / farScale); - const mid = this.add.tileSprite(bounds.x, GAME_HEIGHT * 0.35, width, GAME_HEIGHT * 0.65, 'bg_mid').setOrigin(0, 0); - mid.setScrollFactor(0.45, 0); + // Mid/near are ground-level layers stacked from the bottom of the + // screen upward, each at its own uniform (non-stretching) scale, tiled + // across the level via the normal scrollFactor-driven parallax. Repeat + // count isn't pinned to an exact number - it falls out of each layer's + // target height, and near's is shorter than mid's, so it naturally + // repeats more often across the same span. + this._addGroundLayer('bg_mid', bounds.x, width, GAME_HEIGHT * 0.6, 0.45); + this._addGroundLayer('bg_near', bounds.x, width, GAME_HEIGHT * 0.35, 0.75); + } - const near = this.add.tileSprite(bounds.x, GAME_HEIGHT * 0.55, width, GAME_HEIGHT * 0.45, 'bg_near').setOrigin(0, 0); - near.setScrollFactor(0.75, 0); + _nativeTextureSize(key) { + const src = this.textures.get(key).getSourceImage(); + return { width: src.width, height: src.height }; + } + + // Adds a bottom-anchored parallax layer scaled uniformly (same factor on + // both axes, so its native aspect ratio is preserved - no stretch), + // tiled horizontally across `displayWidth` however many times that scale + // naturally allows. + _addGroundLayer(key, x, displayWidth, targetHeight, scrollFactorX) { + const native = this._nativeTextureSize(key); + const scale = targetHeight / native.height; + const y = GAME_HEIGHT - targetHeight; + const layer = this.add.tileSprite(x, y, displayWidth, targetHeight, key).setOrigin(0, 0); + layer.tileScaleX = scale; + layer.tileScaleY = scale; + layer.setScrollFactor(scrollFactorX, 0); + return layer; + } + + // Drives bg_far's pan independent of camera scroll: 0 at the bus's start + // position, 1 at the goal, clamped so overshoot/backing up doesn't run + // the image past its own ends. + _updateParallax() { + if (!this.bgFar) return; + const startX = this.level.startPosition.x; + const endX = this.level.goal.x; + const progress = Phaser.Math.Clamp((this.bus.chassis.x - startX) / Math.max(1, endX - startX), 0, 1); + this.bgFar.tilePositionX = progress * this._bgFarPanRange; } _buildGoal() { diff --git a/src/systems/KidManager.js b/src/systems/KidManager.js index a76d379..457e64b 100644 --- a/src/systems/KidManager.js +++ b/src/systems/KidManager.js @@ -1,5 +1,5 @@ import Phaser from 'phaser'; -import { GFORCE } from '../config.js'; +import { GFORCE, KID } from '../config.js'; import Kid from '../entities/Kid.js'; export default class KidManager { @@ -16,6 +16,25 @@ export default class KidManager { this._onGForceExceeded = this._onGForceExceeded.bind(this); scene.events.on('gforce-exceeded', this._onGForceExceeded); + + // Ejected kids set body.ignoreGravity (see Kid.eject) so the engine's + // own gravity application skips them entirely; this replaces it with a + // lighter fraction, applied here (before the engine integrates the + // step, matching how Matter itself applies gravity) rather than in + // Kid.update, since it needs the world's live gravity config, which + // KidManager already has scene access to. + this._onBeforeUpdate = this._onBeforeUpdate.bind(this); + scene.matter.world.on('beforeupdate', this._onBeforeUpdate); + } + + _onBeforeUpdate() { + const gravity = this.scene.matter.world.engine.gravity; + const g = gravity.y * gravity.scale * KID.ejectedGravityScale; + for (const kid of this.kids) { + if (kid.state === 'aboard') continue; + const body = kid.image.body; + body.force.y += body.mass * g; + } } get kidsAboardCount() { @@ -31,7 +50,7 @@ export default class KidManager { this._lastEjectTime = now; const kid = Phaser.Utils.Array.GetRandom(aboard); - kid.eject(this.bus.chassis.body.velocity, gForce); + kid.eject(gForce); this.scene.events.emit('kid-ejected', { kidsAboard: this.kidsAboardCount,