Fix all-kids-lost failing too early; add bus-fell fail condition
The level now only fails on "all kids lost" once every ejected kid has settled outside the bus (restSpeed/settleDuration hysteresis) or fallen past the camera bounds, giving a rescue window after a jump. Also fails when the bus itself drops below the level, and guards the compartment clamp with a distance radius so far-away kids aren't yanked toward the bus.
This commit is contained in:
parent
c4954b2292
commit
f3e7f96220
|
|
@ -53,6 +53,17 @@ export const BUS = {
|
|||
compartmentFloorThickness: 10 * WORLD_SCALE,
|
||||
compartmentFloorY: 12 * WORLD_SCALE,
|
||||
compartmentTopY: -60 * WORLD_SCALE,
|
||||
// The clamp in KidManager only makes sense as a same-tick tunnel-through
|
||||
// guard against the compartment's OWN fixtures - a kid further than this
|
||||
// from the chassis was never inside the compartment to begin with, and
|
||||
// local-frame coordinates alone can't tell "just tunneled through" apart
|
||||
// from "resting somewhere else in the level entirely" without a distance
|
||||
// check like this (a kid at rest far away can still end up with local
|
||||
// coordinates that look like a boundary violation purely because the
|
||||
// chassis itself has moved since). Generous enough to still catch a
|
||||
// genuinely fast tunnel-through, nowhere near large enough to reach a kid
|
||||
// that's actually elsewhere on the level.
|
||||
compartmentClampRadius: 400 * WORLD_SCALE,
|
||||
|
||||
// Density/friction/stiffness/damping are dimensionless ratios, not pixel
|
||||
// distances - they don't scale with WORLD_SCALE.
|
||||
|
|
@ -138,6 +149,23 @@ export const KID = {
|
|||
// bus does, Snuggle-Truck style. Primary tuning knob for "how floaty,"
|
||||
// not physically derived.
|
||||
ejectedGravityScale: 0.35,
|
||||
// How slow (raw Matter velocity magnitude, same convention as everything
|
||||
// else that reads body.velocity directly in this codebase) an ejected
|
||||
// kid needs to be moving to count as "settled" outside the bus - used by
|
||||
// PlayScene to delay the all-kids-lost failure until every kid has
|
||||
// actually landed and stopped, not the instant the last one leaves the
|
||||
// compartment, so a jump that pops everyone loose still leaves a window
|
||||
// to drive back underneath and catch them. Calibrated via a headless
|
||||
// matter-js simulation of a kid landing with realistic horizontal speed
|
||||
// under ejectedGravityScale: settles below this within ~3s of landing,
|
||||
// fully stops within ~10s - tune by playtesting if that feels off.
|
||||
restSpeedThreshold: 1,
|
||||
// Must stay below restSpeedThreshold continuously for this long to count
|
||||
// as settled, not just an instant - a kid arcing under weak gravity can
|
||||
// pass through near-zero speed for a single tick right at the peak of
|
||||
// its jump while still very much airborne (about to fall again, not
|
||||
// landed), so a momentary dip alone isn't proof it's actually stopped.
|
||||
settleDurationMs: 500,
|
||||
};
|
||||
|
||||
export const CAMERA = {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export default class Kid {
|
|||
this.bus = bus;
|
||||
this.seatIndex = seatIndex;
|
||||
this.state = 'aboard'; // aboard | ejected
|
||||
this._slowSince = null; // see isSettled()
|
||||
|
||||
const worldPos = bus.getSeatWorldPosition(seatIndex);
|
||||
|
||||
|
|
@ -73,6 +74,23 @@ export default class Kid {
|
|||
this.image.body.ignoreGravity = false;
|
||||
}
|
||||
|
||||
// True once this kid's speed has stayed below KID.restSpeedThreshold for
|
||||
// KID.settleDurationMs straight - used by PlayScene to tell "actually
|
||||
// landed and stopped" apart from a kid just passing through low speed for
|
||||
// an instant at the peak of its arc while still airborne. Only meaningful
|
||||
// to call repeatedly (e.g. every frame) once ejected - the timer resets
|
||||
// itself the moment speed rises back above threshold.
|
||||
isSettled(now) {
|
||||
const v = this.image.body.velocity;
|
||||
const speed = Math.hypot(v.x, v.y);
|
||||
if (speed >= KID.restSpeedThreshold) {
|
||||
this._slowSince = null;
|
||||
return false;
|
||||
}
|
||||
if (this._slowSince === null) this._slowSince = now;
|
||||
return now - this._slowSince >= KID.settleDurationMs;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.image.destroy();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createButton } from '../util/ui.js';
|
|||
|
||||
const REASON_TEXT = {
|
||||
'all-kids-lost': 'Every kid got thrown off the bus!',
|
||||
'bus-fell': 'The bus fell into the gap!',
|
||||
};
|
||||
|
||||
export default class LevelFailedScene extends Phaser.Scene {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export default class PlayScene extends Phaser.Scene {
|
|||
this.bus.applyIntent(intent);
|
||||
this.cameraRig.update();
|
||||
this._updateParallax();
|
||||
this._checkBusFell();
|
||||
this._checkAllKidsLost();
|
||||
|
||||
if (DEBUG && this.debugText) {
|
||||
const g = this.gForceMonitor.lastGForce || 0;
|
||||
|
|
@ -121,6 +123,39 @@ export default class PlayScene extends Phaser.Scene {
|
|||
this.bgFar.tilePositionX = progress * this._bgFarPanRange;
|
||||
}
|
||||
|
||||
// A bus that's fallen past the bottom of the level's own camera bounds
|
||||
// is somewhere the camera can never scroll down far enough to show again
|
||||
// (that's the literal definition of "off screen" here) - treated the
|
||||
// same as losing every kid, since there's no way to recover either.
|
||||
_checkBusFell() {
|
||||
if (this._isBelowCameraBounds(this.bus.chassis.y)) {
|
||||
this.events.emit('level-failed', { reason: 'bus-fell' });
|
||||
}
|
||||
}
|
||||
|
||||
_isBelowCameraBounds(y) {
|
||||
const bounds = this.level.cameraBounds;
|
||||
return y > bounds.y + bounds.height;
|
||||
}
|
||||
|
||||
// 0 kids aboard isn't itself a fail condition - KidManager just tracks
|
||||
// who's still seated. A kid popped loose by a jump is still catchable
|
||||
// (falls down through the compartment's open top and re-boards - see
|
||||
// KidManager) right up until it's either settled outside the bus or lost
|
||||
// down a gap, so the level only fails once EVERY kid has reached one of
|
||||
// those two unrecoverable states, not the instant the last one leaves.
|
||||
_checkAllKidsLost() {
|
||||
if (this.kidManager.kidsAboardCount > 0) return;
|
||||
|
||||
const allUnrecoverable = this.kidManager.kids.every((kid) => {
|
||||
return this._isBelowCameraBounds(kid.image.y) || kid.isSettled(this.time.now);
|
||||
});
|
||||
|
||||
if (allUnrecoverable) {
|
||||
this.events.emit('level-failed', { reason: 'all-kids-lost' });
|
||||
}
|
||||
}
|
||||
|
||||
_buildGoal() {
|
||||
const goal = this.level.goal;
|
||||
this.goalBody = this.matter.add.rectangle(goal.x, goal.y, goal.width, goal.height, {
|
||||
|
|
|
|||
|
|
@ -78,10 +78,11 @@ export default class KidManager {
|
|||
kidsAboard: this.kidsAboardCount,
|
||||
total: this.total,
|
||||
});
|
||||
|
||||
if (this.kidsAboardCount === 0) {
|
||||
this.scene.events.emit('level-failed', { reason: 'all-kids-lost' });
|
||||
}
|
||||
// No level-failed here even at 0 aboard - PlayScene decides that once
|
||||
// every kid has actually settled outside the bus (or fallen down a
|
||||
// gap), not the instant the last one leaves, so a jump that pops
|
||||
// everyone loose still leaves a window to drive back underneath and
|
||||
// catch them (see PlayScene._checkAllKidsLost).
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +93,16 @@ export default class KidManager {
|
|||
_clampToCompartment(kid, chassis, cos, sin) {
|
||||
const dx = kid.image.x - chassis.x;
|
||||
const dy = kid.image.y - chassis.y;
|
||||
|
||||
// A kid genuinely outside compartmentClampRadius was never inside the
|
||||
// compartment this tick - skip it entirely, rather than let the below
|
||||
// checks (which only bound ONE axis each) treat "far away in the other
|
||||
// axis" as a boundary violation. Without this, a kid resting anywhere
|
||||
// else in the level could get yanked toward wherever the bus currently
|
||||
// is, since local-frame coordinates alone don't distinguish "just
|
||||
// tunneled through" from "elsewhere entirely."
|
||||
if (dx * dx + dy * dy > BUS.compartmentClampRadius * BUS.compartmentClampRadius) return;
|
||||
|
||||
let localX = dx * cos + dy * sin;
|
||||
let localY = -dx * sin + dy * cos;
|
||||
let hitFloor = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue