From 9da32195296ff7705cb297b69289056c7711ef60 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Thu, 20 Aug 2026 11:36:06 -0600 Subject: [PATCH] improve bus compartment re-boarding logic and wall thickness - Add `isSettledRelativeToChassis()` to measure settling speed relative to the bus instead of absolute world velocity, allowing kids resting inside a moving bus to re-board properly - Gate re-boarding on being within the compartment's horizontal width to prevent false positives from kids resting on the ground at the bus's height - Use a separate settling timer (`_slowSinceRelative`) to prevent interference between absolute and relative settle checks - Increase `compartmentWallThickness` to 16 for better physical collision response against fast-moving kids --- src/config.js | 5 ++++- src/entities/Kid.js | 21 +++++++++++++++++++++ src/systems/KidManager.js | 24 +++++++++++++++++------- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/config.js b/src/config.js index 6bdea21..9f29147 100644 --- a/src/config.js +++ b/src/config.js @@ -49,7 +49,10 @@ export const BUS = { // back below it while inside compartmentHalfWidth re-contains the kid. // Tune by playtesting - not physically derived. compartmentHalfWidth: 75 * WORLD_SCALE, - compartmentWallThickness: 10 * WORLD_SCALE, + // A bit thicker than the floor - gives a fast-moving kid more physical + // wall to hit before KidManager's per-tick clamp is what's actually + // stopping it, rather than the fixture itself. + compartmentWallThickness: 16 * WORLD_SCALE, compartmentFloorThickness: 10 * WORLD_SCALE, compartmentFloorY: 12 * WORLD_SCALE, compartmentTopY: -60 * WORLD_SCALE, diff --git a/src/entities/Kid.js b/src/entities/Kid.js index 6933faf..2a56316 100644 --- a/src/entities/Kid.js +++ b/src/entities/Kid.js @@ -13,6 +13,7 @@ export default class Kid { this.seatIndex = seatIndex; this.state = 'aboard'; // aboard | ejected this._slowSince = null; // see isSettled() + this._slowSinceRelative = null; // see isSettledRelativeToChassis() const worldPos = bus.getSeatWorldPosition(seatIndex); @@ -91,6 +92,26 @@ export default class Kid { return now - this._slowSince >= KID.settleDurationMs; } + // Same idea as isSettled, but measured relative to the chassis's own + // velocity rather than the world - a kid at rest inside a moving bus + // shares the bus's velocity, which is almost always well above + // restSpeedThreshold, so gating re-boarding on isSettled's absolute speed + // would mean a kid resting inside the compartment while the bus drives + // could never actually re-board. Kept as its own method with its own + // timer (not a parameter on isSettled) since KidManager and PlayScene call + // these in the same tick with different references - sharing one timer + // between them would have each call reset the other's progress. + isSettledRelativeToChassis(now, chassisVelocity) { + const v = this.image.body.velocity; + const speed = Math.hypot(v.x - chassisVelocity.x, v.y - chassisVelocity.y); + if (speed >= KID.restSpeedThreshold) { + this._slowSinceRelative = null; + return false; + } + if (this._slowSinceRelative === null) this._slowSinceRelative = now; + return now - this._slowSinceRelative >= KID.settleDurationMs; + } + destroy() { this.image.destroy(); } diff --git a/src/systems/KidManager.js b/src/systems/KidManager.js index c9a66b6..c2f615d 100644 --- a/src/systems/KidManager.js +++ b/src/systems/KidManager.js @@ -49,7 +49,7 @@ export default class KidManager { // chassis's own rotated frame. A kid drifting up past the open top // becomes ejected immediately. A floating kid drifting back down through // the open top only re-boards once it has actually come to rest inside - // the compartment (see Kid.isSettled), so a kid still mid-arc can't + // the compartment (see Kid.isSettledRelativeToChassis), so a kid still mid-arc can't // teleport back in mid-air. _onAfterUpdate() { const chassis = this.bus.chassis; @@ -62,19 +62,29 @@ export default class KidManager { const dx = kid.image.x - chassis.x; const dy = kid.image.y - chassis.y; + const localX = dx * cos + dy * sin; const localY = -dx * sin + dy * cos; const isAboveOpenTop = localY < BUS.compartmentTopY; + // Re-boarding additionally requires being within the compartment's + // horizontal span, not just below the open top - localY alone can't + // tell "resting inside the compartment" apart from "resting on the + // ground anywhere else in the level at roughly the bus's height," + // since compartmentTopY is measured purely along the chassis's local Y + // axis with no horizontal extent to it. + const isWithinCompartmentWidth = Math.abs(localX) <= BUS.compartmentHalfWidth; if (isAboveOpenTop && kid.state === 'aboard') { kid.markEjected(); playRandomKidFallSound(this.scene); anyChanged = true; - } else if (!isAboveOpenTop && kid.state === 'ejected' && kid.isSettled(this.scene.time.now)) { - // Re-boarding is gated on the kid actually settling (see Kid.isSettled), - // not just passing through the compartment's vertical band again. Ejected - // kids run under reduced gravity, so a kid still floating/arc-ing will - // happily drift back through the open top while airborne; without this - // gate it would flip back to aboard mid-air and "teleport" back in. + } else if (!isAboveOpenTop && isWithinCompartmentWidth && kid.state === 'ejected' && + kid.isSettledRelativeToChassis(this.scene.time.now, chassis.body.velocity)) { + // Re-boarding is gated on the kid actually settling relative to the + // chassis (see Kid.isSettledRelativeToChassis), not just passing + // through the compartment's open top again. Ejected kids run under + // reduced gravity, so a kid still floating/arc-ing will happily + // drift back through the open top while airborne; without this gate + // it would flip back to aboard mid-air and "teleport" back in. kid.markAboard(); anyChanged = true; }