From 52b9f2b29f3952d3c8892292fce4eb559b61176d Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 14 Aug 2026 12:00:26 -0600 Subject: [PATCH] Small Updates --- src/games/mastervega/MasterOfVegaGame.js | 32 ++++++++++++++---- src/games/mastervega/VegaLogic.js | 30 +++++++++++++++-- src/games/mastervega/VegaSidePanel.js | 42 +++++++++++++++++++----- tools/verifyMasterOfVega.js | 41 +++++++++++++++++++++++ 4 files changed, 128 insertions(+), 17 deletions(-) diff --git a/src/games/mastervega/MasterOfVegaGame.js b/src/games/mastervega/MasterOfVegaGame.js index 89caa3a..bdd5eea 100644 --- a/src/games/mastervega/MasterOfVegaGame.js +++ b/src/games/mastervega/MasterOfVegaGame.js @@ -706,11 +706,24 @@ export default class MasterOfVegaGame extends Phaser.Scene { hud.add(this.endTurnBtn); // --- status log, bottom left + // + // Bottom-anchored (origin 0,1) rather than pinned at a fixed top-left y: + // a top anchor meant a full 8-line log grew DOWN into the ☰ button below + // it, while a short log left the anchor point looking arbitrary. Anchoring + // the bottom edge just above the button (same "22 + gap" geometry as the + // ☰ button's own popover a few lines down) means the log grows upward + // instead and can never overlap it, at any line count. + const LOG_BOTTOM_Y = (GAME_HEIGHT - 40) - 22 - 16; this.logLines = []; - this.logText = this.add.text(24, GAME_HEIGHT - 168, '', { + this.logText = this.add.text(24, LOG_BOTTOM_Y, '', { fontFamily: FONT, fontSize: '16px', color: '#8fa8c0', lineSpacing: 3, - }); + }).setOrigin(0, 1); hud.add(this.logText); + // Worst-case top edge of the log once it grows to its own 8-line cap + // (log(), below) — refreshOfferNotifications() stacks its cards starting + // just above THIS, not the button, so a full log and a held offer can't + // collide either now that the log's top edge actually moves. + this.logTopY = LOG_BOTTOM_Y - 8 * 24; // Held peace/alliance/trade-agreement offers from an AI empire get an // actionable card (portrait + Seek Audience button) stacked just above @@ -948,7 +961,7 @@ export default class MasterOfVegaGame extends Phaser.Scene { && canNegotiate(this.rules, this.state, this.state.humanIndex, idx)); otherIdxs.forEach((otherIdx, row) => { const other = this.state.empires[otherIdx]; - const cy = GAME_HEIGHT - 168 - 16 - row * (H + GAP) - H / 2; + const cy = this.logTopY - 16 - row * (H + GAP) - H / 2; const card = this.add.container(X + W / 2, cy); card.add(this.add.rectangle(0, 0, W, H, 0x0b1220, 0.92) .setStrokeStyle(1.5, Phaser.Display.Color.HexStringToColor(other.color).color, 0.6)); @@ -1056,10 +1069,17 @@ export default class MasterOfVegaGame extends Phaser.Scene { // 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) { + const sf = this.selectedFleet; + const docked = sf && sf.starIdx >= 0 && sf.starIdx !== idx; + // Hyperspace Communications' redirectInFlight effect (Brian's ask, + // 2026-08-14) opens the same "quote a route" flow for a fleet already + // under way — anchored on its CURRENT destination, the one real star + // still available mid-flight (VegaLogic.js's sendFleet does the same). + const redirectable = sf && sf.starIdx < 0 && sf.toStar >= 0 && sf.toStar !== idx + && Logic.empireComponents(this.rules, this.state, sf.empireIdx).redirectInFlight; + if (sf && this.state.fleets.includes(sf) && (docked || redirectable)) { playSound(this, SFX.VEGA_VIEW); - this.map.setRoutePreview(this.selectedFleet.starIdx, idx); + this.map.setRoutePreview(docked ? sf.starIdx : sf.toStar, idx); this.panel.showOrder(idx); return; } diff --git a/src/games/mastervega/VegaLogic.js b/src/games/mastervega/VegaLogic.js index 3051165..476de4b 100644 --- a/src/games/mastervega/VegaLogic.js +++ b/src/games/mastervega/VegaLogic.js @@ -1159,8 +1159,18 @@ function recomputeTotals(rules, state) { // Movement, arrival, combat export function canSendFleet(rules, state, fleet, toStar) { - if (fleet.toStar >= 0) return false; - if (fleet.starIdx === toStar) return false; + // A DOCKED fleet just needs a real destination different from where it + // sits. An IN-FLIGHT fleet additionally needs Hyperspace Communications' + // redirectInFlight effect — without it, an order already under way is + // locked in, same as always — and a destination other than the one it is + // already headed for (retargeting to that is a no-op, same as ordering a + // docked fleet to stay where it is). + if (fleet.toStar >= 0) { + if (!empireComponents(rules, state, fleet.empireIdx).redirectInFlight) return false; + if (toStar === fleet.toStar) return false; + } else if (fleet.starIdx === toStar) { + return false; + } if (fleetSpeed(rules, state, fleet) <= 0) return false; const reach = reachableStars(rules, state, fleet.empireIdx); return !!reach[toStar]; @@ -1168,6 +1178,22 @@ export function canSendFleet(rules, state, fleet, toStar) { export function sendFleet(rules, state, fleet, toStar) { if (!canSendFleet(rules, state, fleet, toStar)) return false; + if (fleet.toStar >= 0) { + // Redirecting a fleet already under way (Hyperspace Communications, + // gated in canSendFleet above): no garrison to split off — an immobile + // hull can never have left port in the first place — so this is just a + // fresh leg from its ORIGINAL destination, the one real star anchor + // still available mid-flight. Matches the same fromStar-becomes-toStar + // convention etaTo() already uses to estimate an in-flight fleet's ETA + // to some OTHER star, so a quoted order's preview (VegaSidePanel.js's + // buildOrder, which calls etaTo()) lines up with what actually happens + // here on Accept. + fleet.fromStar = fleet.toStar; + fleet.toStar = toStar; + fleet.total = parsecs(state.galaxy, fleet.fromStar, toStar); + fleet.progress = 0; + return true; + } // Star bases defend the system they were built in and do not sail with the // fleet; split them into a garrison that stays put. const garrison = []; diff --git a/src/games/mastervega/VegaSidePanel.js b/src/games/mastervega/VegaSidePanel.js index d29beb7..b68094d 100644 --- a/src/games/mastervega/VegaSidePanel.js +++ b/src/games/mastervega/VegaSidePanel.js @@ -29,7 +29,7 @@ import { coloniesAt, fleetsAt, empireDesign, fleetPower, fleetSpeed, fleetEta, etaTo, colonyMaxPop, colonyProduction, colonyFactoryCap, effectiveFactories, colonyDefenseCap, habitableForEmpire, reachableStars, atWar, queueItemEta, - fleetInScanRange, + fleetInScanRange, empireComponents, } from './VegaLogic.js'; import { FONT, D, ORBIT, uiClick } from './VegaScreens.js'; import { playSound, SFX } from '../../ui/Sounds.js'; @@ -207,9 +207,15 @@ export default class VegaSidePanel { this.hide(); return; } - // An order is quoted from where the fleet stands; if it is no longer - // standing anywhere, there is nothing left to confirm. - if (this.mode === 'order' && this.fleet.starIdx < 0) this.mode = 'fleet'; + // An order is quoted from where the fleet stands — or, with Hyperspace + // Communications, from wherever it currently is mid-flight. Only bail + // back to the fleet view if neither is true any more (docked fleets are + // always fine; an in-flight one needs the tech that let this quote + // happen at all — see onStarClick's matching gate). + if (this.mode === 'order' && this.fleet.starIdx < 0 + && !empireComponents(this.rules, this.state, this.fleet.empireIdx).redirectInFlight) { + this.mode = 'fleet'; + } if (this.mode !== 'star') this.syncSelection(); this.rebuild(); } @@ -611,7 +617,15 @@ export default class VegaSidePanel { this.leaderRow(); this.shipList(); this.y += 8; - this.callout('A fleet in transit cannot be redirected until it arrives.', '#e0b08a'); + // Hyperspace Communications' redirectInFlight effect (Brian's ask, + // 2026-08-14): without it, an order already under way is locked in. + const canRedirect = empireComponents(rules, state, fleet.empireIdx).redirectInFlight; + this.callout( + canRedirect + ? 'Hyperspace Communications online — click a new destination star to redirect this fleet mid-flight.' + : 'A fleet in transit cannot be redirected until it arrives.', + canRedirect ? '#6fc4ff' : '#e0b08a', + ); return; } @@ -823,7 +837,13 @@ export default class VegaSidePanel { const { rules, state } = this; const fleet = this.fleet; const dest = state.galaxy.stars[this.orderStar]; - const from = state.galaxy.stars[fleet.starIdx]; + // A docked fleet quotes from where it stands. An in-flight one (only + // reachable here at all with Hyperspace Communications) quotes from its + // CURRENT destination — the one real star anchor still available + // mid-flight, matching etaTo()'s own convention and what sendFleet() + // actually does to it on Accept (VegaLogic.js). + const originIdx = fleet.starIdx >= 0 ? fleet.starIdx : fleet.toStar; + const from = state.galaxy.stars[originIdx]; const viewer = this.viewer; const explored = !viewer || viewer.explored[this.orderStar]; @@ -833,9 +853,9 @@ export default class VegaSidePanel { const ships = this.selectedShips(); const n = ships.reduce((t, s) => t + s.count, 0); - const distance = parsecs(state.galaxy, fleet.starIdx, this.orderStar); + const distance = parsecs(state.galaxy, originIdx, this.orderStar); const inRange = !!reachableStars(rules, state, fleet.empireIdx)[this.orderStar]; - const sameStar = fleet.starIdx === this.orderStar; + const sameStar = originIdx === this.orderStar; const eta = n ? etaTo(rules, state, fleet, this.orderStar, ships) : Infinity; this.line(`${distance.toFixed(1)} parsecs`, { size: 14, color: '#8fa8c0' }); @@ -879,7 +899,11 @@ export default class VegaSidePanel { } let refusal = null; - if (sameStar) refusal = 'The fleet is already in this system.'; + if (sameStar) { + refusal = fleet.starIdx >= 0 + ? 'The fleet is already in this system.' + : 'The fleet is already headed there.'; + } else if (!n) refusal = 'Select at least one ship to send.'; else if (!inRange) refusal = 'Beyond fuel range. Research propulsion, or plant a colony closer.'; else if (!Number.isFinite(eta)) refusal = 'Nothing in this task force can move.'; diff --git a/tools/verifyMasterOfVega.js b/tools/verifyMasterOfVega.js index fdd24db..03b7428 100644 --- a/tools/verifyMasterOfVega.js +++ b/tools/verifyMasterOfVega.js @@ -1236,6 +1236,47 @@ section('4b. Fleet orders and detachments'); check('idle detachments merge back at the start of the turn', Logic.fleetsAt(st, home).filter((f) => f.empireIdx === 0).length === 1); } + + { + // Hyperspace Communications' redirectInFlight effect (Brian's ask, + // 2026-08-14): the "splitFleet on its own" block above already proved a + // fleet under way is locked in without the tech. With it, canSendFleet/ + // sendFleet open up a genuine mid-flight redirect instead. + const st = mk(); + const home = st.galaxy.homeIdx[0]; + const fleet = Logic.addFleet(RULES, st, 0, home, [{ hullId: 'frigate', mark: 1, count: 3 }]); + const before = countShips(st, 0); + const reach = Logic.reachableStars(RULES, st, 0); + const targets = Object.keys(reach).map(Number).filter((i) => i !== home); + check('at least two reachable stars exist for the redirect test', targets.length >= 2); + if (targets.length >= 2) { + const [firstDest, secondDest] = targets; + Logic.sendFleet(RULES, st, fleet, firstDest); + check('a fleet under way without the tech still cannot be redirected', + !Logic.canSendFleet(RULES, st, fleet, secondDest)); + + Logic.grantTech(RULES, st, 0, 'hyperspacecomms'); + check('redirecting to the SAME destination is still refused even with the tech', + !Logic.canSendFleet(RULES, st, fleet, firstDest)); + check('redirecting to a DIFFERENT reachable star is now allowed', + Logic.canSendFleet(RULES, st, fleet, secondDest)); + + const oldToStar = fleet.toStar; + const ok = Logic.sendFleet(RULES, st, fleet, secondDest); + check('the redirect succeeds', ok); + check('the fleet is still in flight, never touching down mid-redirect', fleet.starIdx < 0); + check('the fleet now heads to the new destination', fleet.toStar === secondDest); + check("the fleet's new leg is anchored on its OLD destination", fleet.fromStar === oldToStar); + check('progress resets for the new leg', fleet.progress === 0); + check('total distance matches the new leg exactly', + Math.abs(fleet.total - parsecs(st.galaxy, oldToStar, secondDest)) < 1e-9); + check('a redirect conserves every ship', countShips(st, 0) === before); + + const eta = Logic.fleetEta(RULES, st, fleet); + check('post-redirect ETA is a positive whole number of turns', + Number.isInteger(eta) && eta >= 1, `${eta}`); + } + } } // ---------------------------------------------------------------------------