import Phaser from 'phaser'; import { GFORCE, KID } from '../config.js'; export default class Kid { constructor(scene, bus, seatIndex) { this.scene = scene; this.bus = bus; this.seatIndex = seatIndex; this.state = 'aboard'; // aboard | ejected | settled 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 }, }); 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; } 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'; } } 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(); } }