Step 10C.1: map art manifest and content sprite keys
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
23b40a4324
commit
102c5bba91
|
|
@ -678,3 +678,11 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
|
||||
- **Change request CR-003 is stored in `memory-bank/change-requests/`** and inserted as Phase 10C, after Phase 10B and before Phase 11.
|
||||
- **Decision numbering:** the CR's 0.5.29–0.5.33 are recorded as **0.5.37 Map art pipeline, 0.5.38 Constant screen size, 0.5.39 Child bodies, 0.5.40 Background, 0.5.41 Fallback**, since 29–36 were already in use.
|
||||
|
||||
## Step 10C.1
|
||||
|
||||
- **Inventory:** `bg.png`, `body-sun.png`, `body-earth.png`, `body-moon.png`, `body-mars.png`, `body-ceres.png`, `body-jupiter.png`, `ship-courier.png`, `ship-freighter.png`, `ship-bulk-hauler.png`, `ship-company-courier.png`. Every name matched exactly one asset id; nothing was ambiguous.
|
||||
- **The Sun's file is `body-sun.png`**; its asset id stays `sun` as in the CR.
|
||||
- **Missing: `badge-cloud-platform`** (the CR marks it optional). It is in the manifest with `file: null`, Jupiter Cloud Platform keeps `badge: "badge-cloud-platform"` in content, and the map simply draws no badge (fallback, decision 0.5.41).
|
||||
- **The manifest is JSON** (`apps/web/src/map/art/manifest.json`) with a `kind` per asset (`background`, `body`, `badge`, `ship`), so both the app (typed wrapper `manifest.ts`) and the Node optimization script read the same file.
|
||||
- **The Sun is not a location**, so its sprite key lives only in the manifest; the map style will refer to `sun` directly.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"sourceDir": "public/assets/map",
|
||||
"assets": {
|
||||
"bg": { "kind": "background", "file": "bg.png" },
|
||||
"sun": { "kind": "body", "file": "body-sun.png" },
|
||||
"body-earth": { "kind": "body", "file": "body-earth.png" },
|
||||
"body-moon": { "kind": "body", "file": "body-moon.png" },
|
||||
"body-mars": { "kind": "body", "file": "body-mars.png" },
|
||||
"body-ceres": { "kind": "body", "file": "body-ceres.png" },
|
||||
"body-jupiter": { "kind": "body", "file": "body-jupiter.png" },
|
||||
"badge-cloud-platform": { "kind": "badge", "file": null },
|
||||
"ship-courier": { "kind": "ship", "file": "ship-courier.png" },
|
||||
"ship-freighter": { "kind": "ship", "file": "ship-freighter.png" },
|
||||
"ship-bulk-hauler": { "kind": "ship", "file": "ship-bulk-hauler.png" },
|
||||
"ship-company-courier": { "kind": "ship", "file": "ship-company-courier.png" }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import manifestJson from "./manifest.json";
|
||||
|
||||
/**
|
||||
* Map art (CR-003, decision 0.5.37): logical asset id → source file in the
|
||||
* user's map assets folder. A `null` file means the asset is expected but
|
||||
* missing, so the map draws its placeholder (decision 0.5.41).
|
||||
*/
|
||||
export type ArtKind = "background" | "body" | "badge" | "ship";
|
||||
|
||||
export interface ArtManifest {
|
||||
/** Source folder, relative to `apps/web`. Never written to. */
|
||||
sourceDir: string;
|
||||
assets: Record<string, { kind: ArtKind; file: string | null }>;
|
||||
}
|
||||
|
||||
export const artManifest = manifestJson as ArtManifest;
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { content } from "@deadhead/content";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { artManifest } from "../src/map/art/manifest";
|
||||
|
||||
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
describe("map art manifest (CR-003)", () => {
|
||||
it("has an entry for every location's and ship model's sprite and badge", () => {
|
||||
const keys = [
|
||||
...content.locations.flatMap((l) => [l.sprite, ...(l.badge ? [l.badge] : [])]),
|
||||
...content.shipModels.map((m) => m.sprite),
|
||||
];
|
||||
for (const key of keys) expect(artManifest.assets, key).toHaveProperty([key]);
|
||||
expect(artManifest.assets).toHaveProperty(["bg"]);
|
||||
expect(artManifest.assets).toHaveProperty(["sun"]);
|
||||
});
|
||||
|
||||
it("names only files that exist in the source folder", () => {
|
||||
for (const [id, asset] of Object.entries(artManifest.assets)) {
|
||||
if (asset.file === null) continue;
|
||||
expect(
|
||||
existsSync(join(webRoot, artManifest.sourceDir, asset.file)),
|
||||
`${id}: ${asset.file}`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -450,6 +450,7 @@ Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In developmen
|
|||
| `src/App.tsx` | Clock sync; loading / start screen / routed layout |
|
||||
| `src/layout/Layout.tsx` | Header (credits, game clock), screen outlet, bottom tab bar |
|
||||
| `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/screens/*` | HQ, Contracts (TutorialBoard / LocationBoards + DispatchSheet), Fleet, Pilots, Market, Locations, StartScreen |
|
||||
| `src/components/*` | `AwaySummary` dialog, `Countdown` |
|
||||
| `src/state/actions.ts` | `useLoadedPlayer`, `useGameAction(route)` (mutation + invalidate state and boards) |
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ Spec: `memory-bank/change-requests/CR-002-open-markets.md`. New decisions record
|
|||
|
||||
Spec: `memory-bank/change-requests/CR-003-map-art.md`. New decisions recorded as plan §0.5.37–0.5.41.
|
||||
|
||||
- [ ] Step 10C.1 — Inventory and asset manifest
|
||||
- [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.
|
||||
- [ ] Step 10C.2 — Asset optimization script
|
||||
- [ ] Step 10C.3 — Map style config
|
||||
- [ ] Step 10C.4 — Asset loading and fallback
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const locations: Location[] = [
|
|||
type: "atmospheric-world",
|
||||
landingRequirement: "atmospheric",
|
||||
body: "earth",
|
||||
sprite: "body-earth",
|
||||
launchCostMultiplier: 3,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 1000,
|
||||
|
|
@ -24,6 +25,7 @@ export const locations: Location[] = [
|
|||
type: "airless-body",
|
||||
landingRequirement: "vacuum",
|
||||
body: "moon",
|
||||
sprite: "body-moon",
|
||||
launchCostMultiplier: 0.5,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 800,
|
||||
|
|
@ -36,6 +38,7 @@ export const locations: Location[] = [
|
|||
type: "atmospheric-world",
|
||||
landingRequirement: "atmospheric",
|
||||
body: "mars",
|
||||
sprite: "body-mars",
|
||||
launchCostMultiplier: 1.5,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 20000,
|
||||
|
|
@ -48,6 +51,7 @@ export const locations: Location[] = [
|
|||
type: "belt-station",
|
||||
landingRequirement: "vacuum",
|
||||
body: "ceres",
|
||||
sprite: "body-ceres",
|
||||
launchCostMultiplier: 0.3,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 40000,
|
||||
|
|
@ -60,6 +64,8 @@ export const locations: Location[] = [
|
|||
type: "cloud-platform",
|
||||
landingRequirement: "atmospheric",
|
||||
body: "jupiter",
|
||||
sprite: "body-jupiter",
|
||||
badge: "badge-cloud-platform",
|
||||
launchCostMultiplier: 2,
|
||||
startsDiscovered: false,
|
||||
bayLeasePrice: 80000,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { ShipModel } from "../schemas";
|
|||
export const shipModels: ShipModel[] = [
|
||||
{
|
||||
id: "courier",
|
||||
sprite: "ship-courier",
|
||||
name: "Courier",
|
||||
archetype: "courier",
|
||||
price: 4000,
|
||||
|
|
@ -23,6 +24,7 @@ export const shipModels: ShipModel[] = [
|
|||
},
|
||||
{
|
||||
id: "freighter",
|
||||
sprite: "ship-freighter",
|
||||
name: "Freighter",
|
||||
archetype: "freighter",
|
||||
price: 100000,
|
||||
|
|
@ -38,6 +40,7 @@ export const shipModels: ShipModel[] = [
|
|||
},
|
||||
{
|
||||
id: "bulk-hauler",
|
||||
sprite: "ship-bulk-hauler",
|
||||
name: "Bulk Hauler",
|
||||
archetype: "bulk-hauler",
|
||||
price: 400000,
|
||||
|
|
@ -55,6 +58,7 @@ export const shipModels: ShipModel[] = [
|
|||
// The company's tutorial loaner (decision 0.5.29): not for sale. Faster with
|
||||
// less overhead than the Courier, so a full Earth–Moon run is under a real minute.
|
||||
id: "company-courier",
|
||||
sprite: "ship-company-courier",
|
||||
name: "Company Courier",
|
||||
archetype: "courier",
|
||||
price: 4000,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ export const locationSchema = z.object({
|
|||
produces: z.array(idSchema),
|
||||
/** Cargo type ids. */
|
||||
demands: z.array(idSchema),
|
||||
/** Map art asset id (CR-003), e.g. `body-earth`; see the web app's art manifest. */
|
||||
sprite: idSchema,
|
||||
/** Optional map badge asset id drawn on the body's edge (CR-003). */
|
||||
badge: idSchema.optional(),
|
||||
});
|
||||
export type Location = z.infer<typeof locationSchema>;
|
||||
|
||||
|
|
@ -69,6 +73,8 @@ export const shipModelSchema = z.object({
|
|||
fuelCostPerMillionKm: z.number().nonnegative(),
|
||||
/** False for company-only ships such as the tutorial loaner (decision 0.5.29). */
|
||||
purchasable: z.boolean(),
|
||||
/** Map art asset id (CR-003), e.g. `ship-freighter`. */
|
||||
sprite: idSchema,
|
||||
});
|
||||
export type ShipModel = z.infer<typeof shipModelSchema>;
|
||||
|
||||
|
|
|
|||
|
|
@ -36,4 +36,10 @@ describe("locations", () => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("gives every location its own map sprite key (CR-003)", () => {
|
||||
const sprites = locations.map((x) => x.sprite);
|
||||
for (const sprite of sprites) expect(sprite).toMatch(/^[a-z][a-z-]*$/);
|
||||
expect(new Set(sprites).size).toBe(sprites.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const validLocation = {
|
|||
launchCostMultiplier: 3,
|
||||
startsDiscovered: true,
|
||||
bayLeasePrice: 5000,
|
||||
sprite: "body-mars",
|
||||
produces: ["standard"],
|
||||
demands: ["heavy"],
|
||||
};
|
||||
|
|
@ -36,6 +37,7 @@ const validShipModel = {
|
|||
overheadGameHours: 2,
|
||||
fuelCostPerMillionKm: 100,
|
||||
purchasable: true,
|
||||
sprite: "ship-courier",
|
||||
};
|
||||
|
||||
const validCargoType = {
|
||||
|
|
|
|||
|
|
@ -44,4 +44,10 @@ describe("ship models", () => {
|
|||
expect(ship("courier").price).toBeLessThan(ship("freighter").price);
|
||||
expect(ship("freighter").price).toBeLessThan(ship("bulk-hauler").price);
|
||||
});
|
||||
|
||||
it("gives every ship model its own map sprite key (CR-003)", () => {
|
||||
const sprites = shipModels.map((x) => x.sprite);
|
||||
for (const sprite of sprites) expect(sprite).toMatch(/^[a-z][a-z-]*$/);
|
||||
expect(new Set(sprites).size).toBe(sprites.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue