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:
@@ -24,7 +24,12 @@
|
||||
import type { Cache } from "../platform/store/index";
|
||||
import type { GalaxyClient } from "../api/galaxy-client";
|
||||
import { fetchOrder } from "./order-load";
|
||||
import type { CommandStatus, OrderCommand } from "./order-types";
|
||||
import {
|
||||
isShipGroupCargo,
|
||||
isShipGroupUpgradeTech,
|
||||
type CommandStatus,
|
||||
type OrderCommand,
|
||||
} from "./order-types";
|
||||
import { submitOrder } from "./submit";
|
||||
import { validateEntityName } from "$lib/util/entity-name";
|
||||
import { validateShipClass } from "$lib/util/ship-class-validation";
|
||||
@@ -513,6 +518,68 @@ function validateCommand(cmd: OrderCommand): CommandStatus {
|
||||
// active production / ship groups. Local validation only
|
||||
// guards the name shape.
|
||||
return validateEntityName(cmd.name).ok ? "valid" : "invalid";
|
||||
case "breakShipGroup":
|
||||
// Engine rule (`controller/ship_group.go.breakGroup`):
|
||||
// quantity must be at least 1 and strictly less than the
|
||||
// source group size. We do not know the source size here
|
||||
// (it lives on the report), so the inspector enforces the
|
||||
// upper bound before emitting; locally we only refuse the
|
||||
// degenerate cases — non-positive `quantity`, missing or
|
||||
// equal UUIDs.
|
||||
if (cmd.quantity <= 0) return "invalid";
|
||||
if (!isUuid(cmd.groupId) || !isUuid(cmd.newGroupId)) return "invalid";
|
||||
if (cmd.groupId === cmd.newGroupId) return "invalid";
|
||||
return "valid";
|
||||
case "sendShipGroup":
|
||||
// Reach is enforced by the picker before the command lands
|
||||
// in the draft. Locally we only refuse a degenerate
|
||||
// destination (the engine uses planet number `0` as the
|
||||
// "no planet" sentinel; FBS encodes as `int64`, so any
|
||||
// strictly-positive number is wire-valid).
|
||||
if (cmd.destinationPlanetNumber <= 0) return "invalid";
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "loadShipGroup":
|
||||
// Cargo type and quantity are pre-checked by the inspector
|
||||
// against the planet stock and the group's free capacity;
|
||||
// local validation only guards the wire-valid shape.
|
||||
if (!isShipGroupCargo(cmd.cargo)) return "invalid";
|
||||
if (cmd.quantity <= 0) return "invalid";
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "unloadShipGroup":
|
||||
if (cmd.quantity <= 0) return "invalid";
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "upgradeShipGroup":
|
||||
// Engine rule
|
||||
// (`controller/ship_group_upgrade.go.shipGroupUpgrade:56`):
|
||||
// `tech === "ALL"` requires `level === 0`; per-block tech
|
||||
// requires a strictly positive level. The inspector also
|
||||
// caps the level to the player's race tech, but the
|
||||
// engine re-validates server-side.
|
||||
if (!isShipGroupUpgradeTech(cmd.tech)) return "invalid";
|
||||
if (cmd.tech === "ALL") {
|
||||
if (cmd.level !== 0) return "invalid";
|
||||
} else if (cmd.level <= 0) {
|
||||
return "invalid";
|
||||
}
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "dismantleShipGroup":
|
||||
return isUuid(cmd.groupId) ? "valid" : "invalid";
|
||||
case "transferShipGroup":
|
||||
// `acceptor` is a race name; race names follow the same
|
||||
// entity-name rules as planet/fleet names. The inspector
|
||||
// restricts the picker to `GameReport.otherRaces`, so a
|
||||
// locally-valid name is always a real race.
|
||||
if (!validateEntityName(cmd.acceptor).ok) return "invalid";
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "joinFleetShipGroup":
|
||||
if (!validateEntityName(cmd.name).ok) return "invalid";
|
||||
if (!isUuid(cmd.groupId)) return "invalid";
|
||||
return "valid";
|
||||
case "placeholder":
|
||||
// Phase 12 placeholder entries are content-free and never
|
||||
// transition out of `draft` — they are not submittable.
|
||||
|
||||
Reference in New Issue
Block a user