feat(ui): map canvas follows light/dark theme; fix invisible gear control
The map view now selects a DARK_THEME or LIGHT_THEME palette from the resolved app theme and threads it through every primitive builder, so the canvas, planets, ship groups, cargo routes, battle/bombing markers, fog, reach + selection rings, pending-Send tracks, and the pick overlay all switch with the rest of the chrome. A theme flip remounts the renderer preserving the camera — Pixi bakes the background at init and every primitive bakes its colour at build, so a live re-tint is not possible on the same instance. This also fixes the reported bug: the gear-popover trigger and the loading overlay hardcoded a dark navy background, so in light theme the gear was invisible (dark icon on dark chip) until hover flipped it to a white chip. Both now use the --color-surface-overlay token and read correctly in both themes. The light palette mirrors the dark one role-for-role, darkened / saturated for contrast on a light background while keeping the incoming, battle, and bombing accents vivid. The values are a first pass meant to be refined during the F8 manual-QA loop. Removes the now-dead "Phase 35" references from the code and lifts the map-recoloring prohibition from the design-system / renderer docs; the battle scene stays a fixed-palette data-viz surface. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -17,18 +17,16 @@
|
||||
// same `world` / `hitLookup` plumbing as planets and ship groups.
|
||||
|
||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||
import type {
|
||||
CirclePrim,
|
||||
LinePrim,
|
||||
Primitive,
|
||||
PrimitiveID,
|
||||
Style,
|
||||
import {
|
||||
DARK_THEME,
|
||||
type CirclePrim,
|
||||
type LinePrim,
|
||||
type Primitive,
|
||||
type PrimitiveID,
|
||||
type Style,
|
||||
type Theme,
|
||||
} from "./world";
|
||||
|
||||
export const BATTLE_MARKER_COLOR = 0xffd400;
|
||||
export const BOMBING_MARKER_COLOR_DAMAGED = 0xffd400;
|
||||
export const BOMBING_MARKER_COLOR_WIPED = 0xff3030;
|
||||
|
||||
/** Battle and bombing marker primitive ids use a high-bit prefix to
|
||||
* avoid colliding with planet numbers or cargo-route line ids. */
|
||||
export const BATTLE_MARKER_ID_PREFIX = 0xa0000000;
|
||||
@@ -102,6 +100,7 @@ export function battleMarkerStrokeWidth(shots: number): number {
|
||||
*/
|
||||
export function buildBattleAndBombingMarkers(
|
||||
report: GameReport,
|
||||
theme: Theme = DARK_THEME,
|
||||
): BuildMarkersResult {
|
||||
const planetByNumber = new Map<number, ReportPlanet>();
|
||||
for (const planet of report.planets) {
|
||||
@@ -127,7 +126,7 @@ export function buildBattleAndBombingMarkers(
|
||||
if (planet === undefined) continue;
|
||||
const strokeWidthPx = battleMarkerStrokeWidth(battle.shots);
|
||||
const style: Style = {
|
||||
strokeColor: BATTLE_MARKER_COLOR,
|
||||
strokeColor: theme.battleMarker,
|
||||
strokeAlpha: 0.95,
|
||||
strokeWidthPx,
|
||||
};
|
||||
@@ -172,9 +171,7 @@ export function buildBattleAndBombingMarkers(
|
||||
const bombing = report.bombings[i];
|
||||
const planet = planetByNumber.get(bombing.planetNumber);
|
||||
if (planet === undefined) continue;
|
||||
const color = bombing.wiped
|
||||
? BOMBING_MARKER_COLOR_WIPED
|
||||
: BOMBING_MARKER_COLOR_DAMAGED;
|
||||
const color = bombing.wiped ? theme.bombingWiped : theme.bombingDamaged;
|
||||
const style: Style = {
|
||||
strokeColor: color,
|
||||
strokeAlpha: 0.9,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// short arrow from the source planet to its destination, drawn as
|
||||
// three `LinePrim` segments — one shaft and two arrowhead wings —
|
||||
// styled per load type so the four cargo kinds are
|
||||
// distinguishable at a glance. Phase 16 ships placeholder
|
||||
// colours; Phase 35 polish picks final values.
|
||||
// distinguishable at a glance. The stroke colours come from the
|
||||
// active `Theme` (dark or light); the alpha and width are fixed.
|
||||
//
|
||||
// Geometry uses `torusShortestDelta` so an arrow that crosses the
|
||||
// torus seam takes the wrap, not the long way round, matching the
|
||||
@@ -13,35 +13,21 @@
|
||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||
import type { CargoLoadType } from "../sync/order-types";
|
||||
import { torusShortestDelta } from "./math";
|
||||
import type { LinePrim, PrimitiveID, Style } from "./world";
|
||||
import { DARK_THEME, type LinePrim, type PrimitiveID, type Style, type Theme } from "./world";
|
||||
|
||||
export const STYLE_ROUTE_COL: Style = {
|
||||
strokeColor: 0x4fc3f7,
|
||||
strokeAlpha: 0.95,
|
||||
strokeWidthPx: 2,
|
||||
};
|
||||
export const STYLE_ROUTE_CAP: Style = {
|
||||
strokeColor: 0xffb74d,
|
||||
strokeAlpha: 0.95,
|
||||
strokeWidthPx: 2,
|
||||
};
|
||||
export const STYLE_ROUTE_MAT: Style = {
|
||||
strokeColor: 0x81c784,
|
||||
strokeAlpha: 0.95,
|
||||
strokeWidthPx: 2,
|
||||
};
|
||||
export const STYLE_ROUTE_EMP: Style = {
|
||||
strokeColor: 0x90a4ae,
|
||||
strokeAlpha: 0.85,
|
||||
strokeWidthPx: 1,
|
||||
};
|
||||
|
||||
const STYLE_BY_LOAD_TYPE: Record<CargoLoadType, Style> = {
|
||||
COL: STYLE_ROUTE_COL,
|
||||
CAP: STYLE_ROUTE_CAP,
|
||||
MAT: STYLE_ROUTE_MAT,
|
||||
EMP: STYLE_ROUTE_EMP,
|
||||
};
|
||||
/**
|
||||
* routeStylesByLoadType builds the per-load-type stroke styles for the
|
||||
* active theme. A single `Style` object is shared by every line of a
|
||||
* given load type within one call so the renderer can dedupe them.
|
||||
*/
|
||||
function routeStylesByLoadType(theme: Theme): Record<CargoLoadType, Style> {
|
||||
return {
|
||||
COL: { strokeColor: theme.routeCol, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||
CAP: { strokeColor: theme.routeCap, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||
MAT: { strokeColor: theme.routeMat, strokeAlpha: 0.95, strokeWidthPx: 2 },
|
||||
EMP: { strokeColor: theme.routeEmp, strokeAlpha: 0.85, strokeWidthPx: 1 },
|
||||
};
|
||||
}
|
||||
|
||||
/** Per-load-type priority. Higher wins hit-test ties; planets sit
|
||||
* at 1..4 (`state-binding.ts.priorityFor`), so route arrows always
|
||||
@@ -91,13 +77,18 @@ const HEAD_HALF_ANGLE = (25 * Math.PI) / 180;
|
||||
* whose routes — outgoing or incoming — should be filtered out so the
|
||||
* arrows do not point at hidden glyphs. Empty / undefined means no
|
||||
* extra filtering, preserving the pre-Phase-29 contract.
|
||||
*
|
||||
* `theme` supplies the per-load-type stroke colours and defaults to
|
||||
* `DARK_THEME`.
|
||||
*/
|
||||
export function buildCargoRouteLines(
|
||||
report: GameReport,
|
||||
opts?: { skipPlanets?: ReadonlySet<number> },
|
||||
theme: Theme = DARK_THEME,
|
||||
): LinePrim[] {
|
||||
if (report.routes.length === 0) return [];
|
||||
const skip = opts?.skipPlanets;
|
||||
const styleByLoadType = routeStylesByLoadType(theme);
|
||||
const planetById = new Map<number, ReportPlanet>();
|
||||
for (const planet of report.planets) {
|
||||
planetById.set(planet.number, planet);
|
||||
@@ -131,7 +122,7 @@ export function buildCargoRouteLines(
|
||||
route.sourcePlanetNumber,
|
||||
entry.loadType,
|
||||
);
|
||||
const style = STYLE_BY_LOAD_TYPE[entry.loadType];
|
||||
const style = styleByLoadType[entry.loadType];
|
||||
const priority = PRIORITY_BY_LOAD_TYPE[entry.loadType];
|
||||
lines.push({
|
||||
kind: "line",
|
||||
|
||||
@@ -4,6 +4,7 @@ export {
|
||||
DEFAULT_HIT_SLOP_PX,
|
||||
KIND_ORDER,
|
||||
DARK_THEME,
|
||||
LIGHT_THEME,
|
||||
World,
|
||||
type Camera,
|
||||
type CirclePrim,
|
||||
|
||||
@@ -15,14 +15,7 @@
|
||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||
import type { OrderCommand } from "../sync/order-types";
|
||||
import { torusShortestDelta } from "./math";
|
||||
import type { LinePrim, PrimitiveID, Style } from "./world";
|
||||
|
||||
const STYLE_PENDING_SEND_LINE: Style = {
|
||||
strokeColor: 0x66bb6a,
|
||||
strokeAlpha: 0.85,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
};
|
||||
import { DARK_THEME, type LinePrim, type PrimitiveID, type Style, type Theme } from "./world";
|
||||
|
||||
// Sit between cargo-route arrows (5..8) and ship-group points (5..)
|
||||
// in priority. The line never participates in hit-test (hitSlopPx=0)
|
||||
@@ -50,15 +43,24 @@ export const PENDING_SEND_LINE_ID_PREFIX = 0xa0000000;
|
||||
* The function is pure — it walks the supplied arrays and returns
|
||||
* a new primitive list. Callers combine the result with cargo-
|
||||
* route lines and feed both into `handle.setExtraPrimitives`.
|
||||
*
|
||||
* `theme` supplies the dashed-line colour and defaults to `DARK_THEME`.
|
||||
*/
|
||||
export function buildPendingSendLines(
|
||||
report: GameReport,
|
||||
commands: readonly OrderCommand[],
|
||||
statuses: Readonly<Record<string, string>>,
|
||||
opts?: { skipPlanets?: ReadonlySet<number> },
|
||||
theme: Theme = DARK_THEME,
|
||||
): LinePrim[] {
|
||||
if (commands.length === 0) return [];
|
||||
const skip = opts?.skipPlanets;
|
||||
const style: Style = {
|
||||
strokeColor: theme.pendingSend,
|
||||
strokeAlpha: 0.85,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
};
|
||||
const planetById = new Map<number, ReportPlanet>();
|
||||
for (const planet of report.planets) {
|
||||
planetById.set(planet.number, planet);
|
||||
@@ -93,7 +95,7 @@ export function buildPendingSendLines(
|
||||
kind: "line",
|
||||
id: pendingSendLineId(serial),
|
||||
priority: PRIORITY_PENDING_SEND_LINE,
|
||||
style: STYLE_PENDING_SEND_LINE,
|
||||
style,
|
||||
hitSlopPx: 0,
|
||||
x1: source.x,
|
||||
y1: source.y,
|
||||
|
||||
@@ -148,23 +148,22 @@ export function computePickOverlay(
|
||||
}
|
||||
|
||||
/**
|
||||
* PICK_OVERLAY_STYLE captures the colours / widths the renderer
|
||||
* applies to each spec channel. Exported so tests and future themes
|
||||
* can read the same values.
|
||||
* PICK_OVERLAY_STYLE captures the per-channel alphas and widths the
|
||||
* renderer applies to the pick overlay, plus the dim alpha for
|
||||
* non-reachable primitives. The colours themselves — the highlight
|
||||
* colour shared by the anchor / line / hover channels and the dim
|
||||
* multiply tint — come from the active `Theme` (`pickHighlight`,
|
||||
* `pickDimTint`) so the overlay tracks the light / dark palette.
|
||||
*
|
||||
* `dimAlpha` and `dimTint` are applied together to non-reachable
|
||||
* primitives during a pick session: the alpha drops their
|
||||
* brightness, and the tint multiplies their fill colour toward dark
|
||||
* gray so the colour identity (planet kind) collapses into a
|
||||
* single muted shade. The combination has to read as "obviously
|
||||
* disabled" against the dark theme — bright planets such as
|
||||
* `STYLE_LOCAL` (`0x6dd2ff`) survive a 0.3 alpha alone too
|
||||
* comfortably, so the tint pulls them down too.
|
||||
* `dimAlpha` and `Theme.pickDimTint` are applied together to
|
||||
* non-reachable primitives during a pick session: the alpha drops
|
||||
* their brightness while the tint collapses the colour identity
|
||||
* (planet kind) into a single muted shade, so the disabled set reads
|
||||
* as obviously inert against the map background.
|
||||
*/
|
||||
export const PICK_OVERLAY_STYLE = {
|
||||
anchor: { color: 0xffe082, alpha: 0.9, width: 2 },
|
||||
line: { color: 0xffe082, alpha: 0.5, width: 1 },
|
||||
hover: { color: 0xffe082, alpha: 1, width: 2 },
|
||||
anchor: { alpha: 0.9, width: 2 },
|
||||
line: { alpha: 0.5, width: 1 },
|
||||
hover: { alpha: 1, width: 2 },
|
||||
dimAlpha: 0.35,
|
||||
dimTint: 0x303841,
|
||||
} as const;
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
// clears the map in one turn therefore shows a single ring; a slow ship
|
||||
// shows all three.
|
||||
|
||||
import type { CirclePrim } from "./world";
|
||||
import { DARK_THEME, type CirclePrim, type Theme } from "./world";
|
||||
|
||||
export const REACH_CIRCLE_COLOR = 0x6d8cff;
|
||||
/** High-bit prefix so reach-circle ids never collide with planet
|
||||
* numbers, cargo-route lines, or battle/bombing markers. */
|
||||
export const REACH_CIRCLE_ID_PREFIX = 0xb0000000;
|
||||
@@ -47,7 +46,8 @@ export function reachBound(
|
||||
* centred on `origin`, with radii speedPerTurn × {1, 2, 3}. A ring for
|
||||
* turn `t` is included only when the previous ring still fits inside the
|
||||
* map's reach bound, so the count shrinks as the per-turn speed grows.
|
||||
* Returns an empty list when the speed is non-positive.
|
||||
* Returns an empty list when the speed is non-positive. `theme`
|
||||
* supplies the ring colour and defaults to `DARK_THEME`.
|
||||
*/
|
||||
export function computeReachCircles(
|
||||
origin: { x: number; y: number },
|
||||
@@ -55,6 +55,7 @@ export function computeReachCircles(
|
||||
mapWidth: number,
|
||||
mapHeight: number,
|
||||
mode: "torus" | "no-wrap",
|
||||
theme: Theme = DARK_THEME,
|
||||
): CirclePrim[] {
|
||||
if (speedPerTurn <= 0) return [];
|
||||
const bound = reachBound(origin, mapWidth, mapHeight, mode);
|
||||
@@ -71,7 +72,7 @@ export function computeReachCircles(
|
||||
y: origin.y,
|
||||
radius: speedPerTurn * turn,
|
||||
style: {
|
||||
strokeColor: REACH_CIRCLE_COLOR,
|
||||
strokeColor: theme.reachCircle,
|
||||
strokeAlpha: 0.55 - (turn - 1) * 0.12,
|
||||
strokeWidthPx: 0.5,
|
||||
},
|
||||
|
||||
@@ -226,12 +226,6 @@ const ORIGIN_COPY_INDEX = 4; // (0, 0) entry of TORUS_OFFSETS
|
||||
// debug surface stays allocation-free.
|
||||
const EMPTY_HIDDEN_IDS: ReadonlySet<PrimitiveID> = new Set();
|
||||
|
||||
// FOG_COLOR is the Phase 29 visibility-fog colour. Two shades
|
||||
// lighter than the dark theme background (`0x0a0e1a`) so it reads
|
||||
// as a faint fog without contrasting against the rest of the map.
|
||||
// The colour is tunable in Phase 35 polish.
|
||||
export const FOG_COLOR = 0x12162a;
|
||||
|
||||
/**
|
||||
* FogPaintOp is one item in the ordered draw sequence produced by
|
||||
* `fogPaintOps`. The renderer dispatches each op directly onto a
|
||||
@@ -657,7 +651,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
g.clear();
|
||||
g.circle(spec.anchor.x, spec.anchor.y, spec.anchor.radius);
|
||||
g.stroke({
|
||||
color: PICK_OVERLAY_STYLE.anchor.color,
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.anchor.alpha,
|
||||
width: PICK_OVERLAY_STYLE.anchor.width,
|
||||
});
|
||||
@@ -665,7 +659,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
g.moveTo(spec.line.x1, spec.line.y1);
|
||||
g.lineTo(spec.line.x2, spec.line.y2);
|
||||
g.stroke({
|
||||
color: PICK_OVERLAY_STYLE.line.color,
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.line.alpha,
|
||||
width: PICK_OVERLAY_STYLE.line.width,
|
||||
});
|
||||
@@ -677,7 +671,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
spec.hoverOutline.radius,
|
||||
);
|
||||
g.stroke({
|
||||
color: PICK_OVERLAY_STYLE.hover.color,
|
||||
color: theme.pickHighlight,
|
||||
alpha: PICK_OVERLAY_STYLE.hover.alpha,
|
||||
width: PICK_OVERLAY_STYLE.hover.width,
|
||||
});
|
||||
@@ -720,7 +714,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
dimmedAlphaBackup.set(g, g.alpha);
|
||||
dimmedTintBackup.set(g, g.tint as number);
|
||||
g.alpha = PICK_OVERLAY_STYLE.dimAlpha;
|
||||
g.tint = PICK_OVERLAY_STYLE.dimTint;
|
||||
g.tint = theme.pickDimTint;
|
||||
}
|
||||
}
|
||||
// Overlay graphic. Lives in the origin copy so the central
|
||||
@@ -891,7 +885,7 @@ export async function createRenderer(opts: RendererOptions): Promise<RendererHan
|
||||
const ops = fogPaintOps(
|
||||
opts.world,
|
||||
circles,
|
||||
FOG_COLOR,
|
||||
theme.fog,
|
||||
theme.background,
|
||||
mode,
|
||||
);
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
// Ship-group selection is intentionally not ringed here — groups are
|
||||
// addressed by report index and have no single stable map coordinate.
|
||||
|
||||
import type { CirclePrim } from "./world";
|
||||
import { DARK_THEME, type CirclePrim, type Theme } from "./world";
|
||||
|
||||
/** Planet marker radius in world units; mirrors `battle-markers.ts`. */
|
||||
const PLANET_RADIUS_WORLD = 6;
|
||||
/** The ring sits just outside the marker (and the bombing ring at +3). */
|
||||
const SELECTION_RING_RADIUS = PLANET_RADIUS_WORLD + 4;
|
||||
|
||||
export const SELECTION_RING_COLOR = 0x6d8cff;
|
||||
/** High-bit prefix so the ring id never collides with planet numbers,
|
||||
* route lines, reach rings (`0xb…`), or battle markers. */
|
||||
export const SELECTION_RING_ID = 0xc0000000;
|
||||
@@ -21,11 +20,13 @@ const SELECTION_RING_PRIORITY = 0;
|
||||
/**
|
||||
* computeSelectionRing returns one ring primitive centred on the selected
|
||||
* planet, or `null` when nothing (or a non-planet) is selected or the
|
||||
* planet is absent from the current report.
|
||||
* planet is absent from the current report. `theme` supplies the ring
|
||||
* colour and defaults to `DARK_THEME`.
|
||||
*/
|
||||
export function computeSelectionRing(
|
||||
planets: ReadonlyArray<{ number: number; x: number; y: number }>,
|
||||
selectedPlanetId: number | null,
|
||||
theme: Theme = DARK_THEME,
|
||||
): CirclePrim | null {
|
||||
if (selectedPlanetId === null) return null;
|
||||
const planet = planets.find((p) => p.number === selectedPlanetId);
|
||||
@@ -39,7 +40,7 @@ export function computeSelectionRing(
|
||||
y: planet.y,
|
||||
radius: SELECTION_RING_RADIUS,
|
||||
style: {
|
||||
strokeColor: SELECTION_RING_COLOR,
|
||||
strokeColor: theme.selectionRing,
|
||||
strokeAlpha: 0.95,
|
||||
strokeWidthPx: 1.5,
|
||||
},
|
||||
|
||||
@@ -38,7 +38,14 @@ import type {
|
||||
} from "../api/game-state";
|
||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||
import { torusShortestDelta } from "./math";
|
||||
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";
|
||||
import {
|
||||
DARK_THEME,
|
||||
type LinePrim,
|
||||
type PointPrim,
|
||||
type PrimitiveID,
|
||||
type Style,
|
||||
type Theme,
|
||||
} from "./world";
|
||||
|
||||
/**
|
||||
* SHIP_GROUP_ID_OFFSETS partitions the primitive-id namespace so a
|
||||
@@ -56,43 +63,46 @@ export const SHIP_GROUP_ID_OFFSETS = {
|
||||
unidentified: 400_000_000,
|
||||
} as const;
|
||||
|
||||
const STYLE_LOCAL_GROUP: Style = {
|
||||
fillColor: 0xfff176,
|
||||
fillAlpha: 0.95,
|
||||
pointRadiusPx: 3,
|
||||
};
|
||||
|
||||
const STYLE_LOCAL_INSPACE_LINE: Style = {
|
||||
strokeColor: 0xfff176,
|
||||
strokeAlpha: 0.7,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
};
|
||||
|
||||
const STYLE_OTHER_GROUP: Style = {
|
||||
fillColor: 0xff6f40,
|
||||
fillAlpha: 0.9,
|
||||
pointRadiusPx: 3,
|
||||
};
|
||||
|
||||
const STYLE_INCOMING_GROUP: Style = {
|
||||
fillColor: 0xff5252,
|
||||
fillAlpha: 1,
|
||||
pointRadiusPx: 4,
|
||||
};
|
||||
|
||||
const STYLE_INCOMING_LINE: Style = {
|
||||
strokeColor: 0xff5252,
|
||||
strokeAlpha: 0.85,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
};
|
||||
|
||||
const STYLE_UNIDENTIFIED_GROUP: Style = {
|
||||
fillColor: 0x9aa3a8,
|
||||
fillAlpha: 0.65,
|
||||
pointRadiusPx: 3,
|
||||
};
|
||||
// shipGroupStyles builds the per-variant `Style` objects for the
|
||||
// active theme. Only the colours are theme-driven; the alpha, radius,
|
||||
// and dash spacing are fixed emphasis values. The in-space track
|
||||
// reuses the own-group colour and the incoming trajectory line reuses
|
||||
// the incoming colour so each pair reads as one entity.
|
||||
function shipGroupStyles(theme: Theme): {
|
||||
local: Style;
|
||||
localLine: Style;
|
||||
other: Style;
|
||||
incoming: Style;
|
||||
incomingLine: Style;
|
||||
unidentified: Style;
|
||||
} {
|
||||
return {
|
||||
local: { fillColor: theme.shipLocal, fillAlpha: 0.95, pointRadiusPx: 3 },
|
||||
localLine: {
|
||||
strokeColor: theme.shipLocal,
|
||||
strokeAlpha: 0.7,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
},
|
||||
other: { fillColor: theme.shipOther, fillAlpha: 0.9, pointRadiusPx: 3 },
|
||||
incoming: {
|
||||
fillColor: theme.shipIncoming,
|
||||
fillAlpha: 1,
|
||||
pointRadiusPx: 4,
|
||||
},
|
||||
incomingLine: {
|
||||
strokeColor: theme.shipIncoming,
|
||||
strokeAlpha: 0.85,
|
||||
strokeWidthPx: 1,
|
||||
strokeDashPx: 4,
|
||||
},
|
||||
unidentified: {
|
||||
fillColor: theme.shipUnidentified,
|
||||
fillAlpha: 0.65,
|
||||
pointRadiusPx: 3,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Priority order inside `hit-test`: ship groups outrank planets so a
|
||||
// hyperspace group landing on top of an unidentified planet is
|
||||
@@ -146,7 +156,11 @@ function addDependent(
|
||||
set.add(primitiveId);
|
||||
}
|
||||
|
||||
export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives {
|
||||
export function shipGroupsToPrimitives(
|
||||
report: GameReport,
|
||||
theme: Theme = DARK_THEME,
|
||||
): ShipGroupPrimitives {
|
||||
const styles = shipGroupStyles(theme);
|
||||
const primitives: (PointPrim | LinePrim)[] = [];
|
||||
const lookup = new Map<PrimitiveID, ShipGroupRef>();
|
||||
const categories = new Map<PrimitiveID, ShipGroupCategory>();
|
||||
@@ -163,7 +177,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||
if (pos === null) continue;
|
||||
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, styles.local));
|
||||
lookup.set(id, { variant: "local", id: group.id });
|
||||
categories.set(id, "hyperspaceGroup");
|
||||
addDependent(planetDependents, group.destination, id);
|
||||
@@ -183,7 +197,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
kind: "line",
|
||||
id: lineId,
|
||||
priority: PRIORITY_LOCAL_LINE,
|
||||
style: STYLE_LOCAL_INSPACE_LINE,
|
||||
style: styles.localLine,
|
||||
hitSlopPx: 0,
|
||||
x1: origin.x,
|
||||
y1: origin.y,
|
||||
@@ -200,7 +214,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||
if (pos === null) continue;
|
||||
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, styles.other));
|
||||
lookup.set(id, { variant: "other", index: i });
|
||||
categories.set(id, "hyperspaceGroup");
|
||||
addDependent(planetDependents, group.destination, id);
|
||||
@@ -225,7 +239,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
kind: "line",
|
||||
id: lineId,
|
||||
priority: PRIORITY_INCOMING_LINE,
|
||||
style: STYLE_INCOMING_LINE,
|
||||
style: styles.incomingLine,
|
||||
hitSlopPx: 0,
|
||||
x1: origin.x,
|
||||
y1: origin.y,
|
||||
@@ -242,7 +256,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
pos.x,
|
||||
pos.y,
|
||||
PRIORITY_INCOMING_POINT,
|
||||
STYLE_INCOMING_GROUP,
|
||||
styles.incoming,
|
||||
/*hitSlopPx*/ 4,
|
||||
),
|
||||
);
|
||||
@@ -261,7 +275,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
group.x,
|
||||
group.y,
|
||||
PRIORITY_UNIDENTIFIED,
|
||||
STYLE_UNIDENTIFIED_GROUP,
|
||||
styles.unidentified,
|
||||
),
|
||||
);
|
||||
lookup.set(id, { variant: "unidentified", index: i });
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
//
|
||||
// The four planet kinds in the report each map to a distinct style so
|
||||
// the user can tell own / other-race / uninhabited / unidentified
|
||||
// planets apart at a glance. The exact colours are Phase 11 defaults
|
||||
// chosen against the dark theme; Phase 35 polish picks final
|
||||
// colours and adds theme switching.
|
||||
// planets apart at a glance. The colours come from the active `Theme`
|
||||
// (dark or light); only the per-kind alpha and radius are fixed here.
|
||||
|
||||
import type { GameReport, ReportPlanet } from "../api/game-state";
|
||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||
@@ -23,31 +22,14 @@ import {
|
||||
shipGroupsToPrimitives,
|
||||
type ShipGroupCategory,
|
||||
} from "./ship-groups";
|
||||
import { World, type Primitive, type PrimitiveID, type Style } from "./world";
|
||||
|
||||
const STYLE_LOCAL: Style = {
|
||||
fillColor: 0x6dd2ff,
|
||||
fillAlpha: 1,
|
||||
pointRadiusPx: 6,
|
||||
};
|
||||
|
||||
const STYLE_OTHER: Style = {
|
||||
fillColor: 0xff8a65,
|
||||
fillAlpha: 1,
|
||||
pointRadiusPx: 5,
|
||||
};
|
||||
|
||||
const STYLE_UNINHABITED: Style = {
|
||||
fillColor: 0xb0bec5,
|
||||
fillAlpha: 0.85,
|
||||
pointRadiusPx: 4,
|
||||
};
|
||||
|
||||
const STYLE_UNIDENTIFIED: Style = {
|
||||
fillColor: 0x546e7a,
|
||||
fillAlpha: 0.7,
|
||||
pointRadiusPx: 3,
|
||||
};
|
||||
import {
|
||||
DARK_THEME,
|
||||
World,
|
||||
type Primitive,
|
||||
type PrimitiveID,
|
||||
type Style,
|
||||
type Theme,
|
||||
} from "./world";
|
||||
|
||||
// PlanetIDs occupy the [0, 4_000_000_000) range — well below
|
||||
// JavaScript's `Number.MAX_SAFE_INTEGER` — so the engine `number`
|
||||
@@ -55,16 +37,24 @@ const STYLE_UNIDENTIFIED: Style = {
|
||||
// binding uses the engine number directly as the primitive id so the
|
||||
// click handler can recover a planet by hit-test result without an
|
||||
// extra lookup.
|
||||
function styleFor(kind: ReportPlanet["kind"]): Style {
|
||||
function styleFor(kind: ReportPlanet["kind"], theme: Theme): Style {
|
||||
switch (kind) {
|
||||
case "local":
|
||||
return STYLE_LOCAL;
|
||||
return { fillColor: theme.planetLocal, fillAlpha: 1, pointRadiusPx: 6 };
|
||||
case "other":
|
||||
return STYLE_OTHER;
|
||||
return { fillColor: theme.planetOther, fillAlpha: 1, pointRadiusPx: 5 };
|
||||
case "uninhabited":
|
||||
return STYLE_UNINHABITED;
|
||||
return {
|
||||
fillColor: theme.planetUninhabited,
|
||||
fillAlpha: 0.85,
|
||||
pointRadiusPx: 4,
|
||||
};
|
||||
case "unidentified":
|
||||
return STYLE_UNIDENTIFIED;
|
||||
return {
|
||||
fillColor: theme.planetUnidentified,
|
||||
fillAlpha: 0.7,
|
||||
pointRadiusPx: 3,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +136,15 @@ export interface ReportToWorldResult {
|
||||
* If the report carries zero planets (turn-zero edge cases or seeded
|
||||
* tests), the World is still well-formed: the renderer mounts on an
|
||||
* empty primitive list without errors.
|
||||
*
|
||||
* `theme` supplies the planet / ship-group / marker colours; it
|
||||
* defaults to `DARK_THEME` so callers that do not care about theming
|
||||
* (tests, the debug playground) keep the original palette.
|
||||
*/
|
||||
export function reportToWorld(report: GameReport): ReportToWorldResult {
|
||||
export function reportToWorld(
|
||||
report: GameReport,
|
||||
theme: Theme = DARK_THEME,
|
||||
): ReportToWorldResult {
|
||||
const primitives: Primitive[] = [];
|
||||
const hitLookup = new Map<PrimitiveID, HitTarget>();
|
||||
const categories = new Map<PrimitiveID, MapCategory>();
|
||||
@@ -158,7 +155,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
||||
kind: "point",
|
||||
id: planet.number,
|
||||
priority: priorityFor(planet.kind),
|
||||
style: styleFor(planet.kind),
|
||||
style: styleFor(planet.kind, theme),
|
||||
hitSlopPx: 0,
|
||||
x: planet.x,
|
||||
y: planet.y,
|
||||
@@ -174,7 +171,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
||||
planetDependents.set(planet.number, own);
|
||||
}
|
||||
|
||||
const groups = shipGroupsToPrimitives(report);
|
||||
const groups = shipGroupsToPrimitives(report, theme);
|
||||
for (const prim of groups.primitives) {
|
||||
primitives.push(prim);
|
||||
}
|
||||
@@ -186,7 +183,7 @@ export function reportToWorld(report: GameReport): ReportToWorldResult {
|
||||
}
|
||||
mergeDependents(planetDependents, groups.planetDependents);
|
||||
|
||||
const markers = buildBattleAndBombingMarkers(report);
|
||||
const markers = buildBattleAndBombingMarkers(report, theme);
|
||||
for (const prim of markers.primitives) {
|
||||
primitives.push(prim);
|
||||
}
|
||||
|
||||
@@ -130,19 +130,122 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
// Theme carries the default colours used when a primitive's `style`
|
||||
// leaves a colour unset. Phase 9 ships a single dark theme; runtime
|
||||
// theme switching is deferred to Phase 35.
|
||||
// Theme is the renderer's colour palette. It carries both the generic
|
||||
// fallbacks used when a primitive's `style` omits a colour and the
|
||||
// semantic colours every primitive builder paints with (planets, ship
|
||||
// groups, cargo routes, battle / bombing markers, reach + selection
|
||||
// rings, pending-Send tracks, and the pick-mode overlay). Two concrete
|
||||
// palettes are shipped — `DARK_THEME` and `LIGHT_THEME` — and the map
|
||||
// view selects between them from the resolved app theme
|
||||
// (`$lib/theme/theme.svelte.ts`), so the canvas follows the user's
|
||||
// light / dark choice like the rest of the chrome.
|
||||
//
|
||||
// Only colours live here: per-primitive alphas, widths, and radii are
|
||||
// emphasis / geometry, not theme, and stay as constants in the builder
|
||||
// modules. The light palette mirrors the dark one role-for-role but
|
||||
// darkens / saturates each hue so it reads against a light background;
|
||||
// the incoming-group, battle, and bombing accents stay deliberately
|
||||
// vivid in both palettes.
|
||||
export interface Theme {
|
||||
// Canvas background and the visibility-fog veil drawn over
|
||||
// unscanned hyperspace.
|
||||
background: number;
|
||||
fog: number;
|
||||
// Generic fallbacks for primitives whose `style` omits a colour.
|
||||
pointFill: number;
|
||||
circleStroke: number;
|
||||
lineStroke: number;
|
||||
// Planet glyphs, one colour per `ReportPlanet.kind`.
|
||||
planetLocal: number;
|
||||
planetOther: number;
|
||||
planetUninhabited: number;
|
||||
planetUnidentified: number;
|
||||
// Ship groups. The in-space track reuses `shipLocal` and the
|
||||
// incoming trajectory line reuses `shipIncoming`.
|
||||
shipLocal: number;
|
||||
shipOther: number;
|
||||
shipIncoming: number;
|
||||
shipUnidentified: number;
|
||||
// Cargo-route arrows, one colour per load type.
|
||||
routeCol: number;
|
||||
routeCap: number;
|
||||
routeMat: number;
|
||||
routeEmp: number;
|
||||
// Battle X-crosses and bombing rings (damaged vs wiped).
|
||||
battleMarker: number;
|
||||
bombingDamaged: number;
|
||||
bombingWiped: number;
|
||||
// Reach rings, the selected-planet ring, and pending-Send tracks.
|
||||
reachCircle: number;
|
||||
selectionRing: number;
|
||||
pendingSend: number;
|
||||
// Pick-mode overlay: the anchor / cursor-line / hover highlight
|
||||
// colour and the multiply tint applied to non-reachable primitives.
|
||||
pickHighlight: number;
|
||||
pickDimTint: number;
|
||||
}
|
||||
|
||||
export const DARK_THEME: Theme = {
|
||||
background: 0x0a0e1a,
|
||||
fog: 0x12162a,
|
||||
pointFill: 0xe8eaf6,
|
||||
circleStroke: 0x4fc3f7,
|
||||
lineStroke: 0xa5d6a7,
|
||||
planetLocal: 0x6dd2ff,
|
||||
planetOther: 0xff8a65,
|
||||
planetUninhabited: 0xb0bec5,
|
||||
planetUnidentified: 0x546e7a,
|
||||
shipLocal: 0xfff176,
|
||||
shipOther: 0xff6f40,
|
||||
shipIncoming: 0xff5252,
|
||||
shipUnidentified: 0x9aa3a8,
|
||||
routeCol: 0x4fc3f7,
|
||||
routeCap: 0xffb74d,
|
||||
routeMat: 0x81c784,
|
||||
routeEmp: 0x90a4ae,
|
||||
battleMarker: 0xffd400,
|
||||
bombingDamaged: 0xffd400,
|
||||
bombingWiped: 0xff3030,
|
||||
reachCircle: 0x6d8cff,
|
||||
selectionRing: 0x6d8cff,
|
||||
pendingSend: 0x66bb6a,
|
||||
pickHighlight: 0xffe082,
|
||||
pickDimTint: 0x303841,
|
||||
};
|
||||
|
||||
// LIGHT_THEME mirrors DARK_THEME role-for-role. The background matches
|
||||
// the app's light shell background (`--color-bg` in `tokens.css`) so
|
||||
// the canvas blends into the surrounding chrome instead of reading as a
|
||||
// dark rectangle; the fog is a faint darkening over the lighter base.
|
||||
// Hues are darkened / saturated relative to the dark palette so small
|
||||
// glyphs and thin strokes stay legible on a light surface, while the
|
||||
// incoming (red), battle (amber), and bombing (amber / red) accents are
|
||||
// kept vivid. Values are a first pass meant to be refined during the
|
||||
// owner's F8 manual-QA loop.
|
||||
export const LIGHT_THEME: Theme = {
|
||||
background: 0xf3f5fb,
|
||||
fog: 0xe2e7f1,
|
||||
pointFill: 0x1a2138,
|
||||
circleStroke: 0x1565c0,
|
||||
lineStroke: 0x2e7d32,
|
||||
planetLocal: 0x1565c0,
|
||||
planetOther: 0xe64a19,
|
||||
planetUninhabited: 0x78909c,
|
||||
planetUnidentified: 0x90a4ae,
|
||||
shipLocal: 0xc79100,
|
||||
shipOther: 0xd84315,
|
||||
shipIncoming: 0xd50000,
|
||||
shipUnidentified: 0x607d8b,
|
||||
routeCol: 0x0288d1,
|
||||
routeCap: 0xef6c00,
|
||||
routeMat: 0x2e7d32,
|
||||
routeEmp: 0x607d8b,
|
||||
battleMarker: 0xf57f17,
|
||||
bombingDamaged: 0xf57f17,
|
||||
bombingWiped: 0xc62828,
|
||||
reachCircle: 0x3949ab,
|
||||
selectionRing: 0x3949ab,
|
||||
pendingSend: 0x388e3c,
|
||||
pickHighlight: 0xef6c00,
|
||||
pickDimTint: 0xaeb6c4,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user