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
+111 -19
View File
@@ -2135,27 +2135,63 @@ Targeted tests:
- Playwright e2e: click each variant from a seeded game, assert all
expected fields render.
## Phase 20. Inspector — Ship Group Actions
## ~~Phase 20. Inspector — Ship Group Actions~~
Status: pending.
Status: done.
Goal: enable group operations from the inspector: split, send, load,
unload, modernize, dismantle, transfer to race, add to fleet.
Artifacts:
- action buttons in `ui/frontend/src/lib/inspectors/ship-group.svelte`
with disabled-state and tooltip when local validation rejects
- `ui/frontend/src/sync/order-types.ts` extends with `SplitGroup`,
`SendGroup`, `LoadCargo`, `UnloadCargo`, `Modernize`, `Dismantle`,
`TransferToRace`, `AssignToFleet` command variants
- `Send` action picks destination through a planet picker filtered by
the group's reach (uses `pkg/calc/` reach function via Core; the
player's tech levels are already on `GameReport.localPlayer*` from
Phase 18, no extra plumbing needed)
- `Modernize` cost preview using `pkg/calc/` formula via Core
- confirmation dialog for `Dismantle` over a foreign planet with
colonists onboard (special-case from [`rules.txt`](../game/rules.txt): colonists die)
- action panel `ui/frontend/src/lib/inspectors/ship-group/actions.svelte`
mounted by the read-only inspector for the local variant; eight
inline forms (one per action) with disabled-button tooltips that
mirror the engine's pre-conditions
(`controller/ship_group*.go`)
- `ui/frontend/src/sync/order-types.ts` extends with eight new
command variants — `breakShipGroup`, `sendShipGroup`,
`loadShipGroup`, `unloadShipGroup`, `upgradeShipGroup`,
`dismantleShipGroup`, `transferShipGroup`, `joinFleetShipGroup` —
plus `ShipGroupCargo` and `ShipGroupUpgradeTech` literal types
- `sync/submit.ts` and `sync/order-load.ts` round-trip every new
variant against the existing FBS classes in
`proto/galaxy/fbs/order/`; the `id` field on each ship-group
payload carries the *target* group UUID (the source group, or
the freshly-minted `newGroupId` when an implicit split precedes
the action)
- `Send` action picks destination through a planet picker filtered
by the group's reach (`localPlayerDrive * 40`, computed inline
via the existing `torusShortestDelta` from
`cargo-routes.svelte`); the player's tech levels are already on
`GameReport.localPlayer*` from Phase 18, no extra plumbing
needed
- `Modernize` cost preview through `core.blockUpgradeCost`
(Phase 20 bridge), summed over the four ship-class blocks for
the targeted ship count; preview hides when `Core` is not yet
booted or the form is invalid (see
`ui/docs/ship-group-actions.md` for the formula breakdown)
- two-step inline confirmation for `Dismantle` over a foreign
planet with colonists onboard (engine reference
`controller/ship_group.go:177-179` — `UnloadColonists` is not
called over a foreign planet, so the cargo is lost)
- `pkg/calc/ship.go.BlockUpgradeCost` (migrated from
`game/internal/controller/ship_group_upgrade.go`) — the bridge
rule says `ui/core/calc/` only wraps `pkg/calc/` formulas, so
the function moved upstream and the controller now imports it
- `GameReport.otherRaces: string[]` populated by the report
decoder from `report.player[]` (non-extinct, ≠ self) — used by
the transfer-to-race picker; Phase 22's Races View reuses the
same field
- planet inspector's stationed-ship rows
(`lib/inspectors/planet/ship-groups.svelte`) become clickable
for own groups, pivoting the `SelectionStore` to the matching
`shipGroup.local` ref so the actions panel is reachable from
the standard click flow (the map deliberately hides on-planet
groups, so this is the on-planet entry point)
- topic doc `ui/docs/ship-group-actions.md` covers the action
surface, disabled-state rules, implicit-split pattern, and the
modernize cost preview formula
Dependencies: Phases 18, 19.
@@ -2171,10 +2207,61 @@ Acceptance criteria:
Targeted tests:
- Vitest unit tests for action enablement logic per action;
- Vitest component tests for the dismantle-with-colonists confirmation;
- Playwright e2e for at least one complete flow (send a group between
two planets) against a local stack.
- `pkg/calc/ship_test.go.TestBlockUpgradeCost` — formula coverage
on the migrated function;
- `ui/core/calc/ship_test.go.TestBlockUpgradeCostParity` — bridge
parity against `pkg/calc/`;
- Vitest:
- `tests/inspector-ship-group-actions.test.ts` — disabled-state
rules per action and the implicit-split pattern;
- `tests/inspector-ship-group-dismantle-confirm.test.ts` —
two-step confirm over foreign-COL groups;
- `tests/inspector-ship-group-modernize-cost.test.ts` —
preview formula matches `BlockUpgradeCost` × ship count and
hides when `Core` is null;
- `tests/sync-order-types-ship-group.test.ts` —
`validateCommand` for each new variant;
- `tests/sync-submit-ship-group.test.ts` — encoder/decoder
round-trip per new variant;
- Playwright `tests/e2e/ship-group-send.spec.ts` — synthetic
report with a 3-ship group on Earth and a reachable Mars,
drives the planet inspector → ship-group inspector pivot, then
Send 2 of 3 with map-pick destination, asserts both Break and
Send land in the order draft via the order tab.
Decisions during stage:
1. **`BlockUpgradeCost` migration**. The pre-existing copy in
`game/internal/controller/ship_group_upgrade.go` moved to
`pkg/calc/ship.go`; the controller's `GroupUpgradeCost` and
`UpgradeGroupPreference` now call `calc.BlockUpgradeCost`.
The unit test moved from `controller/ship_group_upgrade_test.go`
to `pkg/calc/ship_test.go`.
2. **`GameReport.otherRaces`** field added to
`ui/frontend/src/api/game-state.ts`; the synthetic-report
decoder populates it the same way (`api/synthetic-report.ts`).
Phase 22's Races View can read this directly without a fresh
plumbing pass — the Phase 22 stage text below is updated to
reflect that.
3. **Stationed-ship rows are clickable**. The Phase 19 stationed-
ship subsection on the planet inspector becomes interactive
for own groups (Phase 21+ table view stays a separate target).
The map renderer continues to hide on-planet groups — this is
the cheaper navigational fix.
4. **Inline forms, no modal**. Every action opens an inline
editor under the buttons row, matching the Phase 14 rename and
Phase 16 cargo-route patterns. Send reuses
`MAP_PICK_CONTEXT_KEY` (Phase 16's renderer service) for the
destination picker. Foreign-COL Dismantle uses a two-step
inline confirm (button label flips to "confirm — colonists
die") rather than a separate modal component.
5. **Implicit split for Send/Load/Unload/Modernize/Dismantle/
Transfer**. The number-of-ships input defaults to the group's
full count; when the player picks a smaller M, the inspector
prepends `breakShipGroup(id, newId, M)` and routes the action
at `newId`. JoinFleet and Split do not get a counter (JoinFleet
is whole-group atomically per the engine; Split *is* the break
command).
## Phase 21. Sciences — CRUD List + Designer
@@ -2226,7 +2313,12 @@ Artifacts:
- `ui/frontend/src/routes/games/[id]/table/races/+page.svelte` table
with one row per race, including name, tech levels, total
population, total production, planet count, war-or-peace from this
race's perspective, votes received
race's perspective, votes received. The race list itself is read
from `GameReport.otherRaces` (introduced in Phase 20 for the
ship-group transfer-to-race picker); the table view widens the
per-race shape (tech / population / production / planet count /
votes / relation) by walking `report.player[]` directly when those
fields are needed
- per-row toggle for declaring war or peace (adds
`SetDiplomaticStance` command)
- voting control: a single slot for `give my votes to <race>` (adds