Step 10C.7: locks, HQ markers and idle badges on the map
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5916cf8728
commit
09a89b2a74
|
|
@ -727,3 +727,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **Labels and badges avoid collisions** with a small greedy pass: a label goes below its body, else above it if below hits another body or label; an idle badge takes the first clear diagonal. This handles Earth and the Moon and long names (Jupiter Cloud Platform next to Mars) without special cases.
|
- **Labels and badges avoid collisions** with a small greedy pass: a label goes below its body, else above it if below hits another body or label; an idle badge takes the first clear diagonal. This handles Earth and the Moon and long names (Jupiter Cloud Platform next to Mars) without special cases.
|
||||||
- **Tap targets** come from the same layout as drawing: each is a circle of the larger of the sprite size and 44 px. Ships still win ties.
|
- **Tap targets** come from the same layout as drawing: each is a circle of the larger of the sprite size and 44 px. Ships still win ties.
|
||||||
- **The idle badge sits just off the body's edge** (2 px, up-left by default) instead of a fixed distance from the centre, so it clears big bodies like Jupiter.
|
- **The idle badge sits just off the body's edge** (2 px, up-left by default) instead of a fixed distance from the centre, so it clears big bodies like Jupiter.
|
||||||
|
|
||||||
|
## Step 10C.7
|
||||||
|
|
||||||
|
- **Overlays are decided by a pure function** (`locationOverlays`) that the renderer applies. PixiJS `Text` cannot be measured under jsdom, so the scene test checks this plan (filter, lock, HQ, badge) rather than live display objects. The manual screenshots cover the drawing.
|
||||||
|
- **The padlock is drawn over the body's centre** and the label loses its 🔒 emoji. Undiscovered labels keep the muted color.
|
||||||
|
- **The HQ marker is a small green house** placed beyond the label (below a label that is below the body, above one that is above), so it never sits between label and body. The old green ring around bay bodies is gone.
|
||||||
|
- **Lock arc fix:** a bare `Graphics.arc` in PixiJS continues the current path, which drew long stray lines. The lock now starts its shackle with `moveTo`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Scene } from "./scene";
|
||||||
|
import { mapStyle, type MapStyle } from "./style";
|
||||||
|
|
||||||
|
/** How a location's state is shown on the map (Step 10C.7). */
|
||||||
|
export interface LocationOverlay {
|
||||||
|
id: string;
|
||||||
|
/** Undiscovered: a code-drawn lock over the body. */
|
||||||
|
locked: boolean;
|
||||||
|
/** Undiscovered: the sprite is desaturated (color matrix filter) and dimmed. */
|
||||||
|
desaturate: boolean;
|
||||||
|
alpha: number;
|
||||||
|
/** The player has a bay (HQ) here: a small marker by the label. */
|
||||||
|
hq: boolean;
|
||||||
|
/** Idle ships here, as a count badge; null for none. Idle ships are never drawn singly. */
|
||||||
|
idleBadge: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function locationOverlays(scene: Scene, style: MapStyle = mapStyle): LocationOverlay[] {
|
||||||
|
return scene.locations.map((l) => ({
|
||||||
|
id: l.id,
|
||||||
|
locked: !l.discovered,
|
||||||
|
desaturate: !l.discovered,
|
||||||
|
alpha: l.discovered ? 1 : style.undiscoveredAlpha,
|
||||||
|
hq: l.hasBay,
|
||||||
|
idleBadge: l.idleShips > 0 ? l.idleShips : null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { Container, Graphics, Sprite, Text, type Texture } from "pixi.js";
|
import { ColorMatrixFilter, Container, Graphics, Sprite, Text, type Texture } from "pixi.js";
|
||||||
import type { ArtTextures } from "./art/load";
|
import type { ArtTextures } from "./art/load";
|
||||||
import { sceneVisuals, type Visual } from "./art/visuals";
|
import { sceneVisuals, type Visual } from "./art/visuals";
|
||||||
import { toScreen, type Camera, type Viewport } from "./camera";
|
import { toScreen, type Camera, type Viewport } from "./camera";
|
||||||
import { backgroundRect, decorate, layoutScene, polar } from "./layout";
|
import { backgroundRect, decorate, layoutScene, polar } from "./layout";
|
||||||
|
import { locationOverlays } from "./overlays";
|
||||||
import type { MapPoint } from "./projection";
|
import type { MapPoint } from "./projection";
|
||||||
import type { Scene } from "./scene";
|
import type { Scene } from "./scene";
|
||||||
import { mapStyle } from "./style";
|
import { mapStyle } from "./style";
|
||||||
|
|
@ -15,6 +16,44 @@ function fit(sprite: Sprite, size: number) {
|
||||||
sprite.scale.set(size / Math.max(width, height, 1));
|
sprite.scale.set(size / Math.max(width, height, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A padlock (undiscovered location), `size` px tall, centred on `at`. */
|
||||||
|
function drawLock(g: Graphics, at: MapPoint, size: number) {
|
||||||
|
const w = size * 0.75;
|
||||||
|
const h = size * 0.55;
|
||||||
|
const top = at.y - size / 2 + size * 0.45;
|
||||||
|
// moveTo first: a bare arc would join on from the previous path point.
|
||||||
|
g.moveTo(at.x - w * 0.3, top)
|
||||||
|
.arc(at.x, top, w * 0.3, Math.PI, 0)
|
||||||
|
.stroke({ width: size * 0.14, color: colors.lock });
|
||||||
|
g.roundRect(at.x - w / 2, top, w, h, size * 0.12)
|
||||||
|
.fill(colors.lock)
|
||||||
|
.stroke({ width: 1.5, color: colors.labelHalo });
|
||||||
|
g.circle(at.x, top + h * 0.45, size * 0.08).fill(colors.labelHalo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The HQ (bay) marker: a small house, `size` px, centred on `at`. */
|
||||||
|
function drawHq(g: Graphics, at: MapPoint, size: number) {
|
||||||
|
const s = size / 2;
|
||||||
|
g.poly([
|
||||||
|
at.x - s,
|
||||||
|
at.y,
|
||||||
|
at.x,
|
||||||
|
at.y - s,
|
||||||
|
at.x + s,
|
||||||
|
at.y,
|
||||||
|
at.x + s * 0.7,
|
||||||
|
at.y,
|
||||||
|
at.x + s * 0.7,
|
||||||
|
at.y + s,
|
||||||
|
at.x - s * 0.7,
|
||||||
|
at.y + s,
|
||||||
|
at.x - s * 0.7,
|
||||||
|
at.y,
|
||||||
|
])
|
||||||
|
.fill(colors.hq)
|
||||||
|
.stroke({ width: 1, color: colors.labelHalo });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the map with PixiJS (Steps 10.1–10.2, CR-003). Positions and sizes come
|
* Draws the map with PixiJS (Steps 10.1–10.2, CR-003). Positions and sizes come
|
||||||
* from `layoutScene`; sprites and text are created once per item and updated
|
* from `layoutScene`; sprites and text are created once per item and updated
|
||||||
|
|
@ -32,11 +71,14 @@ export class MapRenderer {
|
||||||
private readonly sprites = new Map<string, Sprite>();
|
private readonly sprites = new Map<string, Sprite>();
|
||||||
private readonly labels = new Map<string, Text>();
|
private readonly labels = new Map<string, Text>();
|
||||||
private readonly badges = new Map<string, Text>();
|
private readonly badges = new Map<string, Text>();
|
||||||
|
/** One shared filter for every undiscovered location's sprite. */
|
||||||
|
private readonly desaturate = new ColorMatrixFilter();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stage: Container,
|
stage: Container,
|
||||||
private readonly textures: ArtTextures<Texture>,
|
private readonly textures: ArtTextures<Texture>,
|
||||||
) {
|
) {
|
||||||
|
this.desaturate.desaturate();
|
||||||
const bg = textures.get("bg");
|
const bg = textures.get("bg");
|
||||||
this.background = bg ? new Sprite({ texture: bg }) : null;
|
this.background = bg ? new Sprite({ texture: bg }) : null;
|
||||||
if (this.background) stage.addChild(this.background);
|
if (this.background) stage.addChild(this.background);
|
||||||
|
|
@ -124,19 +166,25 @@ export class MapRenderer {
|
||||||
this.overlay.circle(sun.x, sun.y, 7).fill(colors.sun),
|
this.overlay.circle(sun.x, sun.y, 7).fill(colors.sun),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const overlays = new Map(locationOverlays(scene).map((o) => [o.id, o]));
|
||||||
for (const location of scene.locations) {
|
for (const location of scene.locations) {
|
||||||
const { center: p, size } = placed.get(location.id)!;
|
const { center: p, size } = placed.get(location.id)!;
|
||||||
const placeholderColor = location.discovered
|
const state = overlays.get(location.id)!;
|
||||||
? colors.placeholderBody
|
const placeholderColor = state.locked ? colors.labelUndiscovered : colors.placeholderBody;
|
||||||
: colors.labelUndiscovered;
|
const sprite = this.place(
|
||||||
this.place(
|
|
||||||
`loc:${location.id}`,
|
`loc:${location.id}`,
|
||||||
visuals.locations.get(location.id)!,
|
visuals.locations.get(location.id)!,
|
||||||
this.bodies,
|
this.bodies,
|
||||||
p,
|
p,
|
||||||
size,
|
size,
|
||||||
() => this.overlay.circle(p.x, p.y, 5).fill(placeholderColor),
|
() =>
|
||||||
|
this.overlay.circle(p.x, p.y, 5).fill({ color: placeholderColor, alpha: state.alpha }),
|
||||||
);
|
);
|
||||||
|
// Undiscovered: desaturated and dimmed (Step 10C.7).
|
||||||
|
if (sprite) {
|
||||||
|
sprite.filters = state.desaturate ? [this.desaturate] : [];
|
||||||
|
sprite.alpha = state.alpha;
|
||||||
|
}
|
||||||
|
|
||||||
// Location badge art (Jupiter's cloud platform) on the body's edge, if provided.
|
// Location badge art (Jupiter's cloud platform) on the body's edge, if provided.
|
||||||
const badgeArt = visuals.badges.get(location.id);
|
const badgeArt = visuals.badges.get(location.id);
|
||||||
|
|
@ -144,12 +192,18 @@ export class MapRenderer {
|
||||||
const edge = polar(size / 2, mapStyle.locationBadge.angleDeg);
|
const edge = polar(size / 2, mapStyle.locationBadge.angleDeg);
|
||||||
const at = { x: p.x + edge.x, y: p.y + edge.y };
|
const at = { x: p.x + edge.x, y: p.y + edge.y };
|
||||||
const badgeSize = mapStyle.locationBadge.sizePx * layout.scale;
|
const badgeSize = mapStyle.locationBadge.sizePx * layout.scale;
|
||||||
this.place(`badge:${location.id}`, badgeArt, this.bodies, at, badgeSize, () => {});
|
const art = this.place(
|
||||||
|
`badge:${location.id}`,
|
||||||
|
badgeArt,
|
||||||
|
this.bodies,
|
||||||
|
at,
|
||||||
|
badgeSize,
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
if (art) art.alpha = state.alpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (location.hasBay) {
|
if (state.locked) drawLock(this.overlay, p, mapStyle.lockSizePx);
|
||||||
this.overlay.circle(p.x, p.y, size / 2 + 4).stroke({ width: 2, color: colors.hq });
|
|
||||||
}
|
|
||||||
|
|
||||||
// The name under the body, with a dark halo for contrast on the background.
|
// The name under the body, with a dark halo for contrast on the background.
|
||||||
const label = this.text(this.labels, location.id, () => {
|
const label = this.text(this.labels, location.id, () => {
|
||||||
|
|
@ -165,11 +219,9 @@ export class MapRenderer {
|
||||||
text.anchor.set(0.5, 0);
|
text.anchor.set(0.5, 0);
|
||||||
return text;
|
return text;
|
||||||
});
|
});
|
||||||
// Undiscovered locations are locked until a delivery reaches them (Step 10A.16).
|
label.style.fill = state.locked ? colors.labelUndiscovered : colors.label;
|
||||||
label.text = location.discovered ? location.name : `🔒 ${location.name}`;
|
|
||||||
label.style.fill = location.discovered ? colors.label : colors.labelUndiscovered;
|
|
||||||
|
|
||||||
// Idle ships here: a count badge just off the body's edge (Step 10.2).
|
// Idle ships here: a count badge just off the body's edge; never single sprites.
|
||||||
const badge = this.text(this.badges, location.id, () => {
|
const badge = this.text(this.badges, location.id, () => {
|
||||||
const text = new Text({
|
const text = new Text({
|
||||||
text: "",
|
text: "",
|
||||||
|
|
@ -182,17 +234,23 @@ export class MapRenderer {
|
||||||
text.anchor.set(0.5);
|
text.anchor.set(0.5);
|
||||||
return text;
|
return text;
|
||||||
});
|
});
|
||||||
badge.visible = location.idleShips > 0;
|
badge.visible = state.idleBadge !== null;
|
||||||
badge.text = String(location.idleShips);
|
badge.text = String(state.idleBadge ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels below (or above) bodies, badges on a clear diagonal (Step 10C.6).
|
// Labels below (or above) bodies, badges on a clear diagonal (Step 10C.6), and
|
||||||
|
// the HQ marker on the far side of the label from its body (Step 10C.7).
|
||||||
const decorations = decorate(layout, (id) => this.labels.get(id)!);
|
const decorations = decorate(layout, (id) => this.labels.get(id)!);
|
||||||
for (const location of scene.locations) {
|
for (const location of scene.locations) {
|
||||||
const { label, badge } = decorations.get(location.id)!;
|
const { label, badge } = decorations.get(location.id)!;
|
||||||
const text = this.labels.get(location.id)!;
|
const text = this.labels.get(location.id)!;
|
||||||
text.anchor.set(0.5, label.anchorY);
|
text.anchor.set(0.5, label.anchorY);
|
||||||
text.position.set(label.x, label.y);
|
text.position.set(label.x, label.y);
|
||||||
|
if (overlays.get(location.id)!.hq) {
|
||||||
|
const gap = 3 + mapStyle.hqSizePx / 2;
|
||||||
|
const y = label.anchorY === 0 ? label.y + text.height + gap : label.y - text.height - gap;
|
||||||
|
drawHq(this.overlay, { x: label.x, y }, mapStyle.hqSizePx);
|
||||||
|
}
|
||||||
const count = this.badges.get(location.id)!;
|
const count = this.badges.get(location.id)!;
|
||||||
if (count.visible) {
|
if (count.visible) {
|
||||||
this.overlay.circle(badge.x, badge.y, mapStyle.idleBadge.radiusPx).fill(colors.badgeFill);
|
this.overlay.circle(badge.x, badge.y, mapStyle.idleBadge.radiusPx).fill(colors.badgeFill);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ export const mapStyleSchema = z
|
||||||
idleBadge: offset.extend({ radiusPx: px, fontSizePx: px }),
|
idleBadge: offset.extend({ radiusPx: px, fontSizePx: px }),
|
||||||
/** Location badge art (e.g. Jupiter's cloud platform), on the body's edge. */
|
/** Location badge art (e.g. Jupiter's cloud platform), on the body's edge. */
|
||||||
locationBadge: z.object({ sizePx: px, angleDeg: z.number() }),
|
locationBadge: z.object({ sizePx: px, angleDeg: z.number() }),
|
||||||
|
/** Opacity of an undiscovered location's (desaturated) sprite. */
|
||||||
|
undiscoveredAlpha: z.number().min(0).max(1),
|
||||||
|
/** Code-drawn lock (undiscovered) and HQ marker sizes, px. */
|
||||||
|
lockSizePx: px,
|
||||||
|
hqSizePx: px,
|
||||||
/** No tap target is smaller than this (px, square). */
|
/** No tap target is smaller than this (px, square). */
|
||||||
minTapPx: px,
|
minTapPx: px,
|
||||||
/** Background moves this fraction of the camera pan (decision 0.5.40). */
|
/** Background moves this fraction of the camera pan (decision 0.5.40). */
|
||||||
|
|
@ -84,6 +89,9 @@ export const mapStyle: MapStyle = mapStyleSchema.parse({
|
||||||
childOffsets: { moon: { parentId: "earth", distancePx: 46, angleDeg: -30 } },
|
childOffsets: { moon: { parentId: "earth", distancePx: 46, angleDeg: -30 } },
|
||||||
idleBadge: { distancePx: 2, angleDeg: -135, radiusPx: 8, fontSizePx: 10 },
|
idleBadge: { distancePx: 2, angleDeg: -135, radiusPx: 8, fontSizePx: 10 },
|
||||||
locationBadge: { sizePx: 16, angleDeg: -45 },
|
locationBadge: { sizePx: 16, angleDeg: -45 },
|
||||||
|
undiscoveredAlpha: 0.55,
|
||||||
|
lockSizePx: 12,
|
||||||
|
hqSizePx: 9,
|
||||||
minTapPx: 44,
|
minTapPx: 44,
|
||||||
parallax: 0.15,
|
parallax: 0.15,
|
||||||
backgroundMargin: 0.1,
|
backgroundMargin: 0.1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import type { Ship } from "@deadhead/schema";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { locationOverlays } from "../src/map/overlays";
|
||||||
|
import { buildScene } from "../src/map/scene";
|
||||||
|
import { mapStyle } from "../src/map/style";
|
||||||
|
import { samplePlayerState } from "./render";
|
||||||
|
|
||||||
|
const NOW = Date.parse("2100-01-01T00:00:00Z");
|
||||||
|
const idleAtMoon = (id: string): Ship => ({
|
||||||
|
id,
|
||||||
|
modelId: "courier",
|
||||||
|
name: id,
|
||||||
|
status: "idle",
|
||||||
|
locationId: "moon",
|
||||||
|
dispatchId: null,
|
||||||
|
pilotId: null,
|
||||||
|
lastArrivalAt: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** After the deal: an Earth bay, two idle ships at the Moon, Mars and beyond undiscovered. */
|
||||||
|
const scene = () =>
|
||||||
|
buildScene(
|
||||||
|
samplePlayerState({
|
||||||
|
tutorial: null,
|
||||||
|
bays: [{ id: "b1", locationId: "earth" }],
|
||||||
|
ships: [idleAtMoon("s1"), idleAtMoon("s2")],
|
||||||
|
}),
|
||||||
|
NOW,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("location state overlays (Step 10C.7)", () => {
|
||||||
|
it("desaturates, dims and locks undiscovered locations only", () => {
|
||||||
|
const overlays = new Map(locationOverlays(scene()).map((o) => [o.id, o]));
|
||||||
|
for (const id of ["mars", "ceres-station", "jupiter-cloud-platform"]) {
|
||||||
|
expect(overlays.get(id), id).toMatchObject({
|
||||||
|
locked: true,
|
||||||
|
desaturate: true,
|
||||||
|
alpha: mapStyle.undiscoveredAlpha,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const id of ["earth", "moon"]) {
|
||||||
|
expect(overlays.get(id), id).toMatchObject({ locked: false, desaturate: false, alpha: 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks bay locations as HQs", () => {
|
||||||
|
const hqs = locationOverlays(scene())
|
||||||
|
.filter((o) => o.hq)
|
||||||
|
.map((o) => o.id);
|
||||||
|
expect(hqs).toEqual(["earth"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows two idle ships as one badge reading 2, and no single ship sprites", () => {
|
||||||
|
const s = scene();
|
||||||
|
const moon = locationOverlays(s).find((o) => o.id === "moon")!;
|
||||||
|
expect(moon.idleBadge).toBe(2);
|
||||||
|
expect(locationOverlays(s).find((o) => o.id === "earth")!.idleBadge).toBeNull();
|
||||||
|
// Ships are only drawn for flights; idle ships have none.
|
||||||
|
expect(s.flights).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 764 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 762 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
|
|
@ -455,6 +455,7 @@ Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In developmen
|
||||||
| `src/map/style.ts` | Map presentation config (CR-003): sprite sizes, zoom scale limits, child-body offsets, badges, tap size, parallax, colors; validated by `mapStyleSchema` |
|
| `src/map/style.ts` | Map presentation config (CR-003): sprite sizes, zoom scale limits, child-body offsets, badges, tap size, parallax, colors; validated by `mapStyleSchema` |
|
||||||
| `src/map/art/load.ts`, `art/visuals.ts` | Load the generated art as one PixiJS bundle (2x at DPR ≥ 2); texture-or-placeholder per scene item (decision 0.5.41) |
|
| `src/map/art/load.ts`, `art/visuals.ts` | Load the generated art as one PixiJS bundle (2x at DPR ≥ 2); texture-or-placeholder per scene item (decision 0.5.41) |
|
||||||
| `src/map/layout.ts` | Pure screen layout: `backgroundRect`, `spriteScale`, `childOffset`, `layoutScene` (centres, sizes, tap targets; used by renderer and hit test), `decorate` (label and badge placement) |
|
| `src/map/layout.ts` | Pure screen layout: `backgroundRect`, `spriteScale`, `childOffset`, `layoutScene` (centres, sizes, tap targets; used by renderer and hit test), `decorate` (label and badge placement) |
|
||||||
|
| `src/map/overlays.ts` | Per-location state overlays (lock, desaturate/dim, HQ marker, idle badge), applied by the renderer |
|
||||||
| `src/map/renderer.ts` | `MapRenderer`: all PixiJS drawing for the map, layer by layer; `MapCanvas` owns the app, input and loading state |
|
| `src/map/renderer.ts` | `MapRenderer`: all PixiJS drawing for the map, layer by layer; `MapCanvas` owns the app, input and loading state |
|
||||||
| `src/screens/*` | HQ, Contracts (TutorialBoard / LocationBoards + DispatchSheet), Fleet, Pilots, Market, Locations, StartScreen |
|
| `src/screens/*` | HQ, Contracts (TutorialBoard / LocationBoards + DispatchSheet), Fleet, Pilots, Market, Locations, StartScreen |
|
||||||
| `src/components/*` | `AwaySummary` dialog, `Countdown` |
|
| `src/components/*` | `AwaySummary` dialog, `Countdown` |
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ Spec: `memory-bank/change-requests/CR-003-map-art.md`. New decisions recorded as
|
||||||
- [x] **Step 10C.4 — Asset loading and fallback.** `map/art/load.ts`: `artBundle(dpr)` (2x at DPR ≥ 2) registered once as PixiJS bundle `map-art`, each asset loaded separately so a failure only costs that asset (one warning, placeholder). `map/art/visuals.ts` picks texture or placeholder per item; the scene now carries sprite keys and each flight's ship model (loaner → Company Courier). Drawing moved from `MapCanvas` into `map/renderer.ts` (`MapRenderer`); the map shows "Loading map…" until art and renderer are ready. Test: `map-art-loading.test.ts`.
|
- [x] **Step 10C.4 — Asset loading and fallback.** `map/art/load.ts`: `artBundle(dpr)` (2x at DPR ≥ 2) registered once as PixiJS bundle `map-art`, each asset loaded separately so a failure only costs that asset (one warning, placeholder). `map/art/visuals.ts` picks texture or placeholder per item; the scene now carries sprite keys and each flight's ship model (loaner → Company Courier). Drawing moved from `MapCanvas` into `map/renderer.ts` (`MapRenderer`); the map shows "Loading map…" until art and renderer are ready. Test: `map-art-loading.test.ts`.
|
||||||
- [x] **Step 10C.5 — Background.** `map/layout.ts` `backgroundRect`: cover viewport + 10% margin each side, zoom follow `zoom^0.05` (never below cover), pan × 0.15 parallax, clamped so no edge shows; drawn as one sprite under everything. The Map screen now fills the space down to the nav (it stopped 42 px short). Test: `map-layout.test.ts`. **Manual check:** `docs/screenshots/10c5-background-tutorial-{desktop,phone}{,-panned}.png`.
|
- [x] **Step 10C.5 — Background.** `map/layout.ts` `backgroundRect`: cover viewport + 10% margin each side, zoom follow `zoom^0.05` (never below cover), pan × 0.15 parallax, clamped so no edge shows; drawn as one sprite under everything. The Map screen now fills the space down to the nav (it stopped 42 px short). Test: `map-layout.test.ts`. **Manual check:** `docs/screenshots/10c5-background-tutorial-{desktop,phone}{,-panned}.png`.
|
||||||
- [x] **Step 10C.6 — Bodies, orbits, and labels.** `layoutScene` (map/layout.ts) gives every body, the Sun and each flight a screen centre, size (base × `spriteScale(zoom)` = zoom^0.35 clamped 0.75–1.75) and tap size (≥ 44 px); hit-testing uses it. The Moon is drawn 46 px from Earth at −30° from the Sun→Earth direction, with a faint ring around Earth; Moon routes start and end there. `decorate` places labels (bold, dark halo) below or above to avoid bodies and other labels, and idle badges on a clear diagonal. Jupiter's badge art is drawn on its edge when provided (not yet). Tests: `map-layout.test.ts`. **Manual check:** `docs/screenshots/10c6-bodies-{tutorial,midgame}-{desktop,phone}.png`.
|
- [x] **Step 10C.6 — Bodies, orbits, and labels.** `layoutScene` (map/layout.ts) gives every body, the Sun and each flight a screen centre, size (base × `spriteScale(zoom)` = zoom^0.35 clamped 0.75–1.75) and tap size (≥ 44 px); hit-testing uses it. The Moon is drawn 46 px from Earth at −30° from the Sun→Earth direction, with a faint ring around Earth; Moon routes start and end there. `decorate` places labels (bold, dark halo) below or above to avoid bodies and other labels, and idle badges on a clear diagonal. Jupiter's badge art is drawn on its edge when provided (not yet). Tests: `map-layout.test.ts`. **Manual check:** `docs/screenshots/10c6-bodies-{tutorial,midgame}-{desktop,phone}.png`.
|
||||||
- [ ] Step 10C.7 — Location state overlays
|
- [x] **Step 10C.7 — Location state overlays.** `map/overlays.ts` `locationOverlays(scene)` decides per location: undiscovered → desaturated (one shared `ColorMatrixFilter`), alpha 0.55 and a code-drawn padlock (replaces the 🔒 label of Step 10A.16; the tap panel is unchanged); bay → a small green house marker on the far side of the label; idle ships → count badge only. Test: `map-overlays.test.ts`. **Manual check:** `docs/screenshots/10c7-overlays-{tutorial,midgame}-{desktop,phone}.png`.
|
||||||
- [ ] Step 10C.8 — Ships in flight
|
- [ ] Step 10C.8 — Ships in flight
|
||||||
- [ ] Step 10C.9 — Performance
|
- [ ] Step 10C.9 — Performance
|
||||||
- [ ] Step 10C.10 — Plan amendments and regression
|
- [ ] Step 10C.10 — Plan amendments and regression
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue