feat(ui): F8-10 — tables planets / ship-groups / fleets, ship-classes delete guard (#53)
Lights up three previously-stubbed table active views and tightens the
existing one:
- table-planets: 4 kind checkboxes (own / foreign / uninhabited /
unknown) + race dropdown that filters the foreign slice; row click
selects + centres the planet on the map.
- table-ship-groups: local + foreign groups in one grid, owner
checkboxes, planet dropdown (destination OR origin), class
dropdown; on-planet click focuses the destination planet, in-space
click focuses the ship group itself (camera follows interpolated
position).
- table-fleets: own fleets only with the shared planet dropdown;
on-planet click focuses the planet, in-space click centres the
camera on the interpolated fleet position without altering the
selection (no fleet variant in Selected).
- table-ship-classes: per-row Delete is disabled with a count tooltip
while at least one local ship group references the class. The
engine refuses the removal anyway; the UI pre-empts the surface.
Wires the click → map flow through a transient `SelectionStore.focus`
/ `focusPoint` channel that `map.svelte` consumes once on mount —
in-memory only, so an F5 does not re-centre.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -63,8 +63,10 @@ preference the store already manages.
|
||||
} from "$lib/game-state.svelte";
|
||||
import {
|
||||
SELECTION_CONTEXT_KEY,
|
||||
type Selected,
|
||||
type SelectionStore,
|
||||
} from "$lib/selection.svelte";
|
||||
import { computeInSpacePosition } from "../../map/ship-groups";
|
||||
import {
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
type RenderedReportSource,
|
||||
@@ -515,7 +517,27 @@ preference the store already manages.
|
||||
},
|
||||
world,
|
||||
);
|
||||
if (previousCamera !== null) {
|
||||
// Consume an F8-10 table click that asked the next map mount
|
||||
// to centre on a particular target. The store self-clears on
|
||||
// read, so any later remount inside the same session sees
|
||||
// null and falls through to the default centring path. The
|
||||
// coord-only `pendingCenter` is the fleet-row fallback: a
|
||||
// fleet has no `Selected` variant, but its xy still feeds
|
||||
// the camera. `pendingFocus` wins when both are queued.
|
||||
const focusTarget = selection?.consumePendingFocus() ?? null;
|
||||
const focusPoint =
|
||||
resolveFocusPoint(focusTarget, report, world.width, world.height)
|
||||
?? selection?.consumePendingCenter()
|
||||
?? null;
|
||||
if (focusPoint !== null) {
|
||||
handle.viewport.moveCenter(focusPoint.x, focusPoint.y);
|
||||
handle.viewport.setZoom(
|
||||
previousCamera === null
|
||||
? minScale * 1.05
|
||||
: Math.max(previousCamera.scale, minScale),
|
||||
true,
|
||||
);
|
||||
} else if (previousCamera !== null) {
|
||||
// Same-game remount — preserve pan/zoom. Clamp zoom
|
||||
// to `minScale` so a remount that re-derives the
|
||||
// minimum (e.g. a viewport resize between renderers)
|
||||
@@ -642,6 +664,45 @@ preference the store already manages.
|
||||
}
|
||||
}
|
||||
|
||||
// resolveFocusPoint maps an F8-10 table click target to world (x, y)
|
||||
// for camera centring. Planets resolve via the report; in-space
|
||||
// ship groups via the shared interpolation helper; on-planet ship
|
||||
// groups fall back to the destination planet's xy (so a click on a
|
||||
// group stationed at #5 centres on #5). Returns null when the
|
||||
// target cannot be resolved — a stale ref after a fresh report,
|
||||
// or a planet that is no longer in the visible set; the caller
|
||||
// then falls through to the default centring path.
|
||||
function resolveFocusPoint(
|
||||
target: Selected | null,
|
||||
report: NonNullable<GameStateStore["report"]>,
|
||||
worldWidth: number,
|
||||
worldHeight: number,
|
||||
): { x: number; y: number } | null {
|
||||
if (target === null) return null;
|
||||
if (target.kind === "planet") {
|
||||
const planet = report.planets.find((p) => p.number === target.id);
|
||||
return planet === undefined ? null : { x: planet.x, y: planet.y };
|
||||
}
|
||||
const ref = target.ref;
|
||||
const group =
|
||||
ref.variant === "local"
|
||||
? report.localShipGroups.find((g) => g.id === ref.id)
|
||||
: ref.variant === "other"
|
||||
? report.otherShipGroups[ref.index]
|
||||
: undefined;
|
||||
if (group === undefined) return null;
|
||||
const planetIndex = new Map(report.planets.map((p) => [p.number, p]));
|
||||
const inSpace = computeInSpacePosition(
|
||||
group,
|
||||
planetIndex,
|
||||
worldWidth,
|
||||
worldHeight,
|
||||
);
|
||||
if (inSpace !== null) return inSpace;
|
||||
const dest = planetIndex.get(group.destination);
|
||||
return dest === undefined ? null : { x: dest.x, y: dest.y };
|
||||
}
|
||||
|
||||
// handleMapClick translates a renderer click into a selection
|
||||
// update. A click that misses every primitive (empty space) is a
|
||||
// deliberate no-op: the selection rule from Phase 13 is that only
|
||||
|
||||
Reference in New Issue
Block a user