113 lines
5.0 KiB
JavaScript
113 lines
5.0 KiB
JavaScript
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
|
|
this._slowSince = null; // see isSettled()
|
|
// Latch set on the first aboard->ejected transition (see markEjected)
|
|
// and never cleared again - KidManager uses it to make re-boarding a
|
|
// one-shot event, so a kid that landed on the ground outside the bus
|
|
// can't be pulled back in later just because the bus happens to be
|
|
// under them at that height (see KidManager._onAfterUpdate).
|
|
this.ejected = false;
|
|
// Wall-clock time (scene.time.now) of the last aboard->ejected
|
|
// transition - KidManager uses this to briefly delay the catch window
|
|
// re-opening after a fresh ejection (see KidManager._onAfterUpdate / the
|
|
// KID.ceilingReboardDelayMs config), so the catch doesn't just undo an
|
|
// ejection a few ticks later (the kid is still drifting slowly back down
|
|
// through the open top right after being pushed out).
|
|
this.ejectedAt = null;
|
|
|
|
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,
|
|
// 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);
|
|
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);
|
|
}
|
|
|
|
// 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.ejected = true; // latch, see field comment above - never unset
|
|
this.ejectedAt = this.scene.time.now;
|
|
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;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|