orbit/js/entities/Planet.js

168 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 the sheet
* frames that kind may be drawn as — the generator picks one per planet).
*
* 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';
/**
* Pick a sheet frame for a planet of `name` from its pool in
* data/planets.json (`frames`), using the given Rng so the choice is
* seed-deterministic (same seed ⇒ same world). A pool that is just a
* single number (or a missing/empty pool) falls back to that number,
* else 0.
*/
static frameFor(name, rng) {
const pool = config.get(`planets.frames.${name}`);
if (Array.isArray(pool) && pool.length > 0) return rng.pick(pool);
return Number.isFinite(pool) ? pool : 0;
}
/**
* @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 (usually from Planet.frameFor)
* @param {string} [name=''] — planet kind, e.g. 'terran' (data/planets.json → frames)
* @param {object} [o={}] — per-planet overrides
* @param {number} [o.scale=1] — extra size multiplier on top of planets.scale
* (gas giants run bigger; see data/planets.json → classScale)
* @param {number} [o.tint] — canvas tint (int) applied to the sheet frame,
* so non-terran kinds read differently (see data/planets.json → classTint)
*/
constructor(scene, x, y, frame = 0, name = '', o = {}) {
super(scene, x, y, Planet.TEXTURE_KEY, frame);
scene.add.existing(this);
this.name = name;
this.sheetFrame = frame; // raw planets.png frame — picks the landing/surface videos
const scale = config.get('planets.scale', 1) * (o.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);
if (o.tint !== undefined && o.tint !== null) this.setTint(o.tint);
}
/** 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).
*
* @returns {boolean} CONTACT: true when the constraint actually did
* something to the ship this frame (pushed it out of the keep-out circle,
* or stripped inward velocity/acceleration). The scene uses this to stop
* a thrusting ship (the Shift+click throttle lock) the moment it runs
* into the planet.
*/
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,
);
const touched =
r.x !== ship.x || r.y !== ship.y ||
r.vx !== body.velocity.x || r.vy !== body.velocity.y ||
(body.acceleration &&
(r.ax !== body.acceleration.x || r.ay !== body.acceleration.y));
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;
}
return touched;
}
/**
* 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 };
}
}