monsterplex/src/entities/Kid.js

86 lines
3.1 KiB
JavaScript

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();
}
}