a89048f6c5
MVP web client (Phases 1-30) is complete; reorganize planning + living docs around that. - PLAN.md kept as the staged MVP record (1-30) with a status block + pointers; removed the 31-36 stages, regression scenarios, and deferred-TODO section (moved out); fixed a stale cross-machine plan path. - ui/PLAN-finalize.md (new): active web-finalization plan in 8 stages (visual system, a11y, i18n, error UX, PWA, build hygiene, docs, owner manual-QA loop); absorbs former Phases 33 and 35. - ui/ROADMAP.md (new): post-MVP (Wails, Capacitor, realistic projection, acceptance + regression scenarios) and triaged deferred follow-ups. - ui/docs/README.md (new): grouped topic-doc index. - De-archaeologized all 20 ui/docs topic docs + ui/README.md + ui/core/README.md: stripped Phase-N build history, rewritten as current-state; deferred work now points at ROADMAP.md / PLAN-finalize.md. Docs-only; no code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
9.8 KiB
Markdown
199 lines
9.8 KiB
Markdown
# Ship-group inspector actions
|
|
|
|
The ship-group inspector
|
|
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) is an
|
|
interactive command source for the player's own groups in orbit.
|
|
This document is the running spec for the actions panel
|
|
(`ui/frontend/src/lib/inspectors/ship-group/actions.svelte`):
|
|
which actions exist, what gates each one, how partial-ship
|
|
operations split a group on the fly, and what the modernize cost
|
|
preview shows.
|
|
|
|
## Reaching a group
|
|
|
|
The map renderer hides on-planet ship groups to avoid crowding
|
|
the canvas. The player reaches an own on-planet group through the
|
|
planet inspector's **stationed ship groups** subsection: clicking
|
|
a row pivots the `SelectionStore` to the matching
|
|
`shipGroup.local` ref, the sidebar swaps from the planet
|
|
inspector to the ship-group inspector, and the actions panel
|
|
mounts. In-flight (in-space) groups appear as map primitives and
|
|
can be selected by clicking the rendered point.
|
|
|
|
## Action surface
|
|
|
|
| Action | Implicit-split? | Partial input | FBS payload | Engine reference |
|
|
| ----------- | :-------------: | ------------- | ---------------------------- | ----------------------------------------------------- |
|
|
| Split | — | ships count | `CommandShipGroupBreak` | `controller/ship_group.go.breakGroup` |
|
|
| Send | yes | ships count + destination | `CommandShipGroupSend` | `controller/ship_group_send.go.shipGroupSend` |
|
|
| Load | yes | ships count + cargo + quantity | `CommandShipGroupLoad` | `controller/ship_group.go.shipGroupLoad` |
|
|
| Unload | yes | ships count + quantity | `CommandShipGroupUnload` | `controller/ship_group.go.shipGroupUnload` |
|
|
| Modernize | yes | ships count + tech + level | `CommandShipGroupUpgrade` | `controller/ship_group_upgrade.go.shipGroupUpgrade` |
|
|
| Dismantle | yes | ships count + foreign-COL confirm | `CommandShipGroupDismantle` | `controller/ship_group.go.shipGroupDismantle` |
|
|
| Transfer | yes | ships count + acceptor race | `CommandShipGroupTransfer` | `controller/ship_group.go.shipGroupTransfer` |
|
|
| Join Fleet | — | fleet name (existing or new) | `CommandShipGroupJoinFleet` | `controller/fleet.go.ShipGroupJoinFleet` |
|
|
|
|
"Implicit-split" means the inspector accepts a number of ships
|
|
`M < N` and emits a `CommandShipGroupBreak(id, newId, M)` command
|
|
*before* the action command, then targets the action at the
|
|
freshly-minted `newId`. The FBS schema only carries a per-ship
|
|
`quantity` on `CommandShipGroupBreak`; every other ship-group
|
|
command applies to the whole group, so the implicit-split
|
|
pattern is the only way to act on a subset without forcing the
|
|
player to pre-split manually. Acceptance criteria: "splitting a
|
|
group of N into K and N-K results in two valid commands" — that
|
|
is exactly the (Break, Action) pair this pipeline emits.
|
|
|
|
Split and Join Fleet do not accept a partial ship count: Split
|
|
*is* the break operation; Join Fleet attaches the whole group
|
|
atomically (the engine handles a partial detach by issuing Split
|
|
first, which the player drives explicitly).
|
|
|
|
## Disabled-state rules
|
|
|
|
The inspector mirrors the engine's pre-conditions per command
|
|
(see the references column above) and surfaces each as a
|
|
disabled-button tooltip. Any state other than `In_Orbit` disables
|
|
every action with `ships are busy ({state})`. Per-action gates:
|
|
|
|
- **Send**: requires the ship class to have a non-zero drive
|
|
block (`controller/ship_group_send.go:32`); the picker
|
|
pre-filters destinations by reach
|
|
(`localPlayerDrive * 40`), so a valid pick is always within
|
|
range. With no reachable planet, the action is disabled with
|
|
the "no planets in drive range" tooltip. Click drops the
|
|
inspector straight into map-pick mode; the form (ship count +
|
|
confirm) appears only after the player chooses a destination —
|
|
there is no destination control inside the form, so cancelling
|
|
the picker leaves the inspector untouched.
|
|
- **Load**: requires the orbit planet to be owned by the player
|
|
or unowned (`controller/ship_group.go:215`) and the ship class
|
|
to have a cargo block (`shipGroupLoad:220`). The dropdown is
|
|
pinned to the existing cargo type when the group is already
|
|
partially loaded (the engine refuses cargo-type changes at
|
|
`shipGroupLoad:223`).
|
|
- **Unload**: requires non-empty cargo. Colonists (`COL`) over a
|
|
foreign planet are blocked (`shipGroupUnload:283`), with the
|
|
matching tooltip in the disabled state.
|
|
- **Modernize**: requires the orbit planet to be own/unowned
|
|
(`shipGroupUpgrade:29`) and at least one block whose race tech
|
|
exceeds the group tech (otherwise nothing can be upgraded).
|
|
- **Dismantle**: always available in orbit. When the orbit is
|
|
over a foreign planet AND the group carries colonists, the
|
|
inline form replaces the normal "confirm" with "confirm —
|
|
colonists die"; the player has to click twice to commit
|
|
(engine reference `shipGroupDismantle:177-179` — over a
|
|
foreign planet, `UnloadColonists` is not called and the
|
|
cargo is lost).
|
|
- **Transfer**: requires at least one non-extinct race other
|
|
than the local player (sourced from
|
|
`GameReport.otherRaces`).
|
|
- **Join Fleet**: existing-fleet picker is restricted to fleets
|
|
in the same orbit (`fleet.go:135-137`); creating a new fleet
|
|
always works.
|
|
|
|
## State-changing-command lock
|
|
|
|
`Send`, `Modernize`, `Dismantle`, and `Transfer` are
|
|
*state-changing* at turn cutoff: the engine moves the group into
|
|
`StateLaunched`, `StateUpgrade`, removes it, or marks it
|
|
`StateTransfer` respectively. Issuing a follow-up action against
|
|
the same group during the same draft window would race the
|
|
engine's pre-condition check, so the inspector locks the group
|
|
as soon as one of the four commands lands in the draft for that
|
|
`groupId`:
|
|
|
|
- every action button on the group's inspector becomes disabled
|
|
with the "an order is already queued" tooltip;
|
|
- a banner above the buttons row names the queued command
|
|
(send / modernize / dismantle / transfer) and tells the player
|
|
to cancel it in the order list to issue something else;
|
|
- removing the queued entry from the order tab releases the lock
|
|
on the next render — the derivation watches `draft.commands`
|
|
directly.
|
|
|
|
Load, Unload, Split, and Join Fleet do not lock the group: those
|
|
four can stack legitimately during the same window. The group
|
|
continues to appear in the planet inspector's stationed-ship
|
|
list while locked — the player can still navigate to the
|
|
inspector to read the state and find the order to cancel.
|
|
|
|
## Map overlays for in-flight and pending-Send groups
|
|
|
|
Two dashed-line overlays run on the same renderer layer as the
|
|
cargo-route arrows:
|
|
|
|
- **Yellow dashed track** for own ship groups currently in
|
|
hyperspace, drawn from the origin planet to the destination
|
|
(matches the colour of the in-space group point so the eye
|
|
reads both as one entity).
|
|
- **Green dashed track** for every wire-valid `sendShipGroup`
|
|
command in the order draft — drawn from the source group's
|
|
orbit planet to the chosen destination. Disappears when the
|
|
command is removed from the order tab, when the engine flips
|
|
it `rejected` / `invalid`, or when the group has left orbit
|
|
(the Send was applied and the in-space track replaces it).
|
|
|
|
Both tracks are wrap-aware via `torusShortestDelta` so a route
|
|
across the seam takes the wrap. Neither participates in
|
|
hit-test — the player still picks ship groups by clicking the
|
|
group point, not the track.
|
|
|
|
## Modernize cost preview
|
|
|
|
The form's preview line calls
|
|
`core.blockUpgradeCost({ blockMass, currentTech, targetTech })`
|
|
once per ship block (drive, weapons, shields, cargo) and sums
|
|
the per-ship totals before multiplying by the targeted ship
|
|
count. Block masses come from the player's
|
|
`ShipClassSummary` for the group's class:
|
|
|
|
- Drive / shields / cargo block mass = the corresponding ship-
|
|
class field (raw value).
|
|
- Weapons block mass = `core.weaponsBlockMass({ weapons,
|
|
armament })`; returns null on the invalid weapons/armament
|
|
pairing, in which case the row contributes zero.
|
|
|
|
For `tech === "ALL"` every block whose mass is non-zero
|
|
contributes against the player's race tech as the target. For
|
|
per-block tech (`DRIVE` / `WEAPONS` / `SHIELDS` / `CARGO`) only
|
|
the chosen block contributes, with `level` as the target.
|
|
|
|
The preview hides when the form is invalid (`tech !== "ALL"`
|
|
with non-positive `level`) or when `Core` has not yet booted —
|
|
the bridge call is the only source of truth, so we surface
|
|
"preview unavailable" rather than fall back to a JS
|
|
re-implementation that could drift from the engine.
|
|
|
|
## Wire shape
|
|
|
|
Every emitted command carries:
|
|
|
|
- `id` — client-minted UUID (`crypto.randomUUID()`), used by the
|
|
order draft for status tracking; mirrored as
|
|
`CommandItem.cmdId` on the wire.
|
|
- `groupId` — the source ship-group's UUID (or `newGroupId` when
|
|
the action is the second half of an implicit split). On the
|
|
wire it is the `id` field of every ship-group payload type.
|
|
|
|
Per-action additional fields are documented on the
|
|
`OrderCommand` union in
|
|
`ui/frontend/src/sync/order-types.ts` next to the JSDoc for each
|
|
variant.
|
|
|
|
## Design notes
|
|
|
|
- **`BlockUpgradeCost` lives in `pkg/calc`**. The cost formula
|
|
lives in `pkg/calc/ship.go`; the `ui/core/calc` bridge wraps
|
|
pure `pkg/calc/` formulas, and the controller imports it
|
|
(`controller/ship_group_upgrade.go`).
|
|
- **`GameReport.otherRaces`**. The transfer-to-race picker reads
|
|
from `GameReport.otherRaces: string[]`, populated by walking
|
|
`report.player[]` and excluding the local race plus every
|
|
`extinct` entry. The Races View reuses the same field.
|
|
- **Stationed-ship rows are clickable**. The map deliberately
|
|
hides on-planet groups; the planet inspector's stationed-ship
|
|
rows pivot the selection to the corresponding ship-group
|
|
variant so the actions panel is reachable from the standard
|
|
click flow.
|