import Phaser from '../vendor/phaser.js'; import { config } from '../config/Config.js'; /** * A planet: a static world rendered from the shared spritesheet * (data/planets.json → texture; `frameWidth`×`frameHeight` frames, frame 0 * is the top-left, and `frames` maps names like "terran" to frame indices). * * A planet is a SOLID DISC. Its collision radius is half the scaled frame * (the world fills its frame — a Terran world is 1024 px across at * scale 1.0), and the ship is kept `shipClearance` px (edge-to-edge) off * the rim: it can come that close, but it can never move through the * planet or any closer. * * The keep-out rule is a plain circle test (constrainShip → static * resolve) applied to the ship after it moves, so it never interferes * with the ship's own flight model and stays testable in Node * (dev/planet.test.mjs) without a scene. */ export class Planet extends Phaser.GameObjects.Sprite { static TEXTURE_KEY = 'planets'; /** * @param {Phaser.Scene} scene * @param {number} x — world x of the planet's center * @param {number} y — world y * @param {number} [frame=0] — spritesheet frame index (data/planets.json → frames) */ constructor(scene, x, y, frame = 0) { super(scene, x, y, Planet.TEXTURE_KEY, frame); scene.add.existing(this); const scale = config.get('planets.scale', 1); this.setScale(scale); // Collision circle: frames are square and the world fills its frame, // so the rim is half the (scaled) frame width from the center. this.radius = (config.get('planets.frameWidth', 1024) * scale) / 2; // Edge-to-edge gap the ship may close in on the rim (never less). this.clearance = config.get('planets.shipClearance', 50); } /** Minimum allowed center-to-center distance for a ship of `shipRadius`. */ minCenterDistance(shipRadius = 0) { return this.radius + this.clearance + shipRadius; } /** * A world point `gap` px (edge-to-edge) off this planet's rim at * `angle` radians — e.g. where to spawn the ship near the home world. */ edgePoint(angle, gap, shipRadius = 0) { const d = this.radius + gap + shipRadius; return { x: this.x + Math.cos(angle) * d, y: this.y + Math.sin(angle) * d }; } /** * A world point the ship may be sent to: a point inside the keep-out * circle (e.g. a click on the planet itself, or through it) is projected * out onto the rim, along the ray from the center — so the ship always * has a reachable destination and is never told to go inside. Points * already outside pass through unchanged. */ aimPoint(wx, wy, shipRadius = 0) { const minDist = this.minCenterDistance(shipRadius); const dx = wx - this.x; const dy = wy - this.y; const dist = Math.hypot(dx, dy); if (dist >= minDist) return { x: wx, y: wy }; if (dist === 0) return { x: this.x + minDist, y: this.y }; // dead center: +x return { x: this.x + (dx / dist) * minDist, y: this.y + (dy / dist) * minDist }; } /** * Keep a ship (anything with x, y and body.velocity) out of the planet: * if it is inside the keep-out circle its center is moved out to * `minCenterDistance` and the inward part of its velocity AND * acceleration is removed — the tangential part is kept, so a near-miss * slides along the rim instead of sticking. Stripping the acceleration * matters: the arcade world integrates it AFTER this runs, so without * it the ship would be pushed back inside a fraction of a pixel on the * very next physics step. A ship already outside is untouched; one * riding exactly on the circle keeps its position but loses its inward * speed, so contact is clean (no in/out jitter). */ constrainShip(ship, shipRadius = 0) { const minDist = this.minCenterDistance(shipRadius); const body = ship.body; const r = Planet.resolve( this.x, this.y, minDist, ship.x, ship.y, body.velocity.x, body.velocity.y, body.acceleration ? body.acceleration.x : 0, body.acceleration ? body.acceleration.y : 0, ); ship.x = r.x; ship.y = r.y; body.velocity.x = r.vx; body.velocity.y = r.vy; if (body.acceleration) { body.acceleration.x = r.ax; body.acceleration.y = r.ay; } } /** * The pure circle constraint (static so it can be tested without a * scene): clamps a point at least `minDist` from (cx, cy) and removes * the components of velocity and acceleration pointing into the circle. */ static resolve(cx, cy, minDist, x, y, vx, vy, ax = 0, ay = 0) { const dx = x - cx; const dy = y - cy; const dist = Math.hypot(dx, dy); if (dist > minDist) return { x, y, vx, vy, ax, ay }; let nx; let ny; if (dist === 0) { nx = 1; ny = 0; } // dead center: push out along +x else { nx = dx / dist; ny = dy / dist; } const ox = cx + nx * minDist; const oy = cy + ny * minDist; // Strip any component pointing into the circle (keep the tangential). const strip = (v) => { const vn = v[0] * nx + v[1] * ny; return vn < 0 ? [v[0] - vn * nx, v[1] - vn * ny] : [v[0], v[1]]; }; const [rvx, rvy] = strip([vx, vy]); const [rax, ray] = strip([ax, ay]); return { x: ox, y: oy, vx: rvx, vy: rvy, ax: rax, ay: ray }; } }