/** * RoutePulse — the route-target "target" halo: the in-world * "your destination is HERE" mark (the route's orange). * * Shape: a steady target ring at `baseRadius` × the object's keepout * radius, plus two radar ripples (staggered half a period) that expand * a further ×`expand` past it and fade out. All the geometry scales with * the object's keepout radius, so it reads the same on a small station * and a gas giant. * * Who wears it (GameScene.update marks exactly one object per frame — * the route's NEXT STOP): * · the ROUTE GATE (the destination's next stop in an intermediate * system — GameScene.routeNextGate) — the FULL pulse * (data/gates.json → route.pulse), via JumpGate.setRouteTarget * · the DESTINATION OBJECT in the destination system (a planet or a * space station — the route's final stop — * GameScene._routeDestinationEntity) — the SUBDUED pulse * (data/gates.json → route.objectPulse), via * Planet.setRouteTarget / Station.setRouteTarget * The compass's orange arrow is the off-screen half; the pulse is what * the player sees the moment the target comes on screen. * * Not a scene object of its own — the host entity places `pulse.ring` * (a scene Graphics) where it belongs: as a child at (0, 0) for * container hosts (gate / station), or beside the object for a Sprite * host (planet — static, so no following is needed), and drives * `pulse.draw(t)` from its own update() (the scene's update loop drives * the entity, as with the gate's swirl or the station's beacon). */ export class RoutePulse { /** * @param {Phaser.Scene} scene * @param {object} o { * size — the object's keepout radius (px) — the geometry's scale, * cfg — { baseRadius, expand, period, ringAlpha, rippleAlpha } * (a data/gates.json section — route.pulse / route.objectPulse), * color — the color int (the route's orange — route.compassColor), * x, y — the ring's position (0, 0 container-local — or the * object's center for a Sprite host), * depth — the ring's depth (the host's world-object depth), * } */ constructor(scene, o) { this.size = o.size; this.cfg = o.cfg ?? {}; this.color = o.color ?? 0xff8c1a; this.on = false; this.ring = scene.add.graphics().setPosition(o.x ?? 0, o.y ?? 0); if (o.depth !== undefined) this.ring.setDepth(o.depth); this.ring.visible = false; // hidden until claimed (set(true)) } /** * Show / hide the pulse (a no-op when unchanged). Hiding clears the * strokes so no stale frame lingers. */ set(on) { const want = on === true; if (this.on === want) return; this.on = want; if (!want) { this.ring.clear(); this.ring.visible = false; } else { this.ring.visible = true; } } /** Draw the pulse at time t (seconds). A no-op while hidden. */ draw(t) { if (!this.on) return; const g = this.ring; g.clear(); const S = this.size; const base = Math.max(2, S * 0.035); const R0 = S * (this.cfg.baseRadius ?? 1.18); const period = this.cfg.period ?? 1.6; const expand = this.cfg.expand ?? 0.55; const aRing = this.cfg.ringAlpha ?? 0.5; const aRipple = this.cfg.rippleAlpha ?? 0.85; // The steady ring — the circle the ripples grow out of. g.lineStyle(base, this.color, aRing); g.strokeCircle(0, 0, R0); // The ripples — two, half a period apart. for (let k = 0; k < 2; k++) { const phase = (t / period + k * 0.5) % 1; g.lineStyle(Math.max(1, base * (1 - 0.6 * phase)), this.color, aRipple * (1 - phase)); g.strokeCircle(0, 0, R0 * (1 + expand * phase)); } } /** Tear the ring down (the host is destroyed). */ destroy() { this.ring.destroy?.(); } }