Small Updates
This commit is contained in:
parent
cba84dc0b8
commit
52b9f2b29f
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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.';
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue