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
@@ -77,9 +77,57 @@ Tests exercise the tab through `__galaxyDebug.seedOrderDraft`
return i18n.t("game.sidebar.order.label.ship_class_remove", {
name: cmd.name,
});
case "breakShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_break", {
group: shortGroupId(cmd.groupId),
quantity: String(cmd.quantity),
});
case "sendShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_send", {
group: shortGroupId(cmd.groupId),
destination: String(cmd.destinationPlanetNumber),
});
case "loadShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_load", {
group: shortGroupId(cmd.groupId),
cargo: cmd.cargo,
quantity: String(cmd.quantity),
});
case "unloadShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_unload", {
group: shortGroupId(cmd.groupId),
quantity: String(cmd.quantity),
});
case "upgradeShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_upgrade", {
group: shortGroupId(cmd.groupId),
tech: cmd.tech,
level: String(cmd.level),
});
case "dismantleShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_dismantle", {
group: shortGroupId(cmd.groupId),
});
case "transferShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_transfer", {
group: shortGroupId(cmd.groupId),
acceptor: cmd.acceptor,
});
case "joinFleetShipGroup":
return i18n.t("game.sidebar.order.label.ship_group_join_fleet", {
group: shortGroupId(cmd.groupId),
fleet: cmd.name,
});
}
}
// Short identifier for the order-tab so the human-readable label
// stays glanceable; the full UUID is still in the underlying
// command and visible in the inspector overlay.
function shortGroupId(uuid: string): string {
return uuid.length > 8 ? uuid.slice(0, 8) : uuid;
}
function statusOf(cmd: OrderCommand): CommandStatus {
return draft?.statuses[cmd.id] ?? "draft";
}