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,
|
compartmentFloorThickness: 10 * WORLD_SCALE,
|
||||||
compartmentFloorY: 12 * WORLD_SCALE,
|
compartmentFloorY: 12 * WORLD_SCALE,
|
||||||
compartmentTopY: -60 * 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
|
// Density/friction/stiffness/damping are dimensionless ratios, not pixel
|
||||||
// distances - they don't scale with WORLD_SCALE.
|
// 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,"
|
// bus does, Snuggle-Truck style. Primary tuning knob for "how floaty,"
|
||||||
// not physically derived.
|
// not physically derived.
|
||||||
ejectedGravityScale: 0.35,
|
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 = {
|
export const CAMERA = {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default class Kid {
|
||||||
this.bus = bus;
|
this.bus = bus;
|
||||||
this.seatIndex = seatIndex;
|
this.seatIndex = seatIndex;
|
||||||
this.state = 'aboard'; // aboard | ejected
|
this.state = 'aboard'; // aboard | ejected
|
||||||
|
this._slowSince = null; // see isSettled()
|
||||||
|
|
||||||
const worldPos = bus.getSeatWorldPosition(seatIndex);
|
const worldPos = bus.getSeatWorldPosition(seatIndex);
|
||||||
|
|
||||||
|
|
@ -73,6 +74,23 @@ export default class Kid {
|
||||||
this.image.body.ignoreGravity = false;
|
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() {
|
destroy() {
|
||||||
this.image.destroy();
|
this.image.destroy();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { createButton } from '../util/ui.js';
|
||||||
|
|
||||||
const REASON_TEXT = {
|
const REASON_TEXT = {
|
||||||
'all-kids-lost': 'Every kid got thrown off the bus!',
|
'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 {
|
export default class LevelFailedScene extends Phaser.Scene {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
this.bus.applyIntent(intent);
|
this.bus.applyIntent(intent);
|
||||||
this.cameraRig.update();
|
this.cameraRig.update();
|
||||||
this._updateParallax();
|
this._updateParallax();
|
||||||
|
this._checkBusFell();
|
||||||
|
this._checkAllKidsLost();
|
||||||
|
|
||||||
if (DEBUG && this.debugText) {
|
if (DEBUG && this.debugText) {
|
||||||
const g = this.gForceMonitor.lastGForce || 0;
|
const g = this.gForceMonitor.lastGForce || 0;
|
||||||
|
|
@ -121,6 +123,39 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
this.bgFar.tilePositionX = progress * this._bgFarPanRange;
|
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() {
|
_buildGoal() {
|
||||||
const goal = this.level.goal;
|
const goal = this.level.goal;
|
||||||
this.goalBody = this.matter.add.rectangle(goal.x, goal.y, goal.width, goal.height, {
|
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,
|
kidsAboard: this.kidsAboardCount,
|
||||||
total: this.total,
|
total: this.total,
|
||||||
});
|
});
|
||||||
|
// No level-failed here even at 0 aboard - PlayScene decides that once
|
||||||
if (this.kidsAboardCount === 0) {
|
// every kid has actually settled outside the bus (or fallen down a
|
||||||
this.scene.events.emit('level-failed', { reason: 'all-kids-lost' });
|
// 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) {
|
_clampToCompartment(kid, chassis, cos, sin) {
|
||||||
const dx = kid.image.x - chassis.x;
|
const dx = kid.image.x - chassis.x;
|
||||||
const dy = kid.image.y - chassis.y;
|
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 localX = dx * cos + dy * sin;
|
||||||
let localY = -dx * sin + dy * cos;
|
let localY = -dx * sin + dy * cos;
|
||||||
let hitFloor = false;
|
let hitFloor = false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue