54733bfb14
Send joins Modernize / Dismantle / Transfer as a lockable command: once any of the four lands in the draft for a group, every action button on its inspector is disabled with a "command pending" tooltip and the banner names the queued kind. Load / Unload / Split / Join Fleet stay non-locking — they stack legitimately on the engine side. Two dashed overlays now run alongside the cargo-route arrows: - Yellow dashed track for own in-space groups, drawn from the origin planet to the destination (matches the in-space point colour so 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 rejects it, or when the group has left orbit (in-space track replaces it). Both tracks are wrap-aware via torusShortestDelta and never participate in hit-test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
# Ship-group inspector actions
|
|
|
|
Phase 20 turns the read-only ship-group inspector
|
|
(`ui/frontend/src/lib/inspectors/ship-group.svelte`) into 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 })` (Phase 18 bridge); 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.
|
|
|
|
## Decisions baked into Phase 20
|
|
|
|
- **`BlockUpgradeCost` migrated to `pkg/calc`**. The cost
|
|
formula previously lived in
|
|
`game/internal/controller/ship_group_upgrade.go`. To keep the
|
|
`ui/core/calc` bridge a wrapper around pure `pkg/calc/`
|
|
formulas, the function moved to `pkg/calc/ship.go` and the
|
|
controller now imports it (`controller/ship_group_upgrade.go`).
|
|
- **`GameReport.otherRaces`**. The transfer-to-race picker reads
|
|
from a new `GameReport.otherRaces: string[]` field, populated
|
|
by walking `report.player[]` and excluding the local race plus
|
|
every `extinct` entry. Phase 22 (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 now pivot the selection to the corresponding ship-group
|
|
variant so the actions panel is reachable from the standard
|
|
click flow.
|