Files
galaxy-game/ui/docs/ship-group-actions.md
T
Ilia Denisov 3626998a33 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>
2026-05-10 16:27:55 +02:00

7.6 KiB

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.
  • 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.

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.