From c4954b2292ea542c84bbe3862245eb85fdcaa1e5 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Wed, 19 Aug 2026 21:05:08 -0600 Subject: [PATCH] Replace impact-based kid ejection with open-top compartment Ejection used to be g-force-triggered, but the suspension isolates the chassis from shocks so the old threshold was effectively unreachable - kids were never actually being ejected. Instead the bus now carries an invisible three-sided box (floor + left/right walls, deliberately no ceiling) pinned to the chassis with rigid constraints. The fixtures are standalone bodies in a dedicated kid collision category, not compounded into the chassis, because Matter only consults the root body's filter on compounds, so only standalone bodies can be made to collide with kids while the chassis part stays exempt. KidManager now checks each kid's rotation-corrected chassis-local position every physics tick and flips state in both directions: crossing above the open top marks the kid ejected (light custom gravity, kid_ejected texture), and drifting back down through it re-marks the kid aboard. A per-tick hard clamp keeps kids from tunneling through the floor/walls on a fast hit. Seat constraints and the settle timer are gone - containment is purely physical collision. GForceMonitor is demoted to a debug-only readout for landing diagnosis. Kid art is now 5-frame spritesheets (one distinct kid per seat), with the frame picked by seat index in Kid.js, and render order is fixed with explicit depths (kids 1, bus chassis/wheels 2). --- assets/sprites/kid_ejected.png | Bin 0 -> 27166 bytes assets/sprites/kid_idle.png | Bin 0 -> 17231 bytes src/config.js | 51 ++++++------ src/entities/Bus.js | 72 ++++++++++++++++- src/entities/Kid.js | 116 +++++++++++++-------------- src/scenes/PlayScene.js | 16 +--- src/scenes/PreloadScene.js | 6 +- src/systems/GForceMonitor.js | 29 +++---- src/systems/KidManager.js | 138 +++++++++++++++++++++++++-------- src/util/assetManifest.js | 10 ++- 10 files changed, 285 insertions(+), 153 deletions(-) create mode 100644 assets/sprites/kid_ejected.png create mode 100644 assets/sprites/kid_idle.png diff --git a/assets/sprites/kid_ejected.png b/assets/sprites/kid_ejected.png new file mode 100644 index 0000000000000000000000000000000000000000..73ec214aa8b221de1de815e64427ec7d9912cf23 GIT binary patch literal 27166 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 literal 0 HcmV?d00001 diff --git a/assets/sprites/kid_idle.png b/assets/sprites/kid_idle.png new file mode 100644 index 0000000000000000000000000000000000000000..5c713e31d9f91a85398fbc11f2df0a16ed34dff9 GIT binary patch literal 17231 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 i`DM0%&lB-Zg#Qod(=m3yUgOgM0000 ({ + pointA: { x: anchorX, y: localY }, + pointB: { x: 0, y: 0 }, + }); + matter.add.constraint(this.chassis, body, 0, 1, pinOptions(localX - pinSpread)); + matter.add.constraint(this.chassis, body, 0, 1, pinOptions(localX + pinSpread)); + + return body; + } + _onAfterUpdate() { this._clampWheelToWishbone(this.wheelRear); this._clampWheelToWishbone(this.wheelFront); @@ -162,9 +229,10 @@ export default class Bus { frictionStatic: BUS.wheelFrictionStatic, frictionAir: BUS.wheelFrictionAir, restitution: BUS.wheelRestitution, - collisionFilter: { group: this.group }, + collisionFilter: { group: this.group, category: this.busCategory }, }); wheel.setDisplaySize(BUS.wheelRadius * 2, BUS.wheelRadius * 2); + wheel.setDepth(2); // matches the chassis - see its setDepth comment return wheel; } diff --git a/src/entities/Kid.js b/src/entities/Kid.js index b67837c..730bad1 100644 --- a/src/entities/Kid.js +++ b/src/entities/Kid.js @@ -1,85 +1,79 @@ -import Phaser from 'phaser'; -import { GFORCE, KID } from '../config.js'; +import { KID } from '../config.js'; +// A kid has no constraint holding it to the bus at all - it's a free body, +// contained purely by physically colliding with the invisible floor/wall +// fixtures pinned to the chassis (see Bus.js's _buildCompartment). state is +// a derived label KidManager updates every tick from the kid's actual +// position relative to the chassis's open top (see +// KidManager._onAfterUpdate), not something this class decides on its own. export default class Kid { constructor(scene, bus, seatIndex) { this.scene = scene; this.bus = bus; this.seatIndex = seatIndex; - this.state = 'aboard'; // aboard | ejected | settled + this.state = 'aboard'; // aboard | ejected - const seat = bus.seatOffsets[seatIndex]; const worldPos = bus.getSeatWorldPosition(seatIndex); this.image = scene.matter.add.image(worldPos.x, worldPos.y, 'kid_idle', null, { shape: { type: 'circle', radius: KID.radius }, density: KID.density, friction: KID.friction, - collisionFilter: { group: bus.group }, + // Exempt from the chassis/wheels forever (contained or not) via mask + // rather than by sharing the bus's group - sharing a group would also + // make every kid on this bus mutually exempt from every OTHER kid + // (group exemption is "never collide with anything sharing this + // group," not "never collide with this one specific body"), which is + // exactly why kids used to pass through each other. Containment + // itself is the floor/wall fixtures' job, which target this category + // specifically (see bus.kidCategory in Bus.js) so they still collide + // with the kid despite this mask. + collisionFilter: { category: bus.kidCategory, mask: 0xffffffff & ~bus.busCategory }, }); 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, - pointB: { x: 0, y: 0 }, - damping: KID.seatDamping, - }); - - this._settleAt = 0; + this._applyTexture('kid_idle'); + // Between the background/terrain (default depth 0) and the bus + // (explicit depth 2, see Bus.js) - visible over the ground, but drawn + // over by the chassis/wheels, contained or not. + this.image.setDepth(1); } - eject(gForce) { - if (this.state !== 'aboard') return; - this.state = 'ejected'; - - this.scene.matter.world.removeConstraint(this.seatConstraint, true); - this.seatConstraint = null; - - // Neutral group re-enables normal category/mask collision (was sharing - // the bus's non-colliding group while seated). - this.image.body.collisionFilter.group = 0; - this.image.setTexture('kid_ejected'); - - // 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: 0, - y: -GFORCE.ejectImpulseMagnitude * severity, - }); - - this._settleAt = this.scene.time.now + KID.settleTimeMs; - } - - update(time) { - if (this.state === 'ejected' && time >= this._settleAt) { - this.state = 'settled'; + // kid_idle/kid_ejected are 5-frame sheets (one distinct kid per frame, + // matching BUS.maxSeats) - picks the frame matching this kid's own seat + // so it stays visually the same character whichever texture it's on. + // Falls back to frame 0 for the single-frame placeholder texture used + // when the real art fails to load (see placeholderTextures.js). + _applyTexture(key) { + if (this.scene.textures.get(key).has(this.seatIndex)) { + this.image.setTexture(key, this.seatIndex); + } else { + this.image.setTexture(key); } } + // Called by KidManager when this kid's position rises above the open top + // of the compartment. + markEjected() { + if (this.state === 'ejected') return; + this.state = 'ejected'; + this._applyTexture('kid_ejected'); + // Ignores the world's real gravity - KidManager applies a lighter, + // custom fraction of it instead (see KidManager._onBeforeUpdate) while + // ejected, so it floats through a much bigger arc than the bus's own + // fall would. + this.image.body.ignoreGravity = true; + } + + // Called by KidManager when a floating kid drifts back down through the + // open top and lands back inside the compartment. + markAboard() { + if (this.state === 'aboard') return; + this.state = 'aboard'; + this._applyTexture('kid_idle'); + this.image.body.ignoreGravity = false; + } + destroy() { - // No removeConstraint() here (unlike eject(), which runs mid-gameplay - // and needs it): destroy() only ever runs from PlayScene's shutdown - // cleanup, by which point Phaser's Matter World plugin may have already - // torn down (nulled) this.scene.matter.world, and World#shutdown() - // already clears every constraint/body itself anyway. - this.seatConstraint = null; this.image.destroy(); } } diff --git a/src/scenes/PlayScene.js b/src/scenes/PlayScene.js index bb9d704..01234fc 100644 --- a/src/scenes/PlayScene.js +++ b/src/scenes/PlayScene.js @@ -33,21 +33,14 @@ export default class PlayScene extends Phaser.Scene { this.inputController = new InputController(this); this.gForceMonitor = new GForceMonitor(this, this.bus.chassis.body); - // Measured via headless testing at the original gravity (1): the - // spawn-to-ground drop settled by ~4.5s. Free-fall time scales as - // 1/sqrt(g), and gravity is now 0.675 (see main.js), so this is scaled - // up by ~1.2x (sqrt(1/0.675)) to ~5.5s rather than re-measured - - // re-measure for real if landings still trip the g-force ejection check - // right at level start. Re-measure this if startPosition, WORLD_SCALE, - // gravity, or the bus's mass/suspension change again. - this.gForceMonitor.startGracePeriod(5500); + this.gForceMonitor.resetBaseline(); this.cameraRig = new CameraRig(this, this.bus.chassis, this.level.cameraBounds); this._buildGoal(); this._buildHud(); - this.events.on('kid-ejected', this._onKidEjected, this); + this.events.on('kid-status-changed', this._onKidStatusChanged, this); this.events.on('level-failed', this._onLevelFailed, this); this.matter.world.on('collisionstart', this._onCollisionStart, this); @@ -60,7 +53,6 @@ export default class PlayScene extends Phaser.Scene { const intent = this.inputController.getIntent(); this.bus.applyIntent(intent); - this.kidManager.update(time); this.cameraRig.update(); this._updateParallax(); @@ -167,7 +159,7 @@ export default class PlayScene extends Phaser.Scene { } } - _onKidEjected({ kidsAboard, total }) { + _onKidStatusChanged({ kidsAboard, total }) { const ejectedCount = total - kidsAboard; for (let i = 0; i < this.hudIcons.length; i++) { this.hudIcons[i].setAlpha(i < ejectedCount ? 0.2 : 1); @@ -203,7 +195,7 @@ export default class PlayScene extends Phaser.Scene { } _cleanup() { - this.events.off('kid-ejected', this._onKidEjected, this); + this.events.off('kid-status-changed', this._onKidStatusChanged, this); this.events.off('level-failed', this._onLevelFailed, this); // No matter.world.off() here: Phaser's Matter World plugin registers its // own 'shutdown' listener before this scene's create() ever runs, so it diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index 8e7501d..55089f4 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -17,7 +17,11 @@ export default class PreloadScene extends Phaser.Scene { }); for (const entry of ASSET_MANIFEST) { - this.load.image(entry.key, entry.path); + if (entry.frameWidth) { + this.load.spritesheet(entry.key, entry.path, { frameWidth: entry.frameWidth, frameHeight: entry.frameHeight }); + } else { + this.load.image(entry.key, entry.path); + } } } diff --git a/src/systems/GForceMonitor.js b/src/systems/GForceMonitor.js index 56b31fd..bb8394b 100644 --- a/src/systems/GForceMonitor.js +++ b/src/systems/GForceMonitor.js @@ -1,24 +1,26 @@ -import { GFORCE, PHYSICS_TIMESTEP_SECONDS } from '../config.js'; +import { PHYSICS_TIMESTEP_SECONDS, GFORCE } from '../config.js'; -// Pure detector: samples the chassis's linear velocity every fixed physics -// step, estimates acceleration via finite difference, and emits -// 'gforce-exceeded' on the scene when it crosses the tunable threshold. -// Ejection logic lives elsewhere (KidManager) so this stays a single-purpose -// sensor. +// Pure debug diagnostic: kid ejection is no longer impact-triggered (see +// KidManager/Bus's open-top compartment box) - this just samples the +// chassis's linear velocity every fixed physics step and estimates +// acceleration via finite difference, purely for the DEBUG g-force readout +// in PlayScene. export default class GForceMonitor { constructor(scene, chassisBody) { this.scene = scene; this.chassisBody = chassisBody; this.previousVelocity = { x: 0, y: 0 }; - this.graceUntil = 0; this.enabled = true; + this.lastGForce = 0; this._onAfterUpdate = this._onAfterUpdate.bind(this); scene.matter.world.on('afterupdate', this._onAfterUpdate); } - startGracePeriod(durationMs = GFORCE.gracePeriodMs) { - this.graceUntil = this.scene.time.now + durationMs; + // Resets the velocity baseline to whatever it is right now, so the next + // tick's comparison isn't against a stale one - call this once whatever + // isn't a "real" impact (e.g. the spawn drop) has settled. + resetBaseline() { const v = this.chassisBody.velocity; this.previousVelocity = { x: v.x, y: v.y }; } @@ -33,16 +35,9 @@ export default class GForceMonitor { const accelPxPerSec2 = deltaV / PHYSICS_TIMESTEP_SECONDS; const accelMPerSec2 = accelPxPerSec2 / GFORCE.pixelsPerMeter; - const gForce = accelMPerSec2 / 9.8; + this.lastGForce = accelMPerSec2 / 9.8; this.previousVelocity = { x: v.x, y: v.y }; - this.lastGForce = gForce; - - if (this.scene.time.now < this.graceUntil) return; - - if (gForce > GFORCE.ejectThresholdG) { - this.scene.events.emit('gforce-exceeded', { gForce }); - } } destroy() { diff --git a/src/systems/KidManager.js b/src/systems/KidManager.js index 457e64b..70d7397 100644 --- a/src/systems/KidManager.js +++ b/src/systems/KidManager.js @@ -1,5 +1,4 @@ -import Phaser from 'phaser'; -import { GFORCE, KID } from '../config.js'; +import { KID, BUS } from '../config.js'; import Kid from '../entities/Kid.js'; export default class KidManager { @@ -8,23 +7,27 @@ export default class KidManager { this.bus = bus; this.total = kidsAboard; this.kids = []; - this._lastEjectTime = -Infinity; for (let i = 0; i < kidsAboard; i++) { this.kids.push(new Kid(scene, bus, i)); } - 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. + // A kid past the open top (see Kid.markEjected) sets body.ignoreGravity + // so the engine's own gravity application skips it 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 an afterupdate hook. this._onBeforeUpdate = this._onBeforeUpdate.bind(this); scene.matter.world.on('beforeupdate', this._onBeforeUpdate); + + // Drives the actual aboard/ejected transition, in both directions - see + // _onAfterUpdate. + this._onAfterUpdate = this._onAfterUpdate.bind(this); + scene.matter.world.on('afterupdate', this._onAfterUpdate); + } + + get kidsAboardCount() { + return this.kids.filter((kid) => kid.state === 'aboard').length; } _onBeforeUpdate() { @@ -37,37 +40,108 @@ export default class KidManager { } } - get kidsAboardCount() { - return this.kids.filter((kid) => kid.state === 'aboard').length; - } + // Every physics step: hard-clamps each kid against the floor/walls (see + // _clampToCompartment - same "soft collision + hard clamp" pairing as + // Bus.js's wheel wishbone, since the fixtures' own collision response + // alone isn't a hard guarantee against a fast enough hit), then checks + // the (now-corrected) position against the compartment's open top in the + // chassis's own rotated frame and flips state whichever way that puts it + // - a kid drifting up past the open top becomes ejected, and a floating + // kid drifting back down through it becomes aboard again, both from the + // same check. There's no synthetic impulse or event driving either + // direction - it's purely "is this kid still inside the box." + _onAfterUpdate() { + const chassis = this.bus.chassis; + const cos = Math.cos(chassis.rotation); + const sin = Math.sin(chassis.rotation); - _onGForceExceeded({ gForce }) { - const now = this.scene.time.now; - if (now - this._lastEjectTime < GFORCE.ejectCooldownMs) return; + let anyChanged = false; + for (const kid of this.kids) { + this._clampToCompartment(kid, chassis, cos, sin); - const aboard = this.kids.filter((kid) => kid.state === 'aboard'); - if (aboard.length === 0) return; + const dx = kid.image.x - chassis.x; + const dy = kid.image.y - chassis.y; + const localY = -dx * sin + dy * cos; + const isAboveOpenTop = localY < BUS.compartmentTopY; - this._lastEjectTime = now; - const kid = Phaser.Utils.Array.GetRandom(aboard); - kid.eject(gForce); + if (isAboveOpenTop && kid.state === 'aboard') { + kid.markEjected(); + anyChanged = true; + } else if (!isAboveOpenTop && kid.state === 'ejected') { + kid.markAboard(); + anyChanged = true; + } + } - this.scene.events.emit('kid-ejected', { - kidsAboard: this.kidsAboardCount, - total: this.total, - }); + if (anyChanged) { + this.scene.events.emit('kid-status-changed', { + kidsAboard: this.kidsAboardCount, + total: this.total, + }); - if (this.kidsAboardCount === 0) { - this.scene.events.emit('level-failed', { reason: 'all-kids-lost' }); + if (this.kidsAboardCount === 0) { + this.scene.events.emit('level-failed', { reason: 'all-kids-lost' }); + } } } - update(time) { - for (const kid of this.kids) kid.update(time); + // Keeps a kid from ever tunneling through the floor or a side wall in a + // single hard hit, in the chassis's own rotated frame (same un-rotate + // technique as Bus.js's wheel clamp). Never clamps the top - that's the + // one side deliberately left open. + _clampToCompartment(kid, chassis, cos, sin) { + const dx = kid.image.x - chassis.x; + const dy = kid.image.y - chassis.y; + let localX = dx * cos + dy * sin; + let localY = -dx * sin + dy * cos; + let hitFloor = false; + let hitLeftWall = false; + let hitRightWall = false; + + const restY = BUS.compartmentFloorY - BUS.compartmentFloorThickness / 2 - KID.radius; + if (localY > restY) { + localY = restY; + hitFloor = true; + } + + // Walls only exist at/below the open top - a kid that's already escaped + // upward has nothing left to bump sideways into. + if (localY >= BUS.compartmentTopY) { + const leftBoundary = -BUS.compartmentHalfWidth + BUS.compartmentWallThickness / 2 + KID.radius; + const rightBoundary = BUS.compartmentHalfWidth - BUS.compartmentWallThickness / 2 - KID.radius; + if (localX < leftBoundary) { + localX = leftBoundary; + hitLeftWall = true; + } else if (localX > rightBoundary) { + localX = rightBoundary; + hitRightWall = true; + } + } + + if (!hitFloor && !hitLeftWall && !hitRightWall) return; + + const worldX = chassis.x + (localX * cos - localY * sin); + const worldY = chassis.y + (localX * sin + localY * cos); + kid.image.setPosition(worldX, worldY); + + // Kill only the relative velocity component(s) still driving it through + // whichever boundary caught it, same reasoning as the wheel clamp - a + // bump stop absorbing the hit, not bouncing off it. + const chassisBody = chassis.body; + const body = kid.image.body; + const relVX = body.velocity.x - chassisBody.velocity.x; + const relVY = body.velocity.y - chassisBody.velocity.y; + let localVX = relVX * cos + relVY * sin; + let localVY = -relVX * sin + relVY * cos; + if (hitFloor && localVY > 0) localVY = 0; + if (hitLeftWall && localVX < 0) localVX = 0; + if (hitRightWall && localVX > 0) localVX = 0; + const newRelVX = localVX * cos - localVY * sin; + const newRelVY = localVX * sin + localVY * cos; + kid.image.setVelocity(chassisBody.velocity.x + newRelVX, chassisBody.velocity.y + newRelVY); } destroy() { - this.scene.events.off('gforce-exceeded', this._onGForceExceeded); for (const kid of this.kids) kid.destroy(); } } diff --git a/src/util/assetManifest.js b/src/util/assetManifest.js index 2ff6cca..b17307a 100644 --- a/src/util/assetManifest.js +++ b/src/util/assetManifest.js @@ -6,8 +6,14 @@ import { WORLD_SCALE } from '../config.js'; export const ASSET_MANIFEST = [ { key: 'bus_chassis', path: 'assets/sprites/bus_chassis.png', width: 170 * WORLD_SCALE, height: 64 * WORLD_SCALE, kind: 'rect', color: 0x2255aa }, { key: 'bus_wheel', path: 'assets/sprites/bus_wheel.png', width: 34 * WORLD_SCALE, height: 34 * WORLD_SCALE, kind: 'circle', color: 0x222222 }, - { key: 'kid_idle', path: 'assets/sprites/kid_idle.png', width: 28 * WORLD_SCALE, height: 28 * WORLD_SCALE, kind: 'circle', color: 0xf2c14e }, - { key: 'kid_ejected', path: 'assets/sprites/kid_ejected.png', width: 28 * WORLD_SCALE, height: 28 * WORLD_SCALE, kind: 'circle', color: 0xe0553b }, + // Real art is a 5-frame sheet (one distinct kid per frame, 56x56px each, + // matching BUS.maxSeats) - frameWidth/frameHeight (raw source pixels, NOT + // WORLD_SCALE-relative) make PreloadScene load it as a spritesheet + // instead of a single squashed image; Kid.js picks the frame matching its + // seat index. width/height here stay the DISPLAY size of one frame, same + // as before - only used for the single-frame placeholder fallback. + { key: 'kid_idle', path: 'assets/sprites/kid_idle.png', width: 28 * WORLD_SCALE, height: 28 * WORLD_SCALE, kind: 'circle', color: 0xf2c14e, frameWidth: 56, frameHeight: 56 }, + { key: 'kid_ejected', path: 'assets/sprites/kid_ejected.png', width: 28 * WORLD_SCALE, height: 28 * WORLD_SCALE, kind: 'circle', color: 0xe0553b, frameWidth: 56, frameHeight: 56 }, { key: 'bg_far', path: 'assets/backgrounds/bg_far.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x8fc7e8, tileable: true }, { key: 'bg_mid', path: 'assets/backgrounds/bg_mid.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x6fae7d, tileable: true }, { key: 'bg_near', path: 'assets/backgrounds/bg_near.png', width: 256 * WORLD_SCALE, height: 540 * WORLD_SCALE, kind: 'rect', color: 0x4c8a5c, tileable: true },