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
This commit is contained in:
Brian Fertig 2026-08-20 11:36:06 -06:00
parent 17fe6efd10
commit 9da3219529
3 changed files with 42 additions and 8 deletions

View File

@ -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,

View File

@ -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();
}

View File

@ -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;
}