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:
@@ -1,29 +1,37 @@
|
||||
<!--
|
||||
Phase 19 read-only "ship groups stationed here" subsection of the
|
||||
planet inspector. Phase 19 originally rendered every in-orbit group
|
||||
as an offset point near its planet on the map, which crowded the
|
||||
canvas to the point of unreadability. The map now hides on-planet
|
||||
groups and the planet inspector lists them instead — one row per
|
||||
group, showing its race, class, ship count, and mass.
|
||||
|
||||
Race attribution is best-effort:
|
||||
"Ship groups stationed here" subsection of the planet inspector.
|
||||
The map deliberately hides on-planet groups (rendering them as
|
||||
offset points crowds the canvas), so this list is the player's
|
||||
view of which fleets sit in this orbit. Race attribution is
|
||||
best-effort:
|
||||
- LocalGroup → the player's own race (`localRace` prop).
|
||||
- OtherGroup on an `other`-kind planet → the planet's owner.
|
||||
- OtherGroup elsewhere → "foreign" placeholder; the engine's
|
||||
typed contract does not carry per-group ownership outside
|
||||
battle rosters.
|
||||
|
||||
Rows are intentionally non-interactive in Phase 19. Phase 21+ will
|
||||
deliver a full ship-groups table view; clicking a row will then
|
||||
deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
Phase 20 makes own-ship rows interactive: clicking a row pivots
|
||||
the inspector to the corresponding ship-group inspector through
|
||||
the shared `SelectionStore`. The actions panel mounts on top of
|
||||
the existing ship-group inspector, so the row is the on-planet
|
||||
entry point for Send / Load / Modernize / etc. Foreign rows stay
|
||||
non-interactive — there are no actions to drive against another
|
||||
race's fleet. Phase 21+ will reuse the same row shape inside the
|
||||
ship-groups table view with an additional `(planet, race)` filter.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type {
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
} from "../../../api/game-state";
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import {
|
||||
SELECTION_CONTEXT_KEY,
|
||||
type SelectionStore,
|
||||
} from "$lib/selection.svelte";
|
||||
|
||||
type Props = {
|
||||
planet: ReportPlanet;
|
||||
@@ -33,12 +41,18 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
};
|
||||
let { planet, localShipGroups, otherShipGroups, localRace }: Props = $props();
|
||||
|
||||
const selection = getContext<SelectionStore | undefined>(
|
||||
SELECTION_CONTEXT_KEY,
|
||||
);
|
||||
|
||||
interface StationedRow {
|
||||
key: string;
|
||||
race: string;
|
||||
class: string;
|
||||
count: number;
|
||||
mass: number;
|
||||
selectable: boolean;
|
||||
groupId: string | null;
|
||||
}
|
||||
|
||||
const stationedRows: StationedRow[] = $derived.by(() => {
|
||||
@@ -52,6 +66,8 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
class: g.class,
|
||||
count: g.count,
|
||||
mass: g.mass,
|
||||
selectable: true,
|
||||
groupId: g.id,
|
||||
});
|
||||
}
|
||||
const foreignRace =
|
||||
@@ -67,6 +83,8 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
class: g.class,
|
||||
count: g.count,
|
||||
mass: g.mass,
|
||||
selectable: false,
|
||||
groupId: null,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -75,6 +93,11 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
function formatNumber(value: number): string {
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
function selectLocalGroup(groupId: string): void {
|
||||
if (selection === undefined) return;
|
||||
selection.selectShipGroup({ variant: "local", id: groupId });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if stationedRows.length > 0}
|
||||
@@ -83,20 +106,45 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
<ul class="rows">
|
||||
{#each stationedRows as row (row.key)}
|
||||
<li class="row" data-testid="inspector-planet-ship-groups-row">
|
||||
<span class="race" data-testid="inspector-planet-ship-groups-race">
|
||||
{row.race}
|
||||
</span>
|
||||
<span class="class">{row.class}</span>
|
||||
<span class="count">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.count", {
|
||||
count: String(row.count),
|
||||
})}
|
||||
</span>
|
||||
<span class="mass">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.mass", {
|
||||
mass: formatNumber(row.mass),
|
||||
})}
|
||||
</span>
|
||||
{#if row.selectable && row.groupId !== null}
|
||||
{@const groupId = row.groupId}
|
||||
<button
|
||||
type="button"
|
||||
class="select"
|
||||
data-testid="inspector-planet-ship-groups-select"
|
||||
onclick={() => selectLocalGroup(groupId)}
|
||||
>
|
||||
<span class="race" data-testid="inspector-planet-ship-groups-race">
|
||||
{row.race}
|
||||
</span>
|
||||
<span class="class">{row.class}</span>
|
||||
<span class="count">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.count", {
|
||||
count: String(row.count),
|
||||
})}
|
||||
</span>
|
||||
<span class="mass">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.mass", {
|
||||
mass: formatNumber(row.mass),
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
{:else}
|
||||
<span class="race" data-testid="inspector-planet-ship-groups-race">
|
||||
{row.race}
|
||||
</span>
|
||||
<span class="class">{row.class}</span>
|
||||
<span class="count">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.count", {
|
||||
count: String(row.count),
|
||||
})}
|
||||
</span>
|
||||
<span class="mass">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.mass", {
|
||||
mass: formatNumber(row.mass),
|
||||
})}
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -125,11 +173,30 @@ deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.row {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.row > span,
|
||||
.row > .select {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto auto;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.select {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
padding: 0.15rem 0.3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.select:hover {
|
||||
border-color: #2a3150;
|
||||
background: #0d1224;
|
||||
}
|
||||
.race {
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user