deadhead-space-freight-tycoon/apps/web/src/map/style.ts

120 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 as the player zooms (decision 0.5.38): zoom ^ exponent,
* clamped to [min, max], so sprites stay roughly the same size on screen.
*/
spriteScale: z.object({ min: px, max: px, zoomExponent: z.number().min(0).max(1) }),
/**
* Child bodies drawn at a fixed screen distance from their parent (decision
* 0.5.39). The angle is measured from the outward direction (Sun → parent),
* so a child never lands on the Sun.
*/
childOffsets: z.record(z.string(), offset.extend({ parentId: z.string() })),
/**
* Idle-ship count badge: `distancePx` beyond the body's edge, at `angleDeg`,
* or the first of the other diagonals that is clear of bodies and labels.
*/
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() }),
/** 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). */
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, zoomExponent: 0.35 },
// 46 px clears Earth and the Moon at the largest sprite scale: (32 + 18) / 2 × 1.75 = 43.75.
childOffsets: { moon: { parentId: "earth", distancePx: 46, angleDeg: -30 } },
idleBadge: { distancePx: 2, angleDeg: -135, radiusPx: 8, fontSizePx: 10 },
locationBadge: { sizePx: 16, angleDeg: -45 },
undiscoveredAlpha: 0.55,
lockSizePx: 12,
hqSizePx: 9,
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,
},
});