Compare commits
3 Commits
f7698166bb
...
f239f452e7
| Author | SHA1 | Date |
|---|---|---|
|
|
f239f452e7 | |
|
|
102c5bba91 | |
|
|
23b40a4324 |
22
DECISIONS.md
|
|
@ -673,3 +673,25 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
|
|
||||||
- **GDD §12 ("Each bay has one contract board") was also corrected**, since it contradicted §10; the CR named only §4 and §10.
|
- **GDD §12 ("Each bay has one contract board") was also corrected**, since it contradicted §10; the CR named only §4 and §10.
|
||||||
- **The manual check was also scripted** against a live dev server with the test clock: after the deal, a Courier flies Earth→Moon and then takes a Moon→Earth job from the Moon board with no Moon bay. The user's own manual check on a device is still the review gate.
|
- **The manual check was also scripted** against a live dev server with the test clock: after the deal, a Courier flies Earth→Moon and then takes a Moon→Earth job from the Moon board with no Moon bay. The user's own manual check on a device is still the review gate.
|
||||||
|
|
||||||
|
## CR-003 — Map Art (Phase 10C)
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
## Step 10C.2
|
||||||
|
|
||||||
|
- **Output naming:** `public/assets/map-gen/<id>@1x.png` and `<id>@2x.png` for sprites, `<id>.webp` for the background. Missing assets (`file: null`) produce nothing and a warning.
|
||||||
|
- **The build runs the script with `--if-available`:** it regenerates when sharp is installed (output is byte-identical, so git sees no change) and otherwise only checks that the committed output exists. Builds therefore never need sharp, as the CR requires.
|
||||||
|
- **The source art is removed from the build output** by a small Vite plugin. Everything in `public/` is copied into `dist` by default, and the sources (about 22 MB) are only script input. The sources stay where the user put them.
|
||||||
|
- **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.
|
||||||
|
- **sharp's install script is not approved** in pnpm; its prebuilt platform binaries load without it.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "node scripts/map-art.mjs --if-available && vite build",
|
||||||
|
"map-art": "node scripts/map-art.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
"@types/react-dom": "^19.3.0",
|
"@types/react-dom": "^19.3.0",
|
||||||
"@vitejs/plugin-react": "^6.1.1",
|
"@vitejs/plugin-react": "^6.1.1",
|
||||||
"jsdom": "^30.1.1",
|
"jsdom": "^30.1.1",
|
||||||
|
"sharp": "^0.35.5",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"vite": "^8.3.2"
|
"vite": "^8.3.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 474 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 615 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 612 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 570 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 585 KiB |
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Generates optimized map art from the user's source images (CR-003, decision 0.5.37).
|
||||||
|
//
|
||||||
|
// node scripts/map-art.mjs [outDir] regenerate (default public/assets/map-gen)
|
||||||
|
// node scripts/map-art.mjs --if-available as above, but if sharp is not installed,
|
||||||
|
// only check the committed output exists
|
||||||
|
//
|
||||||
|
// Sources in the manifest's `sourceDir` are only ever read.
|
||||||
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const manifest = JSON.parse(readFileSync(join(webRoot, "src/map/art/manifest.json"), "utf8"));
|
||||||
|
|
||||||
|
/** Long-side sizes in px for the 1x and 2x variants of each sprite kind. */
|
||||||
|
export const SPRITE_SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] };
|
||||||
|
export const BACKGROUND_MAX = 2048;
|
||||||
|
export const DEFAULT_OUT_DIR = join(webRoot, "public/assets/map-gen");
|
||||||
|
|
||||||
|
/** Every file the script writes for the manifest, relative to the output folder. */
|
||||||
|
export function expectedOutputs() {
|
||||||
|
const outputs = [];
|
||||||
|
for (const [id, asset] of Object.entries(manifest.assets)) {
|
||||||
|
if (asset.file === null) continue;
|
||||||
|
if (asset.kind === "background") outputs.push({ id, file: `${id}.webp`, kind: asset.kind });
|
||||||
|
else {
|
||||||
|
const [one, two] = SPRITE_SIZES[asset.kind];
|
||||||
|
outputs.push({ id, file: `${id}@1x.png`, kind: asset.kind, longSide: one });
|
||||||
|
outputs.push({ id, file: `${id}@2x.png`, kind: asset.kind, longSide: two });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const png = { compressionLevel: 9, adaptiveFiltering: false, palette: false };
|
||||||
|
|
||||||
|
async function generate(sharp, outDir) {
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
for (const [id, asset] of Object.entries(manifest.assets)) {
|
||||||
|
if (asset.file === null) {
|
||||||
|
console.warn(`map-art: ${id} has no source file; the map will use its placeholder`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const source = join(webRoot, manifest.sourceDir, asset.file);
|
||||||
|
if (asset.kind === "background") {
|
||||||
|
await sharp(source)
|
||||||
|
.resize({
|
||||||
|
width: BACKGROUND_MAX,
|
||||||
|
height: BACKGROUND_MAX,
|
||||||
|
fit: "inside",
|
||||||
|
withoutEnlargement: true,
|
||||||
|
})
|
||||||
|
.webp({ quality: 80, effort: 6 })
|
||||||
|
.toFile(join(outDir, `${id}.webp`));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Trim transparent padding once, then export each size from the trimmed image.
|
||||||
|
const trimmed = await sharp(source).trim().png().toBuffer();
|
||||||
|
const [one, two] = SPRITE_SIZES[asset.kind];
|
||||||
|
for (const [suffix, size] of [
|
||||||
|
["1x", one],
|
||||||
|
["2x", two],
|
||||||
|
]) {
|
||||||
|
await sharp(trimmed)
|
||||||
|
.resize({ width: size, height: size, fit: "inside" })
|
||||||
|
.png(png)
|
||||||
|
.toFile(join(outDir, `${id}@${suffix}.png`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(args) {
|
||||||
|
const ifAvailable = args.includes("--if-available");
|
||||||
|
const outDir = resolve(args.find((a) => !a.startsWith("--")) ?? DEFAULT_OUT_DIR);
|
||||||
|
let sharp;
|
||||||
|
try {
|
||||||
|
sharp = (await import("sharp")).default;
|
||||||
|
} catch (error) {
|
||||||
|
if (!ifAvailable) throw error;
|
||||||
|
const missing = expectedOutputs().filter((o) => !existsSync(join(outDir, o.file)));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`map-art: sharp unavailable and output missing: ${missing.map((o) => o.file)}`,
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.warn("map-art: sharp unavailable; using the committed output");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await generate(sharp, outDir);
|
||||||
|
console.log(`map-art: wrote ${expectedOutputs().length} files to ${outDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
await main(process.argv.slice(2));
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
// @vitest-environment node
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||||
|
import { artManifest } from "../src/map/art/manifest";
|
||||||
|
|
||||||
|
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
|
const script = join(webRoot, "scripts/map-art.mjs");
|
||||||
|
sharp.cache(false);
|
||||||
|
/** Reads through a buffer so sharp never holds the temp files open (Windows). */
|
||||||
|
const image = (path: string) => sharp(readFileSync(path));
|
||||||
|
const SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] } as const;
|
||||||
|
|
||||||
|
const run = (outDir: string) =>
|
||||||
|
execFileSync(process.execPath, [script, outDir], { cwd: webRoot, stdio: "pipe" });
|
||||||
|
|
||||||
|
describe("map art optimization script (CR-003)", () => {
|
||||||
|
let first: string;
|
||||||
|
let second: string;
|
||||||
|
beforeAll(() => {
|
||||||
|
first = mkdtempSync(join(tmpdir(), "map-art-a-"));
|
||||||
|
second = mkdtempSync(join(tmpdir(), "map-art-b-"));
|
||||||
|
run(first);
|
||||||
|
run(second);
|
||||||
|
}, 120_000);
|
||||||
|
afterAll(() => {
|
||||||
|
rmSync(first, { recursive: true, force: true });
|
||||||
|
rmSync(second, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const present = Object.entries(artManifest.assets).filter(([, a]) => a.file !== null);
|
||||||
|
|
||||||
|
it("writes every expected file at the right size", async () => {
|
||||||
|
for (const [id, asset] of present) {
|
||||||
|
if (asset.kind === "background") {
|
||||||
|
const meta = await image(join(first, `${id}.webp`)).metadata();
|
||||||
|
expect(meta.format).toBe("webp");
|
||||||
|
expect(Math.max(meta.width!, meta.height!)).toBeLessThanOrEqual(2048);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [one, two] = SIZES[asset.kind];
|
||||||
|
for (const [suffix, longSide] of [
|
||||||
|
["1x", one],
|
||||||
|
["2x", two],
|
||||||
|
] as const) {
|
||||||
|
const meta = await image(join(first, `${id}@${suffix}.png`)).metadata();
|
||||||
|
expect(Math.max(meta.width!, meta.height!), `${id}@${suffix}`).toBe(longSide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("produces identical files when run twice", () => {
|
||||||
|
const files = readdirSync(first).sort();
|
||||||
|
expect(readdirSync(second).sort()).toEqual(files);
|
||||||
|
for (const file of files) {
|
||||||
|
expect(readFileSync(join(second, file)).equals(readFileSync(join(first, file))), file).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the committed output pixel for pixel", async () => {
|
||||||
|
// Pixels, not bytes: PNG compression may differ between platforms (CI runs Linux).
|
||||||
|
const committed = join(webRoot, "public/assets/map-gen");
|
||||||
|
for (const file of readdirSync(first).filter((f) => f.endsWith(".png"))) {
|
||||||
|
const [fresh, stored] = await Promise.all([
|
||||||
|
image(join(first, file)).raw().toBuffer(),
|
||||||
|
image(join(committed, file)).raw().toBuffer(),
|
||||||
|
]);
|
||||||
|
expect(stored.equals(fresh), file).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the background under 1 MB", () => {
|
||||||
|
expect(statSync(join(first, "bg.webp")).size).toBeLessThan(1024 * 1024);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has source sprites with transparent backgrounds", async () => {
|
||||||
|
for (const [id, asset] of present) {
|
||||||
|
if (asset.kind === "background") continue;
|
||||||
|
const { data, info } = await image(join(webRoot, artManifest.sourceDir, asset.file!))
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
const alpha = (x: number, y: number) => data[(y * info.width + x) * info.channels + 3];
|
||||||
|
const corners = [
|
||||||
|
alpha(0, 0),
|
||||||
|
alpha(info.width - 1, 0),
|
||||||
|
alpha(0, info.height - 1),
|
||||||
|
alpha(info.width - 1, info.height - 1),
|
||||||
|
];
|
||||||
|
expect(
|
||||||
|
corners.every((a) => a === 0),
|
||||||
|
`${asset.file} (${id}): background isn't transparent`,
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,30 @@
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's source map art (public/assets/map) is large and only an input to
|
||||||
|
* scripts/map-art.mjs; the app loads public/assets/map-gen. Drop the sources
|
||||||
|
* from the build output (decision 0.5.37).
|
||||||
|
*/
|
||||||
|
function omitSourceArt(): Plugin {
|
||||||
|
let outDir = "dist";
|
||||||
|
return {
|
||||||
|
name: "omit-source-art",
|
||||||
|
apply: "build",
|
||||||
|
configResolved(config) {
|
||||||
|
outDir = config.build.outDir;
|
||||||
|
},
|
||||||
|
async closeBundle() {
|
||||||
|
await rm(join(outDir, "assets/map"), { recursive: true, force: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss(), omitSourceArt()],
|
||||||
server: {
|
server: {
|
||||||
// Same-domain API in development too (decision 0.5.20). The server listens on
|
// Same-domain API in development too (decision 0.5.20). The server listens on
|
||||||
// IPv4 only, so target 127.0.0.1 rather than localhost.
|
// IPv4 only, so target 127.0.0.1 rather than localhost.
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,8 @@ Vite + React 19 + Tailwind 4, React Router 8 (declarative routes). In developmen
|
||||||
| `src/App.tsx` | Clock sync; loading / start screen / routed layout |
|
| `src/App.tsx` | Clock sync; loading / start screen / routed layout |
|
||||||
| `src/layout/Layout.tsx` | Header (credits, game clock), screen outlet, bottom tab bar |
|
| `src/layout/Layout.tsx` | Header (credits, game clock), screen outlet, bottom tab bar |
|
||||||
| `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 |
|
||||||
|
| `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/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) |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Change Request CR-003 — Map Art
|
||||||
|
|
||||||
|
**Project:** Deadhead: Space Freight Tycoon
|
||||||
|
**Applies to:** a build with Phases 1–10, CR-001 (Faster Start), and CR-002 (Open Markets) complete
|
||||||
|
**Do this:** as **Phase 10C**, after Phase 10B and before Phase 11
|
||||||
|
**Related docs:** `implementation-plan.md`, `change-request-faster-start.md`, `change-request-open-markets.md`, `DECISIONS.md`
|
||||||
|
|
||||||
|
> **Numbering note (added when implemented):** the plan's §0.5 already uses 29–34 (CR-001) and 35–36 (CR-002). This CR's decisions 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** and **0.5.41 Fallback**. References to them in §5 and §6 mean the renumbered decisions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Why
|
||||||
|
|
||||||
|
The Phase 10 system map uses placeholder circles. The user has generated real art and placed it in the map assets folder (expected: `apps/web/public/assets/map/`). This change replaces the placeholders with that art and tightens how the map draws, while keeping everything that moves or changes state (positions, orbits, routes, labels, locks, badges) drawn in code.
|
||||||
|
|
||||||
|
## 2. The art
|
||||||
|
|
||||||
|
### What the user provided
|
||||||
|
- `bg.png` — the space background. No planets in it.
|
||||||
|
- One transparent sprite per body: Sun, Earth, Moon, Mars, Ceres Station, Jupiter.
|
||||||
|
- Possibly a small cloud-platform badge for Jupiter.
|
||||||
|
- One transparent top-down sprite per ship model, nose pointing **up**: Courier, Freighter, Bulk Hauler, Company Courier.
|
||||||
|
|
||||||
|
Source images are large (likely 1024×1024 or bigger) and may have empty padding around the subject.
|
||||||
|
|
||||||
|
### What is drawn in code (not art)
|
||||||
|
Orbit rings, route lines, location labels, the undiscovered dim and lock, the HQ (bay) marker, idle-ship count badges, selection highlights.
|
||||||
|
|
||||||
|
### Expected logical asset ids
|
||||||
|
|
||||||
|
| Asset id | Used for |
|
||||||
|
|---|---|
|
||||||
|
| `bg` | Background |
|
||||||
|
| `sun` | Sun |
|
||||||
|
| `body-earth`, `body-moon`, `body-mars`, `body-ceres`, `body-jupiter` | Locations |
|
||||||
|
| `badge-cloud-platform` | Jupiter Cloud Platform marker (optional) |
|
||||||
|
| `ship-courier`, `ship-freighter`, `ship-bulk-hauler`, `ship-company-courier` | Ships |
|
||||||
|
|
||||||
|
The actual file names in the folder may differ. Step 10C.1 maps them.
|
||||||
|
|
||||||
|
## 3. Rules for this change
|
||||||
|
|
||||||
|
- All rules in `implementation-plan.md` §0.1 apply: steps in order, one commit per step, each step's test must pass.
|
||||||
|
- **Never modify the user's source images.** All resizing and conversion writes to a separate generated folder.
|
||||||
|
- Display sizes, offsets, and visual tuning values are presentation settings, not game numbers. They live in one map style config file in `apps/web`, not in `packages/content`. Sprite keys (which asset belongs to which location or ship model) **do** live in `packages/content`.
|
||||||
|
- Visual quality can't be fully tested automatically. Steps marked "manual check" end with a screenshot (desktop and phone width) committed to `docs/screenshots/` and mentioned in `progress.md` for the user to review.
|
||||||
|
- Update `progress.md` after every step. Stop for the user's review at the end of Phase 10C.
|
||||||
|
|
||||||
|
## 4. Decisions
|
||||||
|
|
||||||
|
Record these in `DECISIONS.md`.
|
||||||
|
|
||||||
|
- **0.5.29 Map art pipeline.** Source art lives in the map assets folder and is never edited. A script generates optimized copies (trimmed, resized at 1x and 2x, background converted to WebP) into a generated folder that the app loads.
|
||||||
|
- **0.5.30 Constant screen size.** Body and ship sprites keep a roughly constant on-screen size as the player zooms, scaled only between a configured minimum and maximum factor. Positions still use the compressed radial scale from Step 10.1.
|
||||||
|
- **0.5.31 Child bodies.** The Moon (and any future location whose position is "parent plus small offset") is drawn at a fixed screen-space offset from its parent, not at its scaled real position, so it never overlaps the parent at any zoom.
|
||||||
|
- **0.5.32 Background.** The background is not tiled. It is scaled to cover the viewport with a margin and moves with gentle parallax, clamped so its edges never show.
|
||||||
|
- **0.5.33 Fallback.** If any asset is missing or fails to load, the map draws the old placeholder for that item and logs one warning. The map never fails to render because of art.
|
||||||
|
|
||||||
|
## 5. Steps — Phase 10C
|
||||||
|
|
||||||
|
### Step 10C.1 — Inventory and asset manifest
|
||||||
|
- List the files in the map assets folder. Map each file to the logical asset ids in §2. Record the mapping in a manifest file in `apps/web` (asset id → source file name).
|
||||||
|
- If a file name is ambiguous or an expected asset is missing, record it in `DECISIONS.md` and continue: missing assets use the fallback (0.5.33). If more than one file could match the same id, stop and ask the user.
|
||||||
|
- Add a `sprite` key to each location and ship model in `packages/content` (for example Earth → `body-earth`, Freighter → `ship-freighter`). Add the optional `badge` key to Jupiter Cloud Platform.
|
||||||
|
- **Test:** A test asserts every location and ship model in content has a sprite key, and every sprite key exists in the manifest. A second test asserts every file named in the manifest exists on disk.
|
||||||
|
|
||||||
|
### Step 10C.2 — Asset optimization script
|
||||||
|
- Add a script (using `sharp`) that reads the manifest and writes to a generated folder:
|
||||||
|
- Body and badge sprites: trim transparent padding, then export PNG at 256 px and 512 px on the long side.
|
||||||
|
- Ship sprites: trim, then export PNG at 128 px and 256 px.
|
||||||
|
- Background: convert `bg.png` to WebP at no more than 2048 px on the long side.
|
||||||
|
- Run the script as part of the web build and as a standalone command. Commit the generated output so builds don't depend on `sharp` at deploy time.
|
||||||
|
- **Test:** Running the script produces every expected output file at the right dimensions. Running it twice produces identical files. For every sprite (not the background), the four corner pixels of the **source** image are fully transparent; if not, the test fails with a message naming the file and saying its background isn't transparent (this catches a painted-on checkerboard). The background output is under 1 MB.
|
||||||
|
|
||||||
|
### Step 10C.3 — Map style config
|
||||||
|
- Create one map style config in `apps/web` holding: on-screen base size per sprite (Sun ~64 px, Jupiter ~48 px, Earth and Mars ~32 px, Ceres ~22 px, Moon ~18 px, ships ~24 px); zoom scale limits for sprites (for example 0.75× to 1.75×); Moon screen offset from Earth (distance and angle); badge offset; minimum tap target size (44 px); parallax factor; background cover margin; colors for orbit rings, routes, labels, HQ marker, and badges.
|
||||||
|
- **Test:** The config validates against a small schema (all sizes positive, min scale ≤ max scale, parallax between 0 and 1).
|
||||||
|
|
||||||
|
### Step 10C.4 — Asset loading and fallback
|
||||||
|
- Load the generated assets with PixiJS's asset loader as one bundle. Choose the 2x variants when the device pixel ratio is 2 or more.
|
||||||
|
- Show a small loading state on the map until the bundle resolves. On any per-asset failure, use the placeholder for that item and log one warning per asset id.
|
||||||
|
- **Test:** With all assets mocked as loaded, the scene uses textures for every location and ship. With one asset mocked as failed, that item uses the placeholder, the rest use textures, and exactly one warning is logged. With device pixel ratio 2, the 2x variants are requested.
|
||||||
|
|
||||||
|
### Step 10C.5 — Background
|
||||||
|
- Draw the background per decision 0.5.32: scaled to cover the viewport plus the configured margin, offset by camera pan × parallax factor, and clamped so no edge is ever visible. It does not zoom with the map (or zooms only slightly).
|
||||||
|
- **Test:** Unit test of the background placement function: for a range of viewport sizes, pans, and zooms, the background rectangle always fully contains the viewport. Manual check: panning shows gentle parallax with no visible edges or seams; screenshot committed.
|
||||||
|
|
||||||
|
### Step 10C.6 — Bodies, orbits, and labels
|
||||||
|
- Draw the Sun at the center and each location's sprite at its compressed-scale position, sized per decisions 0.5.30 and 0.5.31.
|
||||||
|
- Draw a thin, faint orbit ring for each body that orbits the Sun, at its current compressed radius. Draw a small ring around Earth for the Moon.
|
||||||
|
- Draw the Jupiter cloud-platform badge at the configured offset on Jupiter's edge, if the asset exists.
|
||||||
|
- Draw each location's name under it with a dark halo for contrast.
|
||||||
|
- Every location's tap target is at least the configured minimum size, even if its sprite is smaller.
|
||||||
|
- **Test:** Unit tests: the sprite scale function stays within the configured limits at extreme zooms; the Moon's screen position is exactly the configured offset from Earth's at several zoom levels and dates; tap targets are never smaller than the minimum. Manual check: at default zoom on a phone-width screen, all bodies are visible, nothing overlaps, and labels are readable; screenshot committed.
|
||||||
|
|
||||||
|
### Step 10C.7 — Location state overlays
|
||||||
|
- Undiscovered locations: sprite desaturated (color matrix filter) and dimmed, with a code-drawn lock icon. This replaces the CR-001 Step 10A.16 lock rendering; keep its tap panel (undiscovered text and reachability list).
|
||||||
|
- Locations with a player bay: a small code-drawn HQ marker under the label.
|
||||||
|
- Locations with idle ships: a count badge (for example "2") at the configured badge offset. Idle ships are **not** drawn individually, so ships never pile up on a planet.
|
||||||
|
- **Test:** Scene test with seeded state: undiscovered locations have the filter and lock; bay locations have the HQ marker; a location with two idle ships shows a badge reading 2 and no individual idle ship sprites. Manual check with screenshot.
|
||||||
|
|
||||||
|
### Step 10C.8 — Ships in flight
|
||||||
|
- Each in-flight ship uses its model's sprite, positioned with the sim interpolation every frame (as in Step 10.2), rotated to face its direction of travel (the art points up, so add the matching angle offset).
|
||||||
|
- Draw each route as a faint line from origin to destination, brighter for the remaining part ahead of the ship.
|
||||||
|
- If several ships are within a few pixels of each other, nudge them apart perpendicular to their route so each is visible and tappable.
|
||||||
|
- The selected ship gets a highlight ring.
|
||||||
|
- **Test:** Unit tests: the rotation for a ship moving right is 90° from the art's up direction, and moving down is 180°; with three ships at the same position on the same route, their screen positions are distinct and at least the minimum tap size apart. Manual check: ships move smoothly and face where they're going; screenshot committed.
|
||||||
|
|
||||||
|
### Step 10C.9 — Performance
|
||||||
|
- Use one texture per asset (no per-frame texture creation). Pause the PixiJS ticker when the Map route is not mounted or the page is hidden, and resume on return.
|
||||||
|
- **Test:** Unmounting the Map route stops the ticker; a hidden-page event pauses it and a visible event resumes it. Manual check: on a mid-range phone, panning and zooming stay smooth with five locations and ten ships in flight; note the result in `progress.md`.
|
||||||
|
|
||||||
|
### Step 10C.10 — Plan amendments and regression
|
||||||
|
- Apply the amendments in §6 to `implementation-plan.md`.
|
||||||
|
- Run the full suite.
|
||||||
|
- **Test:** CI is green. Manual check: the user reviews the committed screenshots (desktop and phone) of the tutorial map, a mid-game map with ships in flight, and a map with undiscovered locations.
|
||||||
|
|
||||||
|
## 6. Amendments to `implementation-plan.md`
|
||||||
|
|
||||||
|
| Step | Change |
|
||||||
|
|---|---|
|
||||||
|
| 0.5 | Add decisions 0.5.29–0.5.33. |
|
||||||
|
| 10.1 | Bodies use sprites, constant screen size, Moon offset (10C.6). |
|
||||||
|
| 10.2 | Ships use rotated sprites; idle ships shown as count badges (10C.7, 10C.8). |
|
||||||
|
| 10.3 | Tap targets meet the minimum size (10C.6). |
|
||||||
|
| CR-001 10A.16 | Lock rendering replaced by 10C.7; panel behavior unchanged. |
|
||||||
|
| 11.1 | The service worker precaches the generated map assets. |
|
||||||
|
|
||||||
|
## 7. Out of scope
|
||||||
|
|
||||||
|
Animated sprites or sprite sheets, engine trail particles, planet rotation, day/night shading, zoom-dependent detail levels, new art beyond what the user provided, and anything else in `implementation-plan.md` §0.3.
|
||||||
|
|
@ -92,6 +92,11 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
||||||
34. **Onboarding goals** (CR-001). A fixed, ordered list of goals in content, each with a condition evaluated from player state and a one-time credit reward the player claims. Goals appear after the tutorial ends.
|
34. **Onboarding goals** (CR-001). A fixed, ordered list of goals in content, each with a condition evaluated from player state and a one-time credit reward the player claims. Goals appear after the tutorial ends.
|
||||||
35. **Ship delivery** (CR-002). Buying a ship requires choosing one of the player's bays. The new ship appears there, idle and uncrewed.
|
35. **Ship delivery** (CR-002). Buying a ship requires choosing one of the player's bays. The new ship appears there, idle and uncrewed.
|
||||||
36. **Undiscovered locations** (CR-002). Requesting the board of an undiscovered location fails with a `location-undiscovered` error. Deadheading to an undiscovered location is not allowed; only a contract delivery discovers a location.
|
36. **Undiscovered locations** (CR-002). Requesting the board of an undiscovered location fails with a `location-undiscovered` error. Deadheading to an undiscovered location is not allowed; only a contract delivery discovers a location.
|
||||||
|
37. **Map art pipeline** (CR-003). Source art lives in the map assets folder and is never edited. A script generates optimized copies (trimmed, resized at 1x and 2x, background converted to WebP) into a generated folder that the app loads.
|
||||||
|
38. **Constant screen size** (CR-003). Body and ship sprites keep a roughly constant on-screen size as the player zooms, scaled only between a configured minimum and maximum factor. Positions still use the compressed radial scale from Step 10.1.
|
||||||
|
39. **Child bodies** (CR-003). The Moon (and any future location whose position is "parent plus small offset") is drawn at a fixed screen-space offset from its parent, not at its scaled real position, so it never overlaps the parent at any zoom.
|
||||||
|
40. **Background** (CR-003). The background is not tiled. It is scaled to cover the viewport with a margin and moves with gentle parallax, clamped so its edges never show.
|
||||||
|
41. **Fallback** (CR-003). If any asset is missing or fails to load, the map draws the old placeholder for that item and logs one warning. The map never fails to render because of art.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -489,6 +494,12 @@ Steps 10B.1–10B.10 are specified in `memory-bank/change-requests/CR-002-open-m
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 10C — Map Art (CR-003)
|
||||||
|
|
||||||
|
Steps 10C.1–10C.10 are specified in `memory-bank/change-requests/CR-003-map-art.md` §5, with its §6 amendments to earlier steps. Done after Phase 10B and before Phase 11; stop for review at the end.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 11 — PWA
|
## Phase 11 — PWA
|
||||||
|
|
||||||
### Step 11.1 — Installable PWA
|
### Step 11.1 — Installable PWA
|
||||||
|
|
|
||||||
|
|
@ -144,3 +144,18 @@ Spec: `memory-bank/change-requests/CR-002-open-markets.md`. New decisions record
|
||||||
- [x] **Step 10B.8 — Client: deadhead action.** Fleet's "Reposition" is now "Deadhead (fly empty)" on every idle ship. The picker lists discovered locations except the ship's own, each with sim fuel cost and trip time; destinations the sim rejects (out of range, can't land, fuel, no pilot) are disabled with the reason. Test: `fleet.test.tsx`.
|
- [x] **Step 10B.8 — Client: deadhead action.** Fleet's "Reposition" is now "Deadhead (fly empty)" on every idle ship. The picker lists discovered locations except the ship's own, each with sim fuel cost and trip time; destinations the sim rejects (out of range, can't land, fuel, no pilot) are disabled with the reason. Test: `fleet.test.tsx`.
|
||||||
- [x] **Step 10B.9 — Client: bays as HQs.** Market Buy opens "Deliver to which HQ?" (radio list of bays, preselected when there is one) and sends `bayId`. Locations and the map panel link every discovered location to "View contracts" and say whether the HQ is there; the lease action shows "Open an HQ here: hire pilots and take delivery of new ships at this location." Tests: `market`, `locations`, `map-interactions`.
|
- [x] **Step 10B.9 — Client: bays as HQs.** Market Buy opens "Deliver to which HQ?" (radio list of bays, preselected when there is one) and sends `bayId`. Locations and the map panel link every discovered location to "View contracts" and say whether the HQ is there; the lease action shows "Open an HQ here: hire pilots and take delivery of new ships at this location." Tests: `market`, `locations`, `map-interactions`.
|
||||||
- [x] **Step 10B.10 — Plan amendments and regression.** CR-002 §5 amendments added to steps 3.9, 3.14, 5.3, 6.5, 6.9–6.11, 9.4, 9.6, 9.8, 9.9, 10.3, 12.1 and 12.5 (glossary and §0.5 were done with the CR). GDD §4, §10 and §12 now describe open markets and bays as HQs. Full suite and coverage green; an end-to-end smoke run against a live server (guest → 4 runs → deal → Earth→Moon → Moon→Earth backhaul with only the Earth bay) passed. **Phase 10B complete — stopped for review.**
|
- [x] **Step 10B.10 — Plan amendments and regression.** CR-002 §5 amendments added to steps 3.9, 3.14, 5.3, 6.5, 6.9–6.11, 9.4, 9.6, 9.8, 9.9, 10.3, 12.1 and 12.5 (glossary and §0.5 were done with the CR). GDD §4, §10 and §12 now describe open markets and bays as HQs. Full suite and coverage green; an end-to-end smoke run against a live server (guest → 4 runs → deal → Earth→Moon → Moon→Earth backhaul with only the Earth bay) passed. **Phase 10B complete — stopped for review.**
|
||||||
|
|
||||||
|
## Phase 10C — Map Art (CR-003)
|
||||||
|
|
||||||
|
Spec: `memory-bank/change-requests/CR-003-map-art.md`. New decisions recorded as plan §0.5.37–0.5.41.
|
||||||
|
|
||||||
|
- [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).
|
||||||
|
- [ ] Step 10C.3 — Map style config
|
||||||
|
- [ ] Step 10C.4 — Asset loading and fallback
|
||||||
|
- [ ] Step 10C.5 — Background
|
||||||
|
- [ ] Step 10C.6 — Bodies, orbits, and labels
|
||||||
|
- [ ] Step 10C.7 — Location state overlays
|
||||||
|
- [ ] Step 10C.8 — Ships in flight
|
||||||
|
- [ ] Step 10C.9 — Performance
|
||||||
|
- [ ] Step 10C.10 — Plan amendments and regression
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export const locations: Location[] = [
|
||||||
type: "atmospheric-world",
|
type: "atmospheric-world",
|
||||||
landingRequirement: "atmospheric",
|
landingRequirement: "atmospheric",
|
||||||
body: "earth",
|
body: "earth",
|
||||||
|
sprite: "body-earth",
|
||||||
launchCostMultiplier: 3,
|
launchCostMultiplier: 3,
|
||||||
startsDiscovered: true,
|
startsDiscovered: true,
|
||||||
bayLeasePrice: 1000,
|
bayLeasePrice: 1000,
|
||||||
|
|
@ -24,6 +25,7 @@ export const locations: Location[] = [
|
||||||
type: "airless-body",
|
type: "airless-body",
|
||||||
landingRequirement: "vacuum",
|
landingRequirement: "vacuum",
|
||||||
body: "moon",
|
body: "moon",
|
||||||
|
sprite: "body-moon",
|
||||||
launchCostMultiplier: 0.5,
|
launchCostMultiplier: 0.5,
|
||||||
startsDiscovered: true,
|
startsDiscovered: true,
|
||||||
bayLeasePrice: 800,
|
bayLeasePrice: 800,
|
||||||
|
|
@ -36,6 +38,7 @@ export const locations: Location[] = [
|
||||||
type: "atmospheric-world",
|
type: "atmospheric-world",
|
||||||
landingRequirement: "atmospheric",
|
landingRequirement: "atmospheric",
|
||||||
body: "mars",
|
body: "mars",
|
||||||
|
sprite: "body-mars",
|
||||||
launchCostMultiplier: 1.5,
|
launchCostMultiplier: 1.5,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 20000,
|
bayLeasePrice: 20000,
|
||||||
|
|
@ -48,6 +51,7 @@ export const locations: Location[] = [
|
||||||
type: "belt-station",
|
type: "belt-station",
|
||||||
landingRequirement: "vacuum",
|
landingRequirement: "vacuum",
|
||||||
body: "ceres",
|
body: "ceres",
|
||||||
|
sprite: "body-ceres",
|
||||||
launchCostMultiplier: 0.3,
|
launchCostMultiplier: 0.3,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 40000,
|
bayLeasePrice: 40000,
|
||||||
|
|
@ -60,6 +64,8 @@ export const locations: Location[] = [
|
||||||
type: "cloud-platform",
|
type: "cloud-platform",
|
||||||
landingRequirement: "atmospheric",
|
landingRequirement: "atmospheric",
|
||||||
body: "jupiter",
|
body: "jupiter",
|
||||||
|
sprite: "body-jupiter",
|
||||||
|
badge: "badge-cloud-platform",
|
||||||
launchCostMultiplier: 2,
|
launchCostMultiplier: 2,
|
||||||
startsDiscovered: false,
|
startsDiscovered: false,
|
||||||
bayLeasePrice: 80000,
|
bayLeasePrice: 80000,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import type { ShipModel } from "../schemas";
|
||||||
export const shipModels: ShipModel[] = [
|
export const shipModels: ShipModel[] = [
|
||||||
{
|
{
|
||||||
id: "courier",
|
id: "courier",
|
||||||
|
sprite: "ship-courier",
|
||||||
name: "Courier",
|
name: "Courier",
|
||||||
archetype: "courier",
|
archetype: "courier",
|
||||||
price: 4000,
|
price: 4000,
|
||||||
|
|
@ -23,6 +24,7 @@ export const shipModels: ShipModel[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "freighter",
|
id: "freighter",
|
||||||
|
sprite: "ship-freighter",
|
||||||
name: "Freighter",
|
name: "Freighter",
|
||||||
archetype: "freighter",
|
archetype: "freighter",
|
||||||
price: 100000,
|
price: 100000,
|
||||||
|
|
@ -38,6 +40,7 @@ export const shipModels: ShipModel[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bulk-hauler",
|
id: "bulk-hauler",
|
||||||
|
sprite: "ship-bulk-hauler",
|
||||||
name: "Bulk Hauler",
|
name: "Bulk Hauler",
|
||||||
archetype: "bulk-hauler",
|
archetype: "bulk-hauler",
|
||||||
price: 400000,
|
price: 400000,
|
||||||
|
|
@ -55,6 +58,7 @@ export const shipModels: ShipModel[] = [
|
||||||
// The company's tutorial loaner (decision 0.5.29): not for sale. Faster with
|
// 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.
|
// less overhead than the Courier, so a full Earth–Moon run is under a real minute.
|
||||||
id: "company-courier",
|
id: "company-courier",
|
||||||
|
sprite: "ship-company-courier",
|
||||||
name: "Company Courier",
|
name: "Company Courier",
|
||||||
archetype: "courier",
|
archetype: "courier",
|
||||||
price: 4000,
|
price: 4000,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ export const locationSchema = z.object({
|
||||||
produces: z.array(idSchema),
|
produces: z.array(idSchema),
|
||||||
/** Cargo type ids. */
|
/** Cargo type ids. */
|
||||||
demands: z.array(idSchema),
|
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>;
|
export type Location = z.infer<typeof locationSchema>;
|
||||||
|
|
||||||
|
|
@ -69,6 +73,8 @@ export const shipModelSchema = z.object({
|
||||||
fuelCostPerMillionKm: z.number().nonnegative(),
|
fuelCostPerMillionKm: z.number().nonnegative(),
|
||||||
/** False for company-only ships such as the tutorial loaner (decision 0.5.29). */
|
/** False for company-only ships such as the tutorial loaner (decision 0.5.29). */
|
||||||
purchasable: z.boolean(),
|
purchasable: z.boolean(),
|
||||||
|
/** Map art asset id (CR-003), e.g. `ship-freighter`. */
|
||||||
|
sprite: idSchema,
|
||||||
});
|
});
|
||||||
export type ShipModel = z.infer<typeof shipModelSchema>;
|
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,
|
launchCostMultiplier: 3,
|
||||||
startsDiscovered: true,
|
startsDiscovered: true,
|
||||||
bayLeasePrice: 5000,
|
bayLeasePrice: 5000,
|
||||||
|
sprite: "body-mars",
|
||||||
produces: ["standard"],
|
produces: ["standard"],
|
||||||
demands: ["heavy"],
|
demands: ["heavy"],
|
||||||
};
|
};
|
||||||
|
|
@ -36,6 +37,7 @@ const validShipModel = {
|
||||||
overheadGameHours: 2,
|
overheadGameHours: 2,
|
||||||
fuelCostPerMillionKm: 100,
|
fuelCostPerMillionKm: 100,
|
||||||
purchasable: true,
|
purchasable: true,
|
||||||
|
sprite: "ship-courier",
|
||||||
};
|
};
|
||||||
|
|
||||||
const validCargoType = {
|
const validCargoType = {
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,10 @@ describe("ship models", () => {
|
||||||
expect(ship("courier").price).toBeLessThan(ship("freighter").price);
|
expect(ship("courier").price).toBeLessThan(ship("freighter").price);
|
||||||
expect(ship("freighter").price).toBeLessThan(ship("bulk-hauler").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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
327
pnpm-lock.yaml
|
|
@ -136,6 +136,9 @@ importers:
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^30.1.1
|
specifier: ^30.1.1
|
||||||
version: 30.1.1(@noble/hashes@2.4.0)
|
version: 30.1.1(@noble/hashes@2.4.0)
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.35.5
|
||||||
|
version: 0.35.5(@types/node@24.19.1)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
|
|
@ -337,6 +340,9 @@ packages:
|
||||||
'@drizzle-team/brocli@0.10.2':
|
'@drizzle-team/brocli@0.10.2':
|
||||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.3':
|
||||||
|
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||||
deprecated: 'Merged into tsx: https://tsx.hirok.io'
|
deprecated: 'Merged into tsx: https://tsx.hirok.io'
|
||||||
|
|
@ -875,6 +881,168 @@ packages:
|
||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@img/colour@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-freebsd-wasm32@0.35.5':
|
||||||
|
resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.3.4':
|
||||||
|
resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.3.4':
|
||||||
|
resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
|
||||||
|
resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.35.5':
|
||||||
|
resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.35.5':
|
||||||
|
resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.35.5':
|
||||||
|
resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
|
||||||
|
'@img/sharp-webcontainers-wasm32@0.35.5':
|
||||||
|
resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.35.5':
|
||||||
|
resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
|
||||||
|
engines: {node: ^20.9.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.35.5':
|
||||||
|
resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
|
@ -2332,6 +2500,15 @@ packages:
|
||||||
set-cookie-parser@3.1.2:
|
set-cookie-parser@3.1.2:
|
||||||
resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
|
resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
|
||||||
|
|
||||||
|
sharp@0.35.5:
|
||||||
|
resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
|
||||||
|
engines: {node: '>=20.9.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2428,6 +2605,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.8.4'
|
typescript: '>=4.8.4'
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
tsx@4.23.15:
|
tsx@4.23.15:
|
||||||
resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==}
|
resolution: {integrity: sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
@ -2760,6 +2940,11 @@ snapshots:
|
||||||
|
|
||||||
'@drizzle-team/brocli@0.10.2': {}
|
'@drizzle-team/brocli@0.10.2': {}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.11.3':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
|
|
@ -3069,6 +3254,112 @@ snapshots:
|
||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@img/colour@1.1.0': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-freebsd-wasm32@0.35.5':
|
||||||
|
dependencies:
|
||||||
|
'@img/sharp-wasm32': 0.35.5
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-riscv64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.3.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-riscv64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.35.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.35.5':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.11.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-webcontainers-wasm32@0.35.5':
|
||||||
|
dependencies:
|
||||||
|
'@img/sharp-wasm32': 0.35.5
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.35.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.35.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.35.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@jridgewell/sourcemap-codec': 1.6.0
|
||||||
|
|
@ -4354,6 +4645,39 @@ snapshots:
|
||||||
|
|
||||||
set-cookie-parser@3.1.2: {}
|
set-cookie-parser@3.1.2: {}
|
||||||
|
|
||||||
|
sharp@0.35.5(@types/node@24.19.1):
|
||||||
|
dependencies:
|
||||||
|
'@img/colour': 1.1.0
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
semver: 7.8.5
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.35.5
|
||||||
|
'@img/sharp-darwin-x64': 0.35.5
|
||||||
|
'@img/sharp-freebsd-wasm32': 0.35.5
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.3.4
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-riscv64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.3.4
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.3.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.3.4
|
||||||
|
'@img/sharp-linux-arm': 0.35.5
|
||||||
|
'@img/sharp-linux-arm64': 0.35.5
|
||||||
|
'@img/sharp-linux-ppc64': 0.35.5
|
||||||
|
'@img/sharp-linux-riscv64': 0.35.5
|
||||||
|
'@img/sharp-linux-s390x': 0.35.5
|
||||||
|
'@img/sharp-linux-x64': 0.35.5
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.35.5
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.35.5
|
||||||
|
'@img/sharp-webcontainers-wasm32': 0.35.5
|
||||||
|
'@img/sharp-win32-arm64': 0.35.5
|
||||||
|
'@img/sharp-win32-ia32': 0.35.5
|
||||||
|
'@img/sharp-win32-x64': 0.35.5
|
||||||
|
'@types/node': 24.19.1
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|
@ -4426,6 +4750,9 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tsx@4.23.15:
|
tsx@4.23.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
|
|
|
||||||