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:
Ilia Denisov
2026-05-10 16:27:55 +02:00
parent f7109af55c
commit 3626998a33
36 changed files with 4033 additions and 89 deletions
+17
View File
@@ -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;