@bitmagic/cli 0.1.83-dev.2 → 0.1.83-dev.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +34 -3
  2. package/assets/metahuman/src/work/metahuman/MetaHumanMaterials.ts +4 -0
  3. package/assets/metahuman/src/work/metahuman/StudioEnvironment.ts +4 -0
  4. package/assets/optimizing-assets/SKILL.md +3 -2
  5. package/assets/optimizing-assets/references/tools.md +1 -1
  6. package/dist/assets/vxl-summary.js +1 -1
  7. package/dist/assets/vxl-summary.js.map +1 -1
  8. package/dist/cli.d.ts +9 -0
  9. package/dist/commands/steam-upload.js +6 -4
  10. package/dist/commands/steam-upload.js.map +1 -1
  11. package/dist/commands/steam.js +4 -2
  12. package/dist/commands/steam.js.map +1 -1
  13. package/dist/commands/upgrade.d.ts +18 -0
  14. package/dist/commands/upgrade.js +94 -1
  15. package/dist/commands/upgrade.js.map +1 -1
  16. package/dist/config/environments.d.ts +1 -1
  17. package/dist/editor/animation-preview-page.js +20 -0
  18. package/dist/editor/animation-preview-page.js.map +1 -1
  19. package/dist/editor/asset-viewer.js +4 -0
  20. package/dist/editor/asset-viewer.js.map +1 -1
  21. package/dist/editor/design.d.ts +2 -2
  22. package/dist/editor/design.js +2 -2
  23. package/dist/editor/design.js.map +1 -1
  24. package/dist/editor/hud-replay-page.js +5 -0
  25. package/dist/editor/hud-replay-page.js.map +1 -1
  26. package/dist/editor/recording-sink.js +1 -1
  27. package/dist/editor/runtime-token.d.ts +61 -0
  28. package/dist/editor/runtime-token.js +74 -0
  29. package/dist/editor/runtime-token.js.map +1 -0
  30. package/dist/editor/save.d.ts +2 -2
  31. package/dist/editor/server.js +39 -12
  32. package/dist/editor/server.js.map +1 -1
  33. package/dist/editor/shell-page.d.ts +12 -8
  34. package/dist/editor/shell-page.js +69 -10
  35. package/dist/editor/shell-page.js.map +1 -1
  36. package/dist/forge/browser-host.d.ts +1 -1
  37. package/dist/forge/browser-host.js +14 -11
  38. package/dist/forge/browser-host.js.map +1 -1
  39. package/dist/generate/model.js +4 -1
  40. package/dist/generate/model.js.map +1 -1
  41. package/dist/generate/prop.js +4 -1
  42. package/dist/generate/prop.js.map +1 -1
  43. package/dist/http/signed-upload.d.ts +6 -4
  44. package/dist/http/signed-upload.js +37 -14
  45. package/dist/http/signed-upload.js.map +1 -1
  46. package/dist/mobile/rewrite-shim.js +1 -1
  47. package/dist/project/published-three.js +4 -6
  48. package/dist/project/published-three.js.map +1 -1
  49. package/dist/publish/game-exchange.js +9 -3
  50. package/dist/publish/game-exchange.js.map +1 -1
  51. package/dist/scaffold/blender-level-skill-files.js +2 -2
  52. package/dist/scaffold/game-migration.d.ts +88 -0
  53. package/dist/scaffold/game-migration.js +492 -0
  54. package/dist/scaffold/game-migration.js.map +1 -0
  55. package/dist/scaffold/import-codemod.d.ts +51 -0
  56. package/dist/scaffold/import-codemod.js +117 -0
  57. package/dist/scaffold/import-codemod.js.map +1 -0
  58. package/dist/scaffold/legacy-game-baselines.d.ts +14 -0
  59. package/dist/scaffold/legacy-game-baselines.js +218 -0
  60. package/dist/scaffold/legacy-game-baselines.js.map +1 -0
  61. package/dist/scaffold/project-files.d.ts +1 -1
  62. package/dist/scaffold/project-files.js +13 -47
  63. package/dist/scaffold/project-files.js.map +1 -1
  64. package/dist/scaffold/project.js +8 -17
  65. package/dist/scaffold/project.js.map +1 -1
  66. package/dist/smoke/run.js +8 -1
  67. package/dist/smoke/run.js.map +1 -1
  68. package/dist/steam/art-generate.js +2 -1
  69. package/dist/steam/art-generate.js.map +1 -1
  70. package/dist/steam/art-render.js.map +1 -1
  71. package/dist/steam/art.js +1 -1
  72. package/dist/steam/art.js.map +1 -1
  73. package/dist/steam/check.js.map +1 -1
  74. package/dist/steam/electron-cache.d.ts +1 -1
  75. package/dist/steam/electron-cache.js +2 -2
  76. package/dist/steam/electron-cache.js.map +1 -1
  77. package/dist/steam/fuses.js +1 -1
  78. package/dist/steam/fuses.js.map +1 -1
  79. package/dist/steam/media.js +6 -2
  80. package/dist/steam/media.js.map +1 -1
  81. package/dist/steam/package-windows.d.ts +1 -2
  82. package/dist/steam/package-windows.js +1 -2
  83. package/dist/steam/package-windows.js.map +1 -1
  84. package/dist/steam/paths.d.ts +0 -2
  85. package/dist/steam/paths.js +0 -1
  86. package/dist/steam/paths.js.map +1 -1
  87. package/dist/steam/portal.js +1 -1
  88. package/dist/steam/portal.js.map +1 -1
  89. package/dist/steam/raster.d.ts +0 -1
  90. package/dist/steam/raster.js +1 -1
  91. package/dist/steam/raster.js.map +1 -1
  92. package/dist/steam/store-copy.d.ts +0 -1
  93. package/dist/steam/store-copy.js +3 -10
  94. package/dist/steam/store-copy.js.map +1 -1
  95. package/dist/steam/tags.js +6 -3
  96. package/dist/steam/tags.js.map +1 -1
  97. package/dist/theme/kernel.d.ts +14 -0
  98. package/dist/theme/kernel.js +15 -0
  99. package/dist/theme/kernel.js.map +1 -0
  100. package/dist/theme/theme-io.js +11 -16
  101. package/dist/theme/theme-io.js.map +1 -1
  102. package/dist/trailer/record-session.d.ts +1 -1
  103. package/dist/trailer/record-session.js +5 -8
  104. package/dist/trailer/record-session.js.map +1 -1
  105. package/dist/trailer/vendored-recorder.d.ts +8 -0
  106. package/dist/trailer/vendored-recorder.js +26 -0
  107. package/dist/trailer/vendored-recorder.js.map +1 -0
  108. package/dist/vendor/hud-theme/catalog.js +71 -0
  109. package/dist/vendor/hud-theme/engine/hud/ThemeTokens.js +115 -0
  110. package/dist/{theme/theme-color-math.js → vendor/hud-theme/engine/hud/colorMath.js} +13 -14
  111. package/dist/vendor/hud-theme/engine/hud/decorations.js +179 -0
  112. package/dist/vendor/hud-theme/engine/hud/fonts.js +147 -0
  113. package/dist/vendor/hud-theme/engine/hud/presets.js +80 -0
  114. package/dist/vendor/hud-theme/engine/hud/resolveWorldTheme.js +66 -0
  115. package/dist/vendor/hud-theme/engine/hud/themes/bitmagic.js +29 -0
  116. package/dist/vendor/hud-theme/engine/hud/themes/nexus.js +69 -0
  117. package/dist/vendor/hud-theme/engine/hud/themes/racing.js +57 -0
  118. package/dist/vendor/hud-theme/engine/hud/themes/rift-raider.js +63 -0
  119. package/dist/vendor/hud-theme/engine/hud/themes/simulator.js +55 -0
  120. package/dist/vendor/hud-theme/engine/hud/themes/village-keep.js +108 -0
  121. package/dist/vendor/hud-theme/engine/hud/validateTheme.js +535 -0
  122. package/dist/vendor/hud-theme/index.js +26 -0
  123. package/dist/{theme/theme-merge.js → vendor/hud-theme/merge.js} +0 -1
  124. package/dist/{theme/theme-report.js → vendor/hud-theme/report.js} +8 -9
  125. package/dist/{theme/theme-stored.js → vendor/hud-theme/stored.js} +6 -9
  126. package/dist/{theme/theme-validate.js → vendor/hud-theme/validate-strict.js} +102 -164
  127. package/dist/verify/browser.d.ts +6 -0
  128. package/dist/verify/browser.js +30 -3
  129. package/dist/verify/browser.js.map +1 -1
  130. package/dist/verify/categories.d.ts +1 -1
  131. package/dist/verify/categories.js +1 -0
  132. package/dist/verify/categories.js.map +1 -1
  133. package/dist/verify/classify.d.ts +7 -0
  134. package/dist/verify/classify.js +7 -0
  135. package/dist/verify/classify.js.map +1 -1
  136. package/dist/verify/hud-theme.d.ts +19 -14
  137. package/dist/verify/hud-theme.js +23 -17
  138. package/dist/verify/hud-theme.js.map +1 -1
  139. package/dist/verify/mobile-parity.d.ts +1 -1
  140. package/dist/verify/mobile-parity.js +1 -1
  141. package/dist/verify/model-downloads.d.ts +47 -0
  142. package/dist/verify/model-downloads.js +127 -0
  143. package/dist/verify/model-downloads.js.map +1 -0
  144. package/dist/verify/start.d.ts +3 -2
  145. package/dist/verify/start.js +1 -1
  146. package/dist/verify/start.js.map +1 -1
  147. package/package.json +11 -7
  148. package/dist/theme/hud-theme-catalog.d.ts +0 -235
  149. package/dist/theme/hud-theme-catalog.js +0 -389
  150. package/dist/theme/hud-theme-catalog.js.map +0 -1
  151. package/dist/theme/theme-color-math.d.ts +0 -21
  152. package/dist/theme/theme-color-math.js.map +0 -1
  153. package/dist/theme/theme-merge.d.ts +0 -36
  154. package/dist/theme/theme-merge.js.map +0 -1
  155. package/dist/theme/theme-report.d.ts +0 -28
  156. package/dist/theme/theme-report.js.map +0 -1
  157. package/dist/theme/theme-stored.d.ts +0 -21
  158. package/dist/theme/theme-stored.js.map +0 -1
  159. package/dist/theme/theme-validate.d.ts +0 -41
  160. package/dist/theme/theme-validate.js.map +0 -1
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The HUD theme catalog as the tools see it: what `get-hud-catalog` returns, what
3
+ * `bitmagic theme` offers, and the vocabulary the strict validator checks against.
4
+ *
5
+ * Nothing here is a copy. Every list and every value is read from the engine's own
6
+ * kernel modules (game/src/engine/hud/), which this package compiles, so a preset,
7
+ * font, decoration, glow key or element class added in the engine reaches the agent
8
+ * and the CLI with no second edit. The only data authored here is tool-facing prose
9
+ * (the icon slot descriptions).
10
+ */
11
+ import { DECORATION_SLOT_KINDS, GLOW_KEYS, HUD_ELEMENT_CLASSES, REQUIRED_THEME_COLOR_KEYS, THEME_COLOR_KEYS, THEME_ICON_KEYS, THEME_SHAPE_KEYS, } from './engine/hud/ThemeTokens.js';
12
+ import { HUD_FONTS } from './engine/hud/fonts.js';
13
+ import { HUD_DECORATIONS } from './engine/hud/decorations.js';
14
+ import { DEFAULT_THEME_DATA, PRESET_NAMES, PRESET_THEME_DATA } from './engine/hud/presets.js';
15
+ export { HUD_FONTS, HUD_DECORATIONS, HUD_ELEMENT_CLASSES };
16
+ /** The five selectable presets, in catalogue order. */
17
+ export const HUD_PRESET_NAMES = PRESET_NAMES;
18
+ export function isHudPresetName(value) {
19
+ return typeof value === 'string' && PRESET_NAMES.includes(value);
20
+ }
21
+ /**
22
+ * Each preset's RAW data, the same shape as an inline theme plus its `evokes` label.
23
+ *
24
+ * This exists because the promoted customization flow — "preset X but with orange
25
+ * accents" — needs the preset's real values to patch. Use `presetTheme(name)` rather
26
+ * than reading this directly: it strips `evokes`, which describes the style rather
27
+ * than rendering it.
28
+ */
29
+ export const HUD_PRESET_THEMES = PRESET_THEME_DATA;
30
+ /**
31
+ * A preset's tokens as a writable inline theme, minus the `evokes` label.
32
+ *
33
+ * The intended patch flow: `const base = presetTheme('nexus')`, apply the user's
34
+ * change, then write the result.
35
+ */
36
+ export function presetTheme(name) {
37
+ const { evokes: _evokes, ...tokens } = PRESET_THEME_DATA[name];
38
+ return structuredClone(tokens);
39
+ }
40
+ /** The one-line mood label per preset — the field an agent picks a preset by. */
41
+ export const HUD_PRESET_INFO = Object.fromEntries(PRESET_NAMES.map((name) => [name, { evokes: PRESET_THEME_DATA[name].evokes }]));
42
+ /**
43
+ * The engine's DEFAULT theme (bitmagic) as raw data — the base a patch applies to
44
+ * when the game has no theme set. Deliberately not among the presets: it is the
45
+ * fallback, not a selectable style.
46
+ */
47
+ export const DEFAULT_THEME_TOKENS = DEFAULT_THEME_DATA;
48
+ /**
49
+ * The curated decorations' SVG source, served through `get-hud-catalog`'s
50
+ * `decorationSvg` input as adaptation reference: an agent authoring a custom inline
51
+ * decoration copies the closest curated one and edits it.
52
+ */
53
+ export const HUD_DECORATION_SVGS = Object.fromEntries(Object.entries(HUD_DECORATIONS).map(([key, entry]) => [key, entry.svg]));
54
+ export const HUD_GLOW_KEYS = GLOW_KEYS;
55
+ export const HUD_SHAPE_TOKENS = THEME_SHAPE_KEYS;
56
+ export const HUD_COLOR_TOKENS = THEME_COLOR_KEYS;
57
+ export const HUD_REQUIRED_COLORS = REQUIRED_THEME_COLOR_KEYS;
58
+ export const HUD_DECORATION_SLOTS = DECORATION_SLOT_KINDS;
59
+ const ICON_DESCRIPTIONS = {
60
+ heart: 'Health label icon (shown next to "HP" on the player health bar). Default: filled heart.',
61
+ mouse: 'Pointer-lock overlay icon ("Click to play" screen). Default: outlined mouse.',
62
+ arrow: 'Movement-key arrow used in the controls overlay. ONE SVG — CSS rotates it for down/left/right. Default: thin chevron pointing up.',
63
+ };
64
+ /**
65
+ * Built-in icon slots a theme can override with custom SVG markup. Each value is
66
+ * inline SVG (≤16kb). SVGs may use `currentColor` so the icon follows the
67
+ * surrounding text colour, or the `__primary__` / `__danger__` / etc. colour tokens
68
+ * that decorations use — the engine substitutes them at apply time.
69
+ */
70
+ export const HUD_ICON_SLOTS = THEME_ICON_KEYS.map((key) => ({ key, description: ICON_DESCRIPTIONS[key] }));
71
+ export const HUD_ICON_KEYS = THEME_ICON_KEYS;
@@ -0,0 +1,115 @@
1
+ // HUD theme tokens: the theme types, and the source of truth for what validateTheme accepts.
2
+ // Authoring rules: engine/CLAUDE.md says option fields are required + use DEFAULT_* constants.
3
+ // Theme JSON has its own validation surface (the schema), so partial-theme inputs are
4
+ // validated separately via validateTheme.ts before being widened into a full theme.
5
+ //
6
+ // Every closed vocabulary is declared ONCE here as a runtime list, and its type is derived
7
+ // from that list. The engine's load-time validator, the strict write-time validator in
8
+ // `@bitmagic/hud-theme` and the agent's catalog tool all read these lists, so a new glow key
9
+ // or element class is one edit. This module and the other DOM-free kernel files (colorMath,
10
+ // fonts, decorations, validateTheme, presets, themes/, resolveWorldTheme) are compiled into
11
+ // that package too, which is why they import nothing outside `engine/hud/` and touch no DOM.
12
+ /** The curated glow keys `shape.glow` accepts. */
13
+ export const GLOW_KEYS = ['aqua', 'pink', 'warm', 'horror-red', 'gold', 'cyan', 'none'];
14
+ export const LABEL_CASES = ['upper', 'normal'];
15
+ export const IMAGE_RENDERINGS = ['auto', 'pixelated'];
16
+ /** The body weights `font.weightBody` accepts. */
17
+ export const FONT_BODY_WEIGHTS = [300, 400, 500, 700];
18
+ /** The heading weights `font.weightHeading` accepts. */
19
+ export const FONT_HEADING_WEIGHTS = [400, 500, 700, 900];
20
+ /** A theme colour: six hex digits with a leading `#`. The one pattern every theme check uses. */
21
+ export const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
22
+ export function isHexColor(value) {
23
+ return typeof value === 'string' && HEX_COLOR.test(value);
24
+ }
25
+ /** The longest inline SVG a decoration or icon may carry, in characters. */
26
+ export const MAX_INLINE_SVG_LENGTH = 16384;
27
+ /**
28
+ * The allowed range of every numeric token, as `[min, max]`. The load-time validator, the
29
+ * strict write-time validator and the agent's catalog all read this table.
30
+ */
31
+ export const THEME_NUMBER_RANGES = {
32
+ trackingLabel: [0, 4],
33
+ outlineWidth: [0, 3],
34
+ sizeScale: [0.85, 1.3],
35
+ radiusPill: [0, 500],
36
+ radiusCard: [0, 32],
37
+ borderWidth: [0, 6],
38
+ bevel: [0, 1],
39
+ gloss: [0, 1],
40
+ borderSlice: [0, 200],
41
+ };
42
+ export const BORDER_IMAGE_REPEATS = ['stretch', 'repeat', 'round', 'space'];
43
+ export const BACKGROUND_POSITIONS = [
44
+ 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left', 'top-left', 'center',
45
+ ];
46
+ export const BACKGROUND_REPEATS = ['no-repeat', 'repeat', 'repeat-x', 'repeat-y'];
47
+ export const BACKGROUND_SIZE_KEYWORDS = ['cover', 'contain', 'auto'];
48
+ /**
49
+ * An explicit `background-size` tile: two plain lengths, `<n>px|%` each, decimals allowed.
50
+ *
51
+ * Deliberately strict rather than "any CSS length": the value is interpolated straight into a
52
+ * `background-size:` declaration, so anything that passes is CSS the theme author controls. No
53
+ * `calc()`, no `var()`, no semicolons.
54
+ */
55
+ export const BACKGROUND_TILE_SIZE = /^\d{1,4}(?:\.\d{1,2})?(?:px|%) \d{1,4}(?:\.\d{1,2})?(?:px|%)$/;
56
+ /** The HUD element classes a theme can decorate or override per element. */
57
+ export const HUD_ELEMENT_CLASSES = [
58
+ 'progressBar',
59
+ 'healthBar',
60
+ 'counter',
61
+ 'iconText',
62
+ 'timer',
63
+ 'toast',
64
+ 'controls',
65
+ 'reticle',
66
+ // The touch layer: joystick + on-screen action buttons. Every game must be
67
+ // mobile-playable, and this is the surface that used to be unreachable by
68
+ // theming. The base CSS already derives the stick from --hud-color-text and
69
+ // the buttons from --hud-color-primary/danger/warning, so a per-element
70
+ // colors override restyles the whole layer with no dedicated tokens.
71
+ 'mobileControls',
72
+ ];
73
+ // Key lists for the interfaces above. Each is built from a Record over the interface's keys, so
74
+ // a field added to the interface without reaching its list (or the reverse) fails to compile.
75
+ const COLOR_KEY_SET = {
76
+ primary: true, danger: true, warning: true, success: true, background: true,
77
+ surface: true, text: true, textMuted: true, outline: true,
78
+ };
79
+ const SHAPE_KEY_SET = {
80
+ radiusPill: true, radiusCard: true, glow: true, imageRendering: true,
81
+ borderWidth: true, bevel: true, gloss: true, glowColor: true,
82
+ };
83
+ const DECORATION_SLOT_SET = {
84
+ borderImage: true, backdrop: true, decorationBefore: true, decorationAfter: true,
85
+ };
86
+ const ICON_KEY_SET = { heart: true, mouse: true, arrow: true };
87
+ /** Every colour token a theme may set. */
88
+ export const THEME_COLOR_KEYS = Object.keys(COLOR_KEY_SET);
89
+ /** The colour tokens a full theme must set; the rest fall back. */
90
+ export const REQUIRED_THEME_COLOR_KEYS = [
91
+ 'primary', 'danger', 'background', 'surface', 'text',
92
+ ];
93
+ /** Every shape token a theme may set. */
94
+ export const THEME_SHAPE_KEYS = Object.keys(SHAPE_KEY_SET);
95
+ /** The decoration slots every element class has. */
96
+ export const DECORATION_SLOT_KINDS = Object.keys(DECORATION_SLOT_SET);
97
+ /** The built-in icons a theme can replace. */
98
+ export const THEME_ICON_KEYS = Object.keys(ICON_KEY_SET);
99
+ export const DEFAULT_THEME_FONT = {
100
+ key: 'red-hat-display',
101
+ case: 'upper',
102
+ weightBody: 400,
103
+ weightHeading: 700,
104
+ trackingLabel: 1.4,
105
+ outlineWidth: 0,
106
+ };
107
+ export const DEFAULT_THEME_SHAPE = {
108
+ radiusPill: 500,
109
+ radiusCard: 8,
110
+ glow: 'aqua',
111
+ imageRendering: 'auto',
112
+ borderWidth: 0,
113
+ bevel: 0,
114
+ gloss: 0,
115
+ };
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Hand-mirror of game/src/engine/hud/colorMath.ts — the WCAG arithmetic behind
3
- * the derived HUD inks. The theme report computes its contrast pairings with
4
- * EXACTLY the maths the engine will apply, so a SUBSTITUTED line in the report
5
- * is a promise about what will render, not an estimate.
2
+ * The WCAG colour arithmetic behind the derived HUD inks — relative luminance,
3
+ * contrast ratios, and the substitution rules that make `--hud-color-on-surface`,
4
+ * `--hud-color-accent-ink` and friends safe to derive.
6
5
  *
