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); this.setCollideWorldBounds(true); // 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); body.velocity.set(0, 0); return; } const nx = dx / dist; const ny = dy / dist; // Ease off the throttle as we close in, so we arrive gently. const throttle = Phaser.Math.Clamp(dist / this.brakeDistance, 0.15, 1); 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); } // Rotate toward the direction of travel (shortest path). const current = Phaser.Math.Angle.Wrap(this.rotation); const wanted = Phaser.Math.Angle.Wrap(Math.atan2(ny, nx)); 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)); } } } }