140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
import Phaser from '../vendor/phaser.js';
|
|
import { config } from '../config/Config.js';
|
|
import { toColor } from '../utils/Color.js';
|
|
|
|
/**
|
|
* The player's ship: an arcade-physics sprite that flies to wherever you click.
|
|
*
|
|
* All feel/balance numbers come from data/ship.json:
|
|
* - thrust, maxSpeed, drag → how it accelerates and coasts
|
|
* - rotSpeed → how fast it turns toward its heading (rad/s)
|
|
* - brakeDistance → where it starts easing off the throttle
|
|
* - arriveRadius / arriveSpeed → when it considers itself "arrived"
|
|
*/
|
|
export class Ship extends Phaser.Physics.Arcade.Sprite {
|
|
static TEXTURE_KEY = '__ship';
|
|
|
|
/** Draws the ship texture once per game (procedural, no assets). */
|
|
static ensureTexture(scene) {
|
|
if (scene.textures.exists(Ship.TEXTURE_KEY)) return;
|
|
|
|
const size = config.get('ship.size', 46);
|
|
const W = size;
|
|
const H = Math.round(size * 0.68);
|
|
const hull = toColor(config.get('ship.color', '#dfe7ff'));
|
|
const cockpit = toColor(config.get('ship.cockpitColor', '#41c7ff'));
|
|
|
|
// A simple dart, pointing right (angle 0).
|
|
const g = scene.make.graphics({ add: false });
|
|
g.fillStyle(hull, 1);
|
|
g.beginPath();
|
|
g.moveTo(W - 4, H / 2); // nose
|
|
g.lineTo(4, 3); // top rear
|
|
g.lineTo(12, H / 2); // tail notch
|
|
g.lineTo(4, H - 3); // bottom rear
|
|
g.closePath();
|
|
g.fillPath();
|
|
g.fillStyle(cockpit, 1);
|
|
g.fillCircle(Math.round(W - W * 0.3), H / 2, Math.max(3, Math.round(W * 0.085)));
|
|
g.generateTexture(Ship.TEXTURE_KEY, W, H);
|
|
g.destroy();
|
|
}
|
|
|
|
constructor(scene, x, y) {
|
|
Ship.ensureTexture(scene);
|
|
super(scene, x, y, Ship.TEXTURE_KEY);
|
|
|
|
scene.add.existing(this);
|
|
scene.physics.add.existing(this);
|
|
|
|
// Tuning (data/ship.json) -----------------------------------------
|
|
this.thrust = config.get('ship.thrust', 900); // px/s^2
|
|
this.maxSpeed = config.get('ship.maxSpeed', 480); // px/s
|
|
this.drag = config.get('ship.drag', 2.4); // 1/s, exponential decay
|
|
this.rotSpeed = config.get('ship.rotSpeed', 10); // rad/s
|
|
this.brakeDistance = config.get('ship.brakeDistance', 260); // px
|
|
this.arriveRadius = config.get('ship.arriveRadius', 8); // px
|
|
this.arriveSpeed = config.get('ship.arriveSpeed', 50); // px/s
|
|
this.setScale(config.get('ship.scale', 1));
|
|
|
|
this.target = null;
|
|
}
|
|
|
|
/** Set the destination to fly to (world coordinates). */
|
|
setTarget(x, y) {
|
|
this.target = { x, y };
|
|
}
|
|
|
|
/** Stop steering and brake immediately. */
|
|
stop() {
|
|
this.target = null;
|
|
this.body.acceleration.set(0, 0);
|
|
this.body.velocity.set(0, 0);
|
|
}
|
|
|
|
update(_time, delta) {
|
|
const body = this.body;
|
|
if (!body) return;
|
|
const dt = Math.min(delta, 64) / 1000;
|
|
|
|
if (this.target) {
|
|
const dx = this.target.x - this.x;
|
|
const dy = this.target.y - this.y;
|
|
const dist = Math.hypot(dx, dy);
|
|
const speed = body.velocity.length();
|
|
|
|
// Arrived: close enough and slow enough → stop cleanly.
|
|
if (dist <= this.arriveRadius && speed <= this.arriveSpeed) {
|
|
this.target = null;
|
|
// Clear acceleration too, so the physics step can't re-kick us.
|
|
body.acceleration.set(0, 0);
|
|
// Come to rest facing the direction we were traveling.
|
|
if (speed > 1) {
|
|
this.rotation = Phaser.Math.Angle.Wrap(Math.atan2(body.velocity.y, body.velocity.x));
|
|
}
|
|
body.velocity.set(0, 0);
|
|
return;
|
|
}
|
|
|
|
const nx = dx / dist;
|
|
const ny = dy / dist;
|
|
|
|
// Braking: with the throttle cut, drag carries the ship a further
|
|
// speed / drag before it stops. While that still lands short of the
|
|
// target, keep easing the throttle with distance; once we are too
|
|
// fast to stop in time, cut the throttle and let drag bleed the
|
|
// speed off, so we reach the target with little or no speed to spare.
|
|
const canStopInTime = speed <= this.drag * dist;
|
|
const throttle = canStopInTime
|
|
? Phaser.Math.Clamp(dist / this.brakeDistance, 0, 1)
|
|
: 0;
|
|
body.acceleration.set(nx * this.thrust * throttle, ny * this.thrust * throttle);
|
|
|
|
// Gentle drag so we never coast forever.
|
|
body.velocity.scale(Math.max(0, 1 - this.drag * dt));
|
|
|
|
// Hard speed cap.
|
|
const v = body.velocity.length();
|
|
if (v > this.maxSpeed) {
|
|
body.velocity.scale(this.maxSpeed / v);
|
|
}
|
|
|
|
// Heading: while the ship has speed it follows its actual direction
|
|
// of motion (so it never flies "backwards" as it brakes); when it
|
|
// (nearly) stands still it points at the target.
|
|
const wanted = speed > 10
|
|
? Phaser.Math.Angle.Wrap(Math.atan2(body.velocity.y, body.velocity.x))
|
|
: Phaser.Math.Angle.Wrap(Math.atan2(ny, nx));
|
|
const current = Phaser.Math.Angle.Wrap(this.rotation);
|
|
const step = Math.min(Math.abs(wanted - current), this.rotSpeed * dt);
|
|
this.rotation = Phaser.Math.Angle.RotateTo(current, wanted, step);
|
|
} else {
|
|
body.acceleration.set(0, 0);
|
|
// Inertial drift, decaying smoothly.
|
|
if (body.velocity.length() > 0.5) {
|
|
body.velocity.scale(Math.max(0, 1 - this.drag * dt));
|
|
}
|
|
}
|
|
}
|
|
}
|