diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index 0888d06..dbc9017 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -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); } diff --git a/src/games/mastervega/VegaStarMap.js b/src/games/mastervega/VegaStarMap.js index 292a1e9..b82009d 100644 --- a/src/games/mastervega/VegaStarMap.js +++ b/src/games/mastervega/VegaStarMap.js @@ -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') { diff --git a/src/games/mastervega/VegaSystemView.js b/src/games/mastervega/VegaSystemView.js index b3a7f1f..15bb3ba 100644 --- a/src/games/mastervega/VegaSystemView.js +++ b/src/games/mastervega/VegaSystemView.js @@ -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'); }, });