Step 10C.3: map style config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f239f452e7
commit
5612788c94
|
|
@ -695,3 +695,10 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
- **The committed-output test compares decoded pixels, not bytes**, because PNG compression can differ between Windows and Linux (CI). The "twice is identical" test compares bytes within one machine. The source-transparency test reads all four corners of each source sprite.
|
- **The committed-output test compares decoded pixels, not bytes**, because PNG compression can differ between Windows and Linux (CI). The "twice is identical" test compares bytes within one machine. The source-transparency test reads all four corners of each source sprite.
|
||||||
- **Sizes:** the 2x body sprites are about 0.5–0.6 MB each (lossless PNG of detailed art). That is acceptable for now. If the PWA precache (Step 11.1) is too heavy, palette quantization is the lever.
|
- **Sizes:** the 2x body sprites are about 0.5–0.6 MB each (lossless PNG of detailed art). That is acceptable for now. If the PWA precache (Step 11.1) is too heavy, palette quantization is the lever.
|
||||||
- **sharp's install script is not approved** in pnpm; its prebuilt platform binaries load without it.
|
- **sharp's install script is not approved** in pnpm; its prebuilt platform binaries load without it.
|
||||||
|
|
||||||
|
## Step 10C.3
|
||||||
|
|
||||||
|
- **Angles are degrees clockwise from screen right** (screen y grows down), so −45° is up and to the right. The Moon sits 30 px up-right of Earth; the idle-ship badge sits 20 px down-left (135°) of a body's center; Jupiter's platform badge sits on its edge at −45°.
|
||||||
|
- **Child offsets are keyed by location id with a `parentId`**, so a future moon only needs a config entry (decision 0.5.39).
|
||||||
|
- **The background follows map zoom by 5%** (`backgroundZoomFollow`), the "zooms only slightly" option in Step 10C.5.
|
||||||
|
- **Placeholder colors are in the style too**, so the fallback (decision 0.5.41) draws with the same palette as before.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map presentation settings (CR-003): sizes in screen pixels, offsets, and
|
||||||
|
* colors. These are not game numbers, so they live here, not in
|
||||||
|
* `@deadhead/content`. Angles are degrees clockwise from screen right.
|
||||||
|
*/
|
||||||
|
const px = z.number().positive();
|
||||||
|
const color = z.number().int().min(0).max(0xffffff);
|
||||||
|
const offset = z.object({ distancePx: px, angleDeg: z.number() });
|
||||||
|
|
||||||
|
export const mapStyleSchema = z
|
||||||
|
.object({
|
||||||
|
/** On-screen base size (long side, px) per sprite asset id (decision 0.5.38). */
|
||||||
|
spriteSizePx: z.record(z.string(), px),
|
||||||
|
/** Every ship sprite's base size (long side, px). */
|
||||||
|
shipSizePx: px,
|
||||||
|
/** Sprite size factor limits as the player zooms (decision 0.5.38). */
|
||||||
|
spriteScale: z.object({ min: px, max: px }),
|
||||||
|
/** Child bodies drawn at a fixed screen offset from their parent (decision 0.5.39). */
|
||||||
|
childOffsets: z.record(z.string(), offset.extend({ parentId: z.string() })),
|
||||||
|
/** Idle-ship count badge, from the body's center. */
|
||||||
|
idleBadge: offset.extend({ radiusPx: px, fontSizePx: px }),
|
||||||
|
/** Location badge art (e.g. Jupiter's cloud platform), on the body's edge. */
|
||||||
|
locationBadge: z.object({ sizePx: px, angleDeg: z.number() }),
|
||||||
|
/** No tap target is smaller than this (px, square). */
|
||||||
|
minTapPx: px,
|
||||||
|
/** Background moves this fraction of the camera pan (decision 0.5.40). */
|
||||||
|
parallax: z.number().min(0).max(1),
|
||||||
|
/** Background covers the viewport plus this fraction of it on every side. */
|
||||||
|
backgroundMargin: z.number().min(0).max(1),
|
||||||
|
/** Background zoom follows map zoom by this fraction (0 = never zooms). */
|
||||||
|
backgroundZoomFollow: z.number().min(0).max(1),
|
||||||
|
label: z.object({ fontSizePx: px, gapPx: z.number().min(0), haloPx: z.number().min(0) }),
|
||||||
|
/** Gap kept between in-flight ships that would otherwise overlap (Step 10C.8). */
|
||||||
|
shipSeparationPx: px,
|
||||||
|
colors: z.object({
|
||||||
|
background: color,
|
||||||
|
orbit: color,
|
||||||
|
orbitAlpha: z.number().min(0).max(1),
|
||||||
|
routeBehind: color,
|
||||||
|
routeAhead: color,
|
||||||
|
label: color,
|
||||||
|
labelUndiscovered: color,
|
||||||
|
labelHalo: color,
|
||||||
|
hq: color,
|
||||||
|
badgeFill: color,
|
||||||
|
badgeText: color,
|
||||||
|
selection: color,
|
||||||
|
lock: color,
|
||||||
|
placeholderBody: color,
|
||||||
|
placeholderShip: color,
|
||||||
|
sun: color,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((s) => s.spriteScale.min <= s.spriteScale.max, {
|
||||||
|
message: "spriteScale.min must not exceed spriteScale.max",
|
||||||
|
path: ["spriteScale"],
|
||||||
|
});
|
||||||
|
export type MapStyle = z.infer<typeof mapStyleSchema>;
|
||||||
|
|
||||||
|
export const mapStyle: MapStyle = mapStyleSchema.parse({
|
||||||
|
spriteSizePx: {
|
||||||
|
sun: 64,
|
||||||
|
"body-jupiter": 48,
|
||||||
|
"body-earth": 32,
|
||||||
|
"body-mars": 32,
|
||||||
|
"body-ceres": 22,
|
||||||
|
"body-moon": 18,
|
||||||
|
},
|
||||||
|
shipSizePx: 24,
|
||||||
|
spriteScale: { min: 0.75, max: 1.75 },
|
||||||
|
childOffsets: { moon: { parentId: "earth", distancePx: 30, angleDeg: -45 } },
|
||||||
|
idleBadge: { distancePx: 20, angleDeg: 135, radiusPx: 8, fontSizePx: 10 },
|
||||||
|
locationBadge: { sizePx: 16, angleDeg: -45 },
|
||||||
|
minTapPx: 44,
|
||||||
|
parallax: 0.15,
|
||||||
|
backgroundMargin: 0.1,
|
||||||
|
backgroundZoomFollow: 0.05,
|
||||||
|
label: { fontSizePx: 11, gapPx: 4, haloPx: 3 },
|
||||||
|
shipSeparationPx: 44,
|
||||||
|
colors: {
|
||||||
|
background: 0x020617,
|
||||||
|
orbit: 0x94a3b8,
|
||||||
|
orbitAlpha: 0.18,
|
||||||
|
routeBehind: 0x38bdf8,
|
||||||
|
routeAhead: 0x7dd3fc,
|
||||||
|
label: 0xe2e8f0,
|
||||||
|
labelUndiscovered: 0x94a3b8,
|
||||||
|
labelHalo: 0x020617,
|
||||||
|
hq: 0x34d399,
|
||||||
|
badgeFill: 0xfbbf24,
|
||||||
|
badgeText: 0x020617,
|
||||||
|
selection: 0xfbbf24,
|
||||||
|
lock: 0xcbd5e1,
|
||||||
|
placeholderBody: 0xe2e8f0,
|
||||||
|
placeholderShip: 0xfbbf24,
|
||||||
|
sun: 0xfbbf24,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { mapStyle, mapStyleSchema } from "../src/map/style";
|
||||||
|
|
||||||
|
describe("map style config (CR-003)", () => {
|
||||||
|
it("validates", () => {
|
||||||
|
expect(mapStyleSchema.safeParse(mapStyle).success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-positive sizes, min scale above max, and parallax outside 0–1", () => {
|
||||||
|
const bad = [
|
||||||
|
{ ...mapStyle, shipSizePx: 0 },
|
||||||
|
{ ...mapStyle, spriteSizePx: { ...mapStyle.spriteSizePx, sun: -1 } },
|
||||||
|
{ ...mapStyle, minTapPx: 0 },
|
||||||
|
{ ...mapStyle, spriteScale: { min: 2, max: 1 } },
|
||||||
|
{ ...mapStyle, parallax: 1.5 },
|
||||||
|
{ ...mapStyle, parallax: -0.1 },
|
||||||
|
];
|
||||||
|
for (const style of bad) expect(mapStyleSchema.safeParse(style).success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sizes every body sprite and keeps tap targets at least 44 px", () => {
|
||||||
|
for (const id of [
|
||||||
|
"sun",
|
||||||
|
"body-earth",
|
||||||
|
"body-moon",
|
||||||
|
"body-mars",
|
||||||
|
"body-ceres",
|
||||||
|
"body-jupiter",
|
||||||
|
]) {
|
||||||
|
expect(mapStyle.spriteSizePx[id], id).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
expect(mapStyle.minTapPx).toBeGreaterThanOrEqual(44);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -452,6 +452,7 @@ Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In developmen
|
||||||
| `src/routes.ts` | Screen list shared by router and navigation |
|
| `src/routes.ts` | Screen list shared by router and navigation |
|
||||||
| `src/map/art/manifest.json` | Map art manifest (CR-003): asset id → `{ kind, file }` in `public/assets/map/` (never edited); `null` file = missing, placeholder used |
|
| `src/map/art/manifest.json` | Map art manifest (CR-003): asset id → `{ kind, file }` in `public/assets/map/` (never edited); `null` file = missing, placeholder used |
|
||||||
| `scripts/map-art.mjs` | Generates `public/assets/map-gen/` (`<id>@1x.png`, `<id>@2x.png`, `bg.webp`) from the manifest with sharp; output committed |
|
| `scripts/map-art.mjs` | Generates `public/assets/map-gen/` (`<id>@1x.png`, `<id>@2x.png`, `bg.webp`) from the manifest with sharp; output committed |
|
||||||
|
| `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/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` |
|
||||||
| `src/state/actions.ts` | `useLoadedPlayer`, `useGameAction(route)` (mutation + invalidate state and boards) |
|
| `src/state/actions.ts` | `useLoadedPlayer`, `useGameAction(route)` (mutation + invalidate state and boards) |
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ Spec: `memory-bank/change-requests/CR-003-map-art.md`. New decisions recorded as
|
||||||
|
|
||||||
- [x] **Step 10C.1 — Inventory and asset manifest.** `apps/web/src/map/art/manifest.json` maps 12 asset ids to the 11 files in `apps/web/public/assets/map/` (all 1254×1254; sprites RGBA). `badge-cloud-platform` is listed with `file: null` (not provided). Content: `sprite` on every location and ship model, `badge` on Jupiter Cloud Platform. Tests: `art-manifest.test.ts`, content sprite-key tests.
|
- [x] **Step 10C.1 — Inventory and asset manifest.** `apps/web/src/map/art/manifest.json` maps 12 asset ids to the 11 files in `apps/web/public/assets/map/` (all 1254×1254; sprites RGBA). `badge-cloud-platform` is listed with `file: null` (not provided). Content: `sprite` on every location and ship model, `badge` on Jupiter Cloud Platform. Tests: `art-manifest.test.ts`, content sprite-key tests.
|
||||||
- [x] **Step 10C.2 — Asset optimization script.** `apps/web/scripts/map-art.mjs` (sharp; `pnpm --filter @deadhead/web map-art`, and first in `build` with `--if-available`) writes 21 files to `public/assets/map-gen/` (committed): bodies and Sun 256/512 px, ships 128/256 px (trimmed), `bg.webp` 1254 px / 106 KB. The Vite build drops the source art from `dist`. Test: `map-art.test.ts` (sizes, byte-identical reruns, pixels match the committed output, bg < 1 MB, transparent source corners).
|
- [x] **Step 10C.2 — Asset optimization script.** `apps/web/scripts/map-art.mjs` (sharp; `pnpm --filter @deadhead/web map-art`, and first in `build` with `--if-available`) writes 21 files to `public/assets/map-gen/` (committed): bodies and Sun 256/512 px, ships 128/256 px (trimmed), `bg.webp` 1254 px / 106 KB. The Vite build drops the source art from `dist`. Test: `map-art.test.ts` (sizes, byte-identical reruns, pixels match the committed output, bg < 1 MB, transparent source corners).
|
||||||
- [ ] Step 10C.3 — Map style config
|
- [x] **Step 10C.3 — Map style config.** `apps/web/src/map/style.ts`: `mapStyleSchema` (Zod) and `mapStyle` with sprite sizes (Sun 64, Jupiter 48, Earth/Mars 32, Ceres 22, Moon 18, ships 24 px), sprite scale 0.75–1.75×, Moon offset 30 px at −45° from Earth, idle and location badge offsets, 44 px minimum tap, parallax 0.15, background margin 10%, label and colors. Test: `map-style.test.ts`.
|
||||||
- [ ] Step 10C.4 — Asset loading and fallback
|
- [ ] Step 10C.4 — Asset loading and fallback
|
||||||
- [ ] Step 10C.5 — Background
|
- [ ] Step 10C.5 — Background
|
||||||
- [ ] Step 10C.6 — Bodies, orbits, and labels
|
- [ ] Step 10C.6 — Bodies, orbits, and labels
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue