ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single inline-form panel: split, send, load, unload, modernize, dismantle, transfer, join fleet. Each action either appends a typed command to the local order draft or surfaces a tooltip explaining the disabled state. Partial-ship operations emit an implicit breakShipGroup command before the targeted action so the engine sees a clean (Break, Action) pair on the wire. `pkg/calc.BlockUpgradeCost` migrates from `game/internal/controller/ship_group_upgrade.go` so the calc bridge can wrap a pure pkg/calc formula; the controller now imports it. The bridge surfaces the function as `core.blockUpgradeCost`, which the inspector calls once per ship block to render the modernize cost preview. `GameReport.otherRaces` is decoded from the report's player block (non-extinct, ≠ self) and feeds the transfer-to-race picker. The planet inspector's stationed-ship rows become clickable for own groups so the actions panel is reachable from the standard click flow (the renderer continues to hide on-planet groups). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -272,6 +272,18 @@ export interface GameReport {
|
||||
incomingShipGroups: ReportIncomingShipGroup[];
|
||||
unidentifiedShipGroups: ReportUnidentifiedShipGroup[];
|
||||
localFleets: ReportLocalFleet[];
|
||||
/**
|
||||
* otherRaces lists the names of every non-extinct race other than
|
||||
* the local player, sorted alphabetically. Drawn from the
|
||||
* `report.player[]` block in the FBS report (each `Player` row
|
||||
* carries an `extinct` flag). The ship-group inspector consumes
|
||||
* this list for the "transfer to race" picker; Phase 22's Races
|
||||
* View reuses the same field so the read shape is stable across
|
||||
* stages. Empty when the report has no `player` block (boot
|
||||
* state, history-mode snapshots) or when the local player is the
|
||||
* only non-extinct race.
|
||||
*/
|
||||
otherRaces: string[];
|
||||
}
|
||||
|
||||
export async function fetchGameReport(
|
||||
@@ -405,6 +417,7 @@ function decodeReport(report: Report): GameReport {
|
||||
const raceName = report.race() ?? "";
|
||||
const routes = decodeReportRoutes(report);
|
||||
const localTech = findLocalPlayerTech(report, raceName);
|
||||
const otherRaces = collectOtherRaces(report, raceName);
|
||||
const localShipGroups = decodeLocalShipGroups(report);
|
||||
const otherShipGroups = decodeOtherShipGroups(report);
|
||||
const incomingShipGroups = decodeIncomingShipGroups(report);
|
||||
@@ -429,6 +442,7 @@ function decodeReport(report: Report): GameReport {
|
||||
incomingShipGroups,
|
||||
unidentifiedShipGroups,
|
||||
localFleets,
|
||||
otherRaces,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -705,6 +719,27 @@ function findLocalPlayerTech(
|
||||
return { drive: 0, weapons: 0, shields: 0, cargo: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* collectOtherRaces walks the `report.player[]` block and returns
|
||||
* the alphabetically-sorted names of every non-extinct race other
|
||||
* than the local player. Used by `GameReport.otherRaces` to back the
|
||||
* ship-group inspector's transfer-to-race picker (Phase 20) and the
|
||||
* Races View list (Phase 22).
|
||||
*/
|
||||
function collectOtherRaces(report: Report, raceName: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < report.playerLength(); i++) {
|
||||
const player = report.player(i);
|
||||
if (player === null) continue;
|
||||
if (player.extinct()) continue;
|
||||
const name = player.name() ?? "";
|
||||
if (name === "" || name === raceName) continue;
|
||||
out.push(name);
|
||||
}
|
||||
out.sort((a, b) => a.localeCompare(b));
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* uuidToHiLo splits the canonical 36-character UUID string
|
||||
* (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) into the two big-endian
|
||||
|
||||
@@ -102,6 +102,7 @@ interface SyntheticPlayer {
|
||||
weapons: number;
|
||||
shields: number;
|
||||
cargo: number;
|
||||
extinct?: boolean;
|
||||
}
|
||||
|
||||
interface SyntheticShipGroup {
|
||||
@@ -269,9 +270,25 @@ function decodeSyntheticReport(json: unknown): GameReport {
|
||||
incomingShipGroups,
|
||||
unidentifiedShipGroups,
|
||||
localFleets,
|
||||
otherRaces: collectOtherRacesFromSynthetic(root, race),
|
||||
};
|
||||
}
|
||||
|
||||
function collectOtherRacesFromSynthetic(
|
||||
root: SyntheticReportRoot,
|
||||
raceName: string,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
for (const player of root.player ?? []) {
|
||||
if (player.extinct === true) continue;
|
||||
const name = typeof player.name === "string" ? player.name : "";
|
||||
if (name === "" || name === raceName) continue;
|
||||
out.push(name);
|
||||
}
|
||||
out.sort((a, b) => a.localeCompare(b));
|
||||
return out;
|
||||
}
|
||||
|
||||
function toShipGroupTech(raw: Record<string, number> | undefined): ShipGroupTech {
|
||||
const out: ShipGroupTech = { drive: 0, weapons: 0, shields: 0, cargo: 0 };
|
||||
if (raw === undefined || raw === null) return out;
|
||||
|
||||
Reference in New Issue
Block a user