7
- * Mirrored by hand because agent tools cannot import engine source (separate
8
- * tsconfig include sets). Drift-checked BEHAVIOURALLY by scripts/
9
- * check-hud-catalog.ts: the gate imports both copies under tsx and compares
10
- * their outputs over every preset palette plus adversarial pairs, so a changed
11
- * threshold or mix factor on either side goes red in CI.
6
+ * A separate module for one load-bearing reason: it is PURE. No DOM, and nothing
7
+ * outside `engine/hud/` — so `@bitmagic/hud-theme` compiles this exact file and
8
+ * the theme report the agent and the CLI print computes its contrast pairings
9
+ * with the arithmetic the engine applies. A SUBSTITUTED line in that report is a
10
+ * promise about what will render, not an estimate.
12
11
  */
12
+ import { isHexColor } from './ThemeTokens.js';
13
13
  export function relLuminance(hex) {
14
14
  const lin = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
15
15
  return 0.2126 * lin(parseInt(hex.slice(1, 3), 16) / 255)
@@ -30,7 +30,7 @@ export function mixHex(from, to, amount) {
30
30
  return `#${channel(1)}${channel(3)}${channel(5)}`;
31
31
  }
32
32
  export function readableTextColor(hex) {
33
- if (!/^#[0-9a-fA-F]{6}$/.test(hex))
33
+ if (!isHexColor(hex))
34
34
  return '#ffffff';
35
35
  return relLuminance(hex) > 0.5 ? '#000000' : '#ffffff';
36
36
  }
@@ -54,7 +54,7 @@ export function readableTextColor(hex) {
54
54
  // to 9.3 for `textMuted`, so this cannot fire on a theme that was already coherent.
55
55
  export const ON_SURFACE_MIN_CONTRAST = 3;
56
56
  export function inkOnSurface(surface, preferred) {
57
- if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
57
+ if (!isHexColor(surface) || !isHexColor(preferred))
58
58
  return preferred;
59
59
  if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
60
60
  return preferred;
@@ -66,7 +66,7 @@ export function inkOnSurface(surface, preferred) {
66
66
  // the bar cannot make the token fire on a theme that was already fine.
67
67
  export const ACCENT_INK_MIN_CONTRAST = 4.5;
68
68
  export function accentInkOn(ground, accent) {
69
- if (!/^#[0-9a-fA-F]{6}$/.test(ground) || !/^#[0-9a-fA-F]{6}$/.test(accent))
69
+ if (!isHexColor(ground) || !isHexColor(accent))
70
70
  return null;
71
71
  if (contrastRatio(accent, ground) >= ACCENT_INK_MIN_CONTRAST)
72
72
  return null;
@@ -84,7 +84,7 @@ export function accentInkOn(ground, accent) {
84
84
  return readableTextColor(ground);
85
85
  }
86
86
  export function mutedInkOnSurface(surface, preferred) {
87
- if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
87
+ if (!isHexColor(surface) || !isHexColor(preferred))
88
88
  return preferred;
89
89
  if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
90
90
  return preferred;
@@ -96,4 +96,3 @@ export function mutedInkOnSurface(surface, preferred) {
96
96
  // the full ink beside it.
97
97
  return mixHex(readableTextColor(surface), surface, 0.78);
98
98
  }
99
- //# sourceMappingURL=theme-color-math.js.map
@@ -0,0 +1,179 @@
1
+ // Compact SVGs (each <500 bytes). Color tokens get substituted at apply time.
2
+ const DRIP_BORDER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" preserveAspectRatio="none"><rect x="0" y="0" width="60" height="2" fill="__primary__"/><rect x="0" y="0" width="2" height="60" fill="__primary__"/><rect x="58" y="0" width="2" height="60" fill="__primary__"/><path d="M0,58 L60,58 L60,50 Q54,60 48,52 Q42,62 36,53 Q30,64 24,54 Q18,60 12,52 Q6,62 0,52 Z" fill="__primary__"/></svg>`;
3
+ const PIXEL_CORNERS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" shape-rendering="crispEdges"><path d="M0,0 H4 V2 H2 V4 H0 Z M8,0 H12 V4 H10 V2 H8 Z M0,8 H2 V10 H4 V12 H0 Z M10,8 H12 V12 H8 V10 H10 Z" fill="__primary__"/></svg>`;
4
+ const SCANLINE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4" preserveAspectRatio="none"><rect width="4" height="1" fill="__primary__" opacity="0.15"/></svg>`;
5
+ const VINE_CORNER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path d="M0,40 Q0,20 8,12 Q16,4 24,8 Q20,16 12,16 Q4,20 4,40 Z" fill="__success__" opacity="0.7"/><circle cx="10" cy="14" r="2" fill="__success__"/><circle cx="16" cy="10" r="1.5" fill="__success__"/></svg>`;
6
+ const GLOW_AURA_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><radialGradient id="g"><stop offset="0%" stop-color="__primary__" stop-opacity="0.5"/><stop offset="60%" stop-color="__primary__" stop-opacity="0.1"/><stop offset="100%" stop-color="__primary__" stop-opacity="0"/></radialGradient></defs><rect width="100" height="100" fill="url(#g)"/></svg>`;
7
+ const BRACKET_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0,0 H8 V1 H1 V8 H0 Z M16,0 H24 V8 H23 V1 H16 Z M0,16 H1 V23 H8 V24 H0 Z M16,24 V23 H23 V16 H24 V24 Z" fill="__primary__"/></svg>`;
8
+ const RIBBON_EDGE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" preserveAspectRatio="none"><path d="M0,0 L30,0 L30,4 L26,8 L22,4 L18,8 L14,4 L10,8 L6,4 L2,8 L0,4 Z" fill="__primary__"/></svg>`;
9
+ const CIRCUIT_TRACE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M0,16 H10 M22,16 H32 M16,0 V10 M16,22 V32" stroke="__primary__" stroke-width="1" fill="none"/><circle cx="16" cy="16" r="2" fill="none" stroke="__primary__" stroke-width="1"/></svg>`;
10
+ // A single soft-edged highlight bowing across the top. The flat-opacity ellipse
11
+ // this replaced had a hard rim, so at any size above a small chip it stopped
12
+ // reading as light and started reading as a grey shape sitting on the element.
13
+ const CANDY_GLOSS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40" preserveAspectRatio="none"><radialGradient id="cgh" cx=".5" cy=".28" r=".72"><stop offset="0" stop-color="#fff" stop-opacity=".38"/><stop offset=".55" stop-color="#fff" stop-opacity=".15"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient><ellipse cx="50" cy="8" rx="48" ry="13" fill="url(#cgh)"/></svg>`;
14
+ const ORNATE_CORNER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><g id="f"><path d="M2 14 Q2 2 14 2" fill="none" stroke="__primary__" stroke-width="2"/><path d="M8 8 L11 5 L14 8 L11 11 Z" fill="__primary__"/></g></defs><rect x="5" y="5" width="38" height="38" fill="none" stroke="__primary__" stroke-width="1.5"/><use href="#f"/><use href="#f" transform="rotate(90 24 24)"/><use href="#f" transform="rotate(180 24 24)"/><use href="#f" transform="rotate(270 24 24)"/></svg>`;
15
+ const ROYAL_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect x="3" y="3" width="42" height="42" rx="6" fill="none" stroke="__primary__" stroke-width="2"/><rect x="8" y="8" width="32" height="32" rx="3" fill="none" stroke="__primary__" stroke-width="1" opacity="0.6"/><circle cx="6" cy="6" r="2.4" fill="__primary__"/><circle cx="42" cy="6" r="2.4" fill="__primary__"/><circle cx="6" cy="42" r="2.4" fill="__primary__"/><circle cx="42" cy="42" r="2.4" fill="__primary__"/></svg>`;
16
+ const HAZARD_STRIPES_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M-4 4 L4 -4 M0 16 L16 0 M12 20 L20 12" stroke="__warning__" stroke-width="4" opacity="0.12" fill="none"/></svg>`;
17
+ const PIXEL_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges"><path fill-rule="evenodd" d="M0 0 H16 V16 H0 Z M1 1 V15 H15 V1 Z" fill="__text__"/><path fill-rule="evenodd" d="M3 3 H13 V13 H3 Z M4 4 V12 H12 V4 Z" fill="__primary__"/></svg>`;
18
+ const GRAIN_TEXTURE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><g fill="__text__" opacity="0.07"><rect x="3" y="5" width="1" height="1"/><rect x="11" y="2" width="1" height="1"/><rect x="19" y="9" width="1" height="1"/><rect x="27" y="4" width="1" height="1"/><rect x="35" y="12" width="1" height="1"/><rect x="6" y="17" width="1" height="1"/><rect x="15" y="22" width="1" height="1"/><rect x="24" y="18" width="1" height="1"/><rect x="33" y="26" width="1" height="1"/><rect x="2" y="29" width="1" height="1"/><rect x="12" y="34" width="1" height="1"/><rect x="22" y="31" width="1" height="1"/><rect x="30" y="37" width="1" height="1"/><rect x="38" y="33" width="1" height="1"/></g></svg>`;
19
+ export const HUD_DECORATIONS = {
20
+ 'drip-border': {
21
+ key: 'drip-border',
22
+ svg: DRIP_BORDER_SVG,
23
+ suitsSlots: ['borderImage', 'decorationAfter'],
24
+ borderImage: { borderSlice: 20, repeat: 'repeat' },
25
+ // Explicit tile width for the same reason as `circuit-trace` below, with
26
+ // one extra twist: this SVG also carries `preserveAspectRatio="none"`,
27
+ // so it has no intrinsic RATIO either. Under `background-size: auto`
28
+ // that resolves to 100% 100% — a single drip motif stretched across the
29
+ // whole strip, which is the opposite of drips repeating along an edge.
30
+ // `100%` for the height is deliberate: the strip is 12px and the drips
31
+ // should fill it.
32
+ background: { size: '60px 100%', position: 'bottom', repeat: 'repeat-x' },
33
+ evokes: 'Horror drips on edges',
34
+ },
35
+ 'pixel-corners': {
36
+ key: 'pixel-corners',
37
+ svg: PIXEL_CORNERS_SVG,
38
+ suitsSlots: ['borderImage'],
39
+ borderImage: { borderSlice: 4, repeat: 'stretch' },
40
+ evokes: '8-bit pixel corner brackets',
41
+ },
42
+ scanline: {
43
+ key: 'scanline',
44
+ svg: SCANLINE_SVG,
45
+ suitsSlots: ['backdrop'],
46
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
47
+ // `auto` scales ONE copy to the element instead of tiling. See the
48
+ // note on `circuit-trace`.
49
+ background: { size: '4px 4px', position: 'center', repeat: 'repeat' },
50
+ evokes: 'CRT scanlines tiled across the surface',
51
+ },
52
+ 'vine-corner': {
53
+ key: 'vine-corner',
54
+ svg: VINE_CORNER_SVG,
55
+ suitsSlots: ['borderImage', 'decorationBefore'],
56
+ borderImage: { borderSlice: 20, repeat: 'stretch' },
57
+ background: { size: 'auto', position: 'bottom-left', repeat: 'no-repeat' },
58
+ evokes: 'Tropical vines / leaves in corners',
59
+ },
60
+ 'glow-aura': {
61
+ key: 'glow-aura',
62
+ svg: GLOW_AURA_SVG,
63
+ suitsSlots: ['backdrop', 'decorationBefore', 'decorationAfter'],
64
+ background: { size: 'cover', position: 'center', repeat: 'no-repeat' },
65
+ evokes: 'Soft radial glow halo behind the element',
66
+ },
67
+ 'bracket-frame': {
68
+ key: 'bracket-frame',
69
+ svg: BRACKET_FRAME_SVG,
70
+ suitsSlots: ['borderImage'],
71
+ borderImage: { borderSlice: 8, repeat: 'stretch' },
72
+ evokes: 'Sci-fi targeting brackets on each corner',
73
+ },
74
+ 'ribbon-edge': {
75
+ key: 'ribbon-edge',
76
+ svg: RIBBON_EDGE_SVG,
77
+ suitsSlots: ['decorationAfter', 'decorationBefore'],
78
+ background: { size: '30px 100%', position: 'bottom', repeat: 'repeat-x' },
79
+ evokes: 'Bunting / banner saw-tooth edge',
80
+ },
81
+ 'circuit-trace': {
82
+ key: 'circuit-trace',
83
+ svg: CIRCUIT_TRACE_SVG,
84
+ suitsSlots: ['backdrop'],
85
+ // The tile size is EXPLICIT, and has to be. An inline SVG carrying only
86
+ // a `viewBox` has no intrinsic size, so `background-size: auto` does not
87
+ // mean "32x32" — it scales ONE copy to fill the element. This pattern
88
+ // shipped that way and drew a handful of giant crosshairs across the
89
+ // controls overlay instead of a fine circuit texture.
90
+ //
91
+ // Any tiling decoration added here needs the same treatment: state the
92
+ // tile size, or give the SVG width/height attributes.
93
+ background: { size: '32px 32px', position: 'center', repeat: 'repeat' },
94
+ evokes: 'PCB / circuit board trace pattern',
95
+ },
96
+ 'candy-gloss': {
97
+ key: 'candy-gloss',
98
+ svg: CANDY_GLOSS_SVG,
99
+ suitsSlots: ['backdrop', 'decorationBefore'],
100
+ // A FIXED-HEIGHT band, not `cover` and not `100% 100%`.
101
+ //
102
+ // `cover` was the shipped value and was simply wrong: this SVG carries
103
+ // preserveAspectRatio="none", i.e. it is built to be stretched, while
104
+ // `cover` honours the viewBox's 100:40 intrinsic ratio. On a tall element
105
+ // (a CONTROLS panel is ~220px high and ~180 wide) that scaled the image to
106
+ // 550x220 and centred it, so what you saw was the middle of a hugely
107
+ // magnified ellipse — a grey blob across the top third of the panel.
108
+ //
109
+ // `100% 100%` fixes the blob and is still wrong, because it makes the
110
+ // highlight a PROPORTION of the element. On that same panel the gloss then
111
+ // covers the top ~100px and the first rows of key text sit inside it:
112
+ // measured 2.22:1 and 2.92:1 against the muted label colour. A highlight
113
+ // is cast by a light source, so its size is set by the light and not by
114
+ // the object — a fixed band is both the physical answer and the one that
115
+ // cannot swallow a tall element. 56px reads as a full candy gloss on a
116
+ // 36-46px chip and as a sheen along the top edge of a panel.
117
+ background: { size: '100% 56px', position: 'top', repeat: 'no-repeat' },
118
+ evokes: 'Glossy sugar highlight across the top — candy button shine',
119
+ },
120
+ 'ornate-corner': {
121
+ key: 'ornate-corner',
122
+ svg: ORNATE_CORNER_SVG,
123
+ suitsSlots: ['borderImage'],
124
+ borderImage: { borderSlice: 14, repeat: 'stretch' },
125
+ evokes: 'Gothic filigree corner frame — dark fantasy, dungeon',
126
+ },
127
+ 'royal-frame': {
128
+ key: 'royal-frame',
129
+ svg: ROYAL_FRAME_SVG,
130
+ suitsSlots: ['borderImage'],
131
+ borderImage: { borderSlice: 12, repeat: 'stretch' },
132
+ evokes: 'Double gold-trim picture frame with corner studs — epic fantasy',
133
+ },
134
+ 'hazard-stripes': {
135
+ key: 'hazard-stripes',
136
+ svg: HAZARD_STRIPES_SVG,
137
+ suitsSlots: ['backdrop'],
138
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
139
+ // `auto` scales ONE copy to the element instead of tiling. See the
140
+ // note on `circuit-trace`.
141
+ background: { size: '16px 16px', position: 'center', repeat: 'repeat' },
142
+ evokes: 'Faint diagonal caution stripes — military / sci-fi warning panel',
143
+ },
144
+ 'pixel-frame': {
145
+ key: 'pixel-frame',
146
+ svg: PIXEL_FRAME_SVG,
147
+ suitsSlots: ['borderImage'],
148
+ borderImage: { borderSlice: 5, repeat: 'round' },
149
+ evokes: 'NES dialog double-ring pixel border',
150
+ },
151
+ 'grain-texture': {
152
+ key: 'grain-texture',
153
+ svg: GRAIN_TEXTURE_SVG,
154
+ suitsSlots: ['backdrop'],
155
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
156
+ // `auto` scales ONE copy to the element instead of tiling. See the
157
+ // note on `circuit-trace`.
158
+ background: { size: '40px 40px', position: 'center', repeat: 'repeat' },
159
+ evokes: 'Subtle parchment / leather grain tooth',
160
+ },
161
+ };
162
+ // Substitutes `__primary__` / `__danger__` etc. with hex codes from the active theme.
163
+ // Unknown tokens are left as-is so non-token usages of double-underscored strings
164
+ // (none in our SVGs today, but defensive) don't get mangled.
165
+ export function substituteThemeColors(svg, colors) {
166
+ return svg.replace(/__([a-zA-Z][a-zA-Z0-9]*)__/g, (match, name) => {
167
+ const key = name;
168
+ // `?? match` also covers optional colors (outline): a token for a color the
169
+ // theme doesn't define stays as-is instead of substituting "undefined".
170
+ const value = Object.prototype.hasOwnProperty.call(colors, key) ? colors[key] : undefined;
171
+ return value ?? match;
172
+ });
173
+ }
174
+ // Wraps SVG markup as a CSS url(...) data URI. The encoder uses encodeURIComponent
175
+ // (not base64) because SVG-as-data-URI is smaller that way and renders identically.
176
+ export function svgToDataUri(svg) {
177
+ const cleaned = svg.replace(/[\r\n]+/g, '').replace(/\s{2,}/g, ' ');
178
+ return `url("data:image/svg+xml;utf8,${encodeURIComponent(cleaned)}")`;
179
+ }
@@ -0,0 +1,147 @@
1
+ export const HUD_FONTS = {
2
+ 'red-hat-display': {
3
+ key: 'red-hat-display',
4
+ family: 'Red Hat Display',
5
+ weights: [300, 400, 500, 700],
6
+ suitsCase: ['upper', 'normal'],
7
+ evokes: 'Default Bitmagic — warm, technical, modern sans',
8
+ },
9
+ 'chakra-petch': {
10
+ key: 'chakra-petch',
11
+ family: 'Chakra Petch',
12
+ weights: [300, 400, 500, 600, 700],
13
+ suitsCase: ['upper', 'normal'],
14
+ evokes: 'Chamfered techno sans — holographic HUD, sci-fi console, cut corners',
15
+ },
16
+ 'ibm-plex-sans': {
17
+ key: 'ibm-plex-sans',
18
+ family: 'IBM Plex Sans',
19
+ weights: [300, 400, 500, 600, 700],
20
+ suitsCase: ['upper', 'normal'],
21
+ evokes: 'Engineered humanist sans — instrument panel, operator console, tabular data',
22
+ },
23
+ 'archivo-narrow': {
24
+ key: 'archivo-narrow',
25
+ family: 'Archivo Narrow',
26
+ weights: [400, 500, 600, 700],
27
+ suitsCase: ['upper', 'normal'],
28
+ evokes: 'Narrow gothic that keeps its ink — motorsport telemetry, broadcast overlay',
29
+ },
30
+ 'bebas-neue': {
31
+ key: 'bebas-neue',
32
+ family: 'Bebas Neue',
33
+ weights: [400],
34
+ suitsCase: ['upper', 'normal'],
35
+ evokes: 'Tall, bold display — sports, action, news ticker',
36
+ },
37
+ 'press-start-2p': {
38
+ key: 'press-start-2p',
39
+ family: 'Press Start 2P',
40
+ weights: [400],
41
+ suitsCase: ['upper', 'normal'],
42
+ evokes: 'Retro arcade pixel — 8-bit, NES, chiptune',
43
+ },
44
+ creepster: {
45
+ key: 'creepster',
46
+ family: 'Creepster',
47
+ weights: [400],
48
+ suitsCase: ['normal'],
49
+ evokes: 'Horror, halloween, dripping — already display-styled',
50
+ },
51
+ pacifico: {
52
+ key: 'pacifico',
53
+ family: 'Pacifico',
54
+ weights: [400],
55
+ suitsCase: ['normal'],
56
+ evokes: 'Handwritten cursive — tropical, beachy, casual',
57
+ },
58
+ 'space-grotesk': {
59
+ key: 'space-grotesk',
60
+ family: 'Space Grotesk',
61
+ weights: [400, 500, 700],
62
+ suitsCase: ['upper', 'normal'],
63
+ evokes: 'Geometric sans — sci-fi, technical, modern',
64
+ },
65
+ 'rubik-mono-one': {
66
+ key: 'rubik-mono-one',
67
+ family: 'Rubik Mono One',
68
+ weights: [400],
69
+ suitsCase: ['upper', 'normal'],
70
+ evokes: 'Heavy, slabby display — bold, blocky, attention-grabbing',
71
+ },
72
+ vt323: {
73
+ key: 'vt323',
74
+ family: 'VT323',
75
+ weights: [400],
76
+ suitsCase: ['upper', 'normal'],
77
+ evokes: 'CRT terminal monospace — retro computer, hacker, glitch',
78
+ },
79
+ orbitron: {
80
+ key: 'orbitron',
81
+ family: 'Orbitron',
82
+ weights: [400, 500, 700, 900],
83
+ suitsCase: ['upper', 'normal'],
84
+ evokes: 'Futuristic geometric — sci-fi, cyber, racing HUD',
85
+ },
86
+ shrikhand: {
87
+ key: 'shrikhand',
88
+ family: 'Shrikhand',
89
+ weights: [400],
90
+ suitsCase: ['normal'],
91
+ evokes: 'Italic display — celebratory, party, energetic',
92
+ },
93
+ 'baloo-2': {
94
+ key: 'baloo-2',
95
+ family: 'Baloo 2',
96
+ weights: [400, 500, 600, 700, 800],
97
+ suitsCase: ['upper', 'normal'],
98
+ evokes: 'Rounded chunky sans with a real weight range — mobile action-RPG chrome',
99
+ },
100
+ 'lilita-one': {
101
+ key: 'lilita-one',
102
+ family: 'Lilita One',
103
+ weights: [400],
104
+ suitsCase: ['upper', 'normal'],
105
+ evokes: 'Chunky rounded comic display — cartoon battler, toon town',
106
+ },
107
+ fredoka: {
108
+ key: 'fredoka',
109
+ family: 'Fredoka',
110
+ weights: [300, 400, 500, 700],
111
+ suitsCase: ['upper', 'normal'],
112
+ evokes: 'Soft rounded bubble sans — candy, casual mobile, sweet',
113
+ },
114
+ 'grenze-gotisch': {
115
+ key: 'grenze-gotisch',
116
+ family: 'Grenze Gotisch',
117
+ weights: [400, 500, 700, 900],
118
+ suitsCase: ['normal'],
119
+ evokes: 'Readable blackletter — dark fantasy, gothic dungeon',
120
+ },
121
+ cinzel: {
122
+ key: 'cinzel',
123
+ family: 'Cinzel',
124
+ weights: [400, 500, 700, 900],
125
+ suitsCase: ['upper', 'normal'],
126
+ evokes: 'Carved Roman caps — epic fantasy, ancient temple',
127
+ },
128
+ };
129
+ export const DEFAULT_FONT_STACK_OPTIONS = {
130
+ fallback: '"Helvetica Neue", Helvetica, Arial, system-ui, sans-serif',
131
+ };
132
+ export function getFontCssStack(key, opts = DEFAULT_FONT_STACK_OPTIONS) {
133
+ const entry = HUD_FONTS[key];
134
+ return `"${entry.family}", ${opts.fallback}`;
135
+ }
136
+ // Builds the Google Fonts CSS URL for a single font with the weights the theme needs.
137
+ // Published games only ever load the active theme's font, so this URL is the sole
138
+ // font request on the page.
139
+ export function buildGoogleFontsUrl(key, weights) {
140
+ const entry = HUD_FONTS[key];
141
+ const allowed = new Set(entry.weights);
142
+ const filtered = weights.filter(w => allowed.has(w));
143
+ const useWeights = filtered.length > 0 ? filtered : entry.weights;
144
+ const family = entry.family.replace(/ /g, '+');
145
+ const wtParam = useWeights.length > 0 ? `:wght@${[...useWeights].sort((a, b) => a - b).join(';')}` : '';
146
+ return `https://fonts.googleapis.com/css2?family=${family}${wtParam}&display=swap`;
147
+ }