120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
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,
|
||
},
|
||
});
|