Small Updates

This commit is contained in:
Brian Fertig 2026-08-14 12:00:26 -06:00
parent cba84dc0b8
commit 52b9f2b29f
4 changed files with 128 additions and 17 deletions

View File

@ -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;
}

View File

@ -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 = [];

View File

@ -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.';

View File

@ -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}`);
}
}
}
// ---------------------------------------------------------------------------