feat: add flowing route previews and fix orrery depth sorting
Replace the static destination ring with a flowing dashed line that indicates the fleet's path and direction of travel. Introduce setSelectedFleet to highlight specific fleet markers and track live routes for fleets already underway. Fix depth sorting in the system view (orrery) by grouping bodies and sorting by Y position on every frame.
This commit is contained in:
parent
b0efbe3a44
commit
c42851f09c
|
|
@ -479,7 +479,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
onViewSystem: (idx) => this.openSystemViewFor(idx),
|
||||
onSelectFleet: (fleet) => this.onFleetClick(fleet),
|
||||
onAcceptOrder: (fleet, toStar, ships) => this.confirmOrder(fleet, toStar, ships),
|
||||
onCancelOrder: () => this.panel.showFleet(this.selectedFleet),
|
||||
onCancelOrder: () => { this.map?.clearRoutePreview(); this.panel.showFleet(this.selectedFleet); },
|
||||
onSelectionLost: () => { this.selectedFleet = null; this.map?.setSelectedStar(-1); },
|
||||
onClose: () => this.clearSelection(),
|
||||
});
|
||||
|
|
@ -579,18 +579,21 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
// something to look at.
|
||||
onStarClick(idx) {
|
||||
if (this.modalOpen || this.busy) return;
|
||||
this.map.setSelectedStar(idx);
|
||||
|
||||
// Clicking the system the fleet is already sitting in is not a move order —
|
||||
// it is a request to look at the place, so the selection is dropped and the
|
||||
// star's own panel comes up (which lists the fleet, one click from getting
|
||||
// it back).
|
||||
// it back). Clicking anywhere else with a fleet in hand quotes a route: the
|
||||
// fleet keeps its own ring and a flowing dashed line marks the course
|
||||
// instead of ringing the destination star.
|
||||
if (this.selectedFleet && this.state.fleets.includes(this.selectedFleet)
|
||||
&& this.selectedFleet.starIdx >= 0 && this.selectedFleet.starIdx !== idx) {
|
||||
this.map.setRoutePreview(this.selectedFleet.starIdx, idx);
|
||||
this.panel.showOrder(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
this.map.setSelectedStar(idx);
|
||||
this.selectedFleet = null;
|
||||
this.panel.showStar(idx);
|
||||
}
|
||||
|
|
@ -605,7 +608,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
this.selectedFleet = fleet;
|
||||
this.map.setSelectedStar(fleet.starIdx);
|
||||
this.map.setSelectedFleet(fleet);
|
||||
this.panel.showFleet(fleet);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export default class VegaStarMap {
|
|||
this.zoomIndex = Math.min(DEFAULT_ZOOM_INDEX, this.zooms.length - 1);
|
||||
this.zoom = this.zooms[this.zoomIndex];
|
||||
this.selectedStar = -1;
|
||||
this.selectedFleet = null;
|
||||
this.routePreview = null;
|
||||
this.hoverStar = -1;
|
||||
this.rangeDirty = true;
|
||||
this.territoryDirty = true;
|
||||
|
|
@ -105,6 +107,8 @@ export default class VegaStarMap {
|
|||
// so a fleet parked over its own star is never hidden by its own highlight.
|
||||
this.selGfx = scene.add.graphics();
|
||||
this.root.add(this.selGfx);
|
||||
this.routeGfx = scene.add.graphics();
|
||||
this.root.add(this.routeGfx);
|
||||
this.fleetLayer = scene.add.container(0, 0);
|
||||
this.root.add(this.fleetLayer);
|
||||
this.labelLayer = scene.add.container(0, 0);
|
||||
|
|
@ -435,23 +439,159 @@ export default class VegaStarMap {
|
|||
/** Ring the system the command panel is currently talking about. -1 clears. */
|
||||
setSelectedStar(idx) {
|
||||
this.selectedStar = idx ?? -1;
|
||||
this.selectedFleet = null;
|
||||
this.clearRoutePreview();
|
||||
this.drawSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ring a specific fleet marker instead of the star it happens to be at.
|
||||
* A fleet that is already underway (or has a standing order) gets the same
|
||||
* flowing route drawn to its live destination — tracked by reference so it
|
||||
* keeps pace with the fleet as it moves, not a snapshot of where it was.
|
||||
*/
|
||||
setSelectedFleet(fleet) {
|
||||
this.selectedFleet = fleet ?? null;
|
||||
this.selectedStar = -1;
|
||||
if (fleet && fleet.toStar >= 0) {
|
||||
this.routePreview = { fleet };
|
||||
} else {
|
||||
this.routePreview = null;
|
||||
}
|
||||
this.routeGfx.clear();
|
||||
this.drawSelection();
|
||||
if (this.routePreview) this.drawRoutePreview();
|
||||
}
|
||||
|
||||
/** Flowing dashes from `fromIdx` to `toIdx` — the route a pending order will fly. */
|
||||
setRoutePreview(fromIdx, toIdx) {
|
||||
if (fromIdx == null || toIdx == null || fromIdx < 0 || toIdx < 0) {
|
||||
this.clearRoutePreview();
|
||||
return;
|
||||
}
|
||||
this.routePreview = { from: fromIdx, to: toIdx };
|
||||
this.drawRoutePreview();
|
||||
}
|
||||
|
||||
clearRoutePreview() {
|
||||
this.routePreview = null;
|
||||
this.routeGfx.clear();
|
||||
}
|
||||
|
||||
/** Resolve the preview's start/end points — either a fixed star pair (a
|
||||
* pending click-to-order) or a live fleet reference (an underway fleet
|
||||
* tracked to its destination as it flies). */
|
||||
resolveRouteEndpoints() {
|
||||
const rp = this.routePreview;
|
||||
if (!rp) return null;
|
||||
if (rp.fleet) {
|
||||
const f = rp.fleet;
|
||||
if (!this.state.fleets.includes(f) || f.toStar < 0) return null;
|
||||
const dest = this.state.galaxy.stars[f.toStar];
|
||||
if (!dest) return null;
|
||||
let ax;
|
||||
let ay;
|
||||
if (f.starIdx >= 0) {
|
||||
const star = this.state.galaxy.stars[f.starIdx];
|
||||
if (!star) return null;
|
||||
// Match the fleet marker's own offset from the star it sits at.
|
||||
ax = star.x + 26;
|
||||
ay = star.y - 22;
|
||||
} else {
|
||||
const from = this.state.galaxy.stars[f.fromStar];
|
||||
if (!from) return null;
|
||||
const t = f.total > 0 ? Phaser.Math.Clamp(f.progress / f.total, 0, 1) : 0;
|
||||
ax = from.x + (dest.x - from.x) * t;
|
||||
ay = from.y + (dest.y - from.y) * t;
|
||||
}
|
||||
return {
|
||||
ax, ay, bx: dest.x, by: dest.y,
|
||||
};
|
||||
}
|
||||
const from = this.state.galaxy.stars[rp.from];
|
||||
const to = this.state.galaxy.stars[rp.to];
|
||||
if (!from || !to) return null;
|
||||
return {
|
||||
ax: from.x, ay: from.y, bx: to.x, by: to.y,
|
||||
};
|
||||
}
|
||||
|
||||
drawRoutePreview() {
|
||||
const g = this.routeGfx;
|
||||
g.clear();
|
||||
const pts = this.resolveRouteEndpoints();
|
||||
if (!pts) return;
|
||||
const a = { x: pts.ax, y: pts.ay };
|
||||
const b = { x: pts.bx, y: pts.by };
|
||||
const dx = b.x - a.x;
|
||||
const dy = b.y - a.y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
if (len < 1) return;
|
||||
const ux = dx / len;
|
||||
const uy = dy / len;
|
||||
|
||||
// Marching dashes: a fixed dash/gap pattern whose phase slides toward the
|
||||
// destination, reading as a current flowing along the route rather than a
|
||||
// static line — the "ship will fly this way" cue the ring around the
|
||||
// target star used to give less directly.
|
||||
const dash = 22;
|
||||
const gap = 16;
|
||||
const period = dash + gap;
|
||||
const speed = 90;
|
||||
// d advances with time (not -phase) so the dashes drift from `a` toward
|
||||
// `b` — matching the ship's actual direction of travel, not the reverse.
|
||||
const phase = ((this.time / 1000) * speed) % period;
|
||||
g.lineStyle(3, 0x9fd8ff, 0.85);
|
||||
for (let d = phase - period; d < len; d += period) {
|
||||
const s = Math.max(d, 0);
|
||||
const e = Math.min(d + dash, len);
|
||||
if (e <= s) continue;
|
||||
g.beginPath();
|
||||
g.moveTo(a.x + ux * s, a.y + uy * s);
|
||||
g.lineTo(a.x + ux * e, a.y + uy * e);
|
||||
g.strokePath();
|
||||
}
|
||||
|
||||
// Arrowhead planted on the destination star, pointing along the route.
|
||||
const ang = Math.atan2(dy, dx);
|
||||
const ah = 11;
|
||||
const tip = { x: b.x - ux * 6, y: b.y - uy * 6 };
|
||||
g.fillStyle(0x9fd8ff, 0.9);
|
||||
g.beginPath();
|
||||
g.moveTo(tip.x, tip.y);
|
||||
g.lineTo(tip.x - Math.cos(ang - 2.6) * ah, tip.y - Math.sin(ang - 2.6) * ah);
|
||||
g.lineTo(tip.x - Math.cos(ang + 2.6) * ah, tip.y - Math.sin(ang + 2.6) * ah);
|
||||
g.closePath();
|
||||
g.fillPath();
|
||||
}
|
||||
|
||||
drawSelection() {
|
||||
const g = this.selGfx;
|
||||
g.clear();
|
||||
const s = this.starSprites?.[this.selectedStar];
|
||||
if (!s) return;
|
||||
let cx;
|
||||
let cy;
|
||||
let r;
|
||||
if (this.selectedFleet) {
|
||||
const marker = this.fleetMarkers?.find((m) => m.fleet === this.selectedFleet);
|
||||
if (!marker) return;
|
||||
cx = marker.container.x;
|
||||
cy = marker.container.y;
|
||||
r = 20;
|
||||
} else {
|
||||
const s = this.starSprites?.[this.selectedStar];
|
||||
if (!s) return;
|
||||
cx = s.star.x;
|
||||
cy = s.star.y;
|
||||
r = s.cls.radius * 2.2 + 20;
|
||||
}
|
||||
// Four arcs rather than a circle: a broken reticle stays legible on top of
|
||||
// an ownership ring, which a second full circle would not.
|
||||
const r = s.cls.radius * 2.2 + 20;
|
||||
const spin = (this.time / 1000) * 0.5;
|
||||
g.lineStyle(2, 0x9fd8ff, 0.95);
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const a = spin + (i * Math.PI) / 2;
|
||||
g.beginPath();
|
||||
g.arc(s.star.x, s.star.y, r, a, a + 0.7);
|
||||
g.arc(cx, cy, r, a, a + 0.7);
|
||||
g.strokePath();
|
||||
}
|
||||
}
|
||||
|
|
@ -462,7 +602,8 @@ export default class VegaStarMap {
|
|||
this.time += delta;
|
||||
const t = this.time / 1000;
|
||||
|
||||
if (this.selectedStar >= 0) this.drawSelection();
|
||||
if (this.selectedStar >= 0 || this.selectedFleet) this.drawSelection();
|
||||
if (this.routePreview) this.drawRoutePreview();
|
||||
|
||||
for (const s of this.starSprites) {
|
||||
if (s.cls.special === 'pulsar') {
|
||||
|
|
|
|||
|
|
@ -30,10 +30,21 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
const orreryCY = shell.body.y + 380;
|
||||
|
||||
// --- star at the centre of the orrery
|
||||
//
|
||||
// The star and every planet share one sub-container so they can be
|
||||
// depth-sorted by screen Y each tick — a Phaser Container renders its
|
||||
// children in LIST order and ignores .depth entirely, so without an
|
||||
// explicit sort a planet lower on screen (nearer the "camera" in this
|
||||
// pseudo-3D tilted view) could render behind one higher up, whichever
|
||||
// happened to be added first. orbitGfx sits fixed at y=0, below every body
|
||||
// and the star, so it never needs re-sorting itself.
|
||||
const orrery = scene.add.container(0, 0);
|
||||
shell.add(orrery);
|
||||
|
||||
const starImg = scene.add.image(orreryCX, orreryCY, art.stars, Math.max(0, starFrame(rules, star.classId)))
|
||||
.setDisplaySize(cls.radius * 9, cls.radius * 9)
|
||||
.setBlendMode(cls.special === 'blackhole' ? Phaser.BlendModes.NORMAL : Phaser.BlendModes.ADD);
|
||||
shell.add(starImg);
|
||||
orrery.add(starImg);
|
||||
|
||||
shell.add(scene.add.text(shell.body.x, shell.body.y, `${cls.name} star — ${cls.desc}`, {
|
||||
fontFamily: FONT, fontSize: '16px', color: '#7f97b3', wordWrap: { width: 700 },
|
||||
|
|
@ -42,7 +53,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
// --- orbits and planets
|
||||
const bodies = [];
|
||||
const orbitGfx = scene.add.graphics();
|
||||
shell.add(orbitGfx);
|
||||
orrery.add(orbitGfx);
|
||||
let selected = null;
|
||||
|
||||
star.planets.forEach((planet, i) => {
|
||||
|
|
@ -53,7 +64,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
34 + (rules.planetSizes[planet.sizeId]?.basePop ?? 40) * 0.18)
|
||||
.setInteractive({ useHandCursor: true });
|
||||
img.on('pointerup', () => select(i));
|
||||
shell.add(img);
|
||||
orrery.add(img);
|
||||
bodies.push({ planet, img, type, angle: planet.orbitAngle, radius: planet.orbitRadius * 1.35 });
|
||||
});
|
||||
|
||||
|
|
@ -254,6 +265,9 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
orreryCY + Math.sin(b.angle) * b.radius * 0.55,
|
||||
);
|
||||
}
|
||||
// Re-sort every tick: whichever body (or the star) sits lowest on
|
||||
// screen this frame draws last, i.e. on top.
|
||||
orrery.sort('y');
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue