@origingame/engine 0.2.0 → 0.3.0

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 (107) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +219 -5
  3. package/dist/web/origin-engine.js +1344 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +68 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1007 -22
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +162 -5
  9. package/dist/web-publish/origin-engine.js +1004 -19
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +52 -3
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1007 -22
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +162 -5
  15. package/dist/web-publish-2d/origin-engine.js +1004 -19
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +52 -3
  18. package/knowledge/looks/catalogue.json +40 -40
  19. package/knowledge/package.json +1 -1
  20. package/knowledge/scaffold/distribution.json +14 -0
  21. package/knowledge/scaffold/origin-engine.json +2 -2
  22. package/knowledge/scaffold/template/README.md +2 -1
  23. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  24. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  25. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  26. package/knowledge/scaffold/template/host.d.ts +46 -0
  27. package/knowledge/scaffold/template/package.json +1 -1
  28. package/knowledge/scaffold/template/project.ogt +1 -1
  29. package/knowledge/scaffold/template/src/main.ts +1 -1
  30. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  31. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  32. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  33. package/knowledge/scaffold/template/tsconfig.json +1 -1
  34. package/knowledge/scripts/mount.ts +2 -1
  35. package/knowledge/scripts/validate.ts +19 -10
  36. package/knowledge/skills/og-build/SKILL.md +8 -5
  37. package/knowledge/skills/og-core/SKILL.md +11 -0
  38. package/knowledge/skills/og-game/SKILL.md +877 -8
  39. package/knowledge/skills/og-platform-sdk/SKILL.md +1 -0
  40. package/knowledge/skills/og-taste/SKILL.md +82 -73
  41. package/knowledge/skills/og-ui/SKILL.md +65 -50
  42. package/knowledge/wiki/ai.md +20 -20
  43. package/knowledge/wiki/anim.md +384 -41
  44. package/knowledge/wiki/atmosphere.md +36 -13
  45. package/knowledge/wiki/audio.md +18 -18
  46. package/knowledge/wiki/camera.md +19 -13
  47. package/knowledge/wiki/commands.md +19 -13
  48. package/knowledge/wiki/debug.md +414 -0
  49. package/knowledge/wiki/game.md +8 -8
  50. package/knowledge/wiki/i18n.md +7 -7
  51. package/knowledge/wiki/index.md +262 -7
  52. package/knowledge/wiki/input.md +47 -47
  53. package/knowledge/wiki/load.md +236 -68
  54. package/knowledge/wiki/math.md +2607 -0
  55. package/knowledge/wiki/nav.md +34 -34
  56. package/knowledge/wiki/net.md +340 -0
  57. package/knowledge/wiki/path.md +402 -0
  58. package/knowledge/wiki/physics.md +95 -95
  59. package/knowledge/wiki/platform.md +463 -29
  60. package/knowledge/wiki/probe.md +681 -67
  61. package/knowledge/wiki/reflect.md +15 -15
  62. package/knowledge/wiki/render.md +3893 -1896
  63. package/knowledge/wiki/rng.md +14 -14
  64. package/knowledge/wiki/stream.md +15 -15
  65. package/knowledge/wiki/system.md +8 -8
  66. package/knowledge/wiki/systems.md +2 -2
  67. package/knowledge/wiki/terrain.md +19 -19
  68. package/knowledge/wiki/tilemap.md +13 -13
  69. package/knowledge/wiki/time.md +4 -4
  70. package/knowledge/wiki/timeline.md +396 -0
  71. package/knowledge/wiki/transform.md +8 -8
  72. package/knowledge/wiki/tween.md +17 -17
  73. package/knowledge/wiki/ui.md +559 -629
  74. package/knowledge/wiki/video.md +157 -0
  75. package/knowledge/wiki/world.md +36 -36
  76. package/package.json +4 -3
  77. package/schema/api.schema.json +21707 -14725
  78. package/shell/page.ts +26 -2
  79. package/shell/platform.ts +46 -0
  80. package/shell/player.ts +7 -0
  81. package/src/depth.ts +71 -0
  82. package/src/e6.ts +32 -1
  83. package/src/env.d.ts +5 -1
  84. package/src/generated/abi.ts +189 -3
  85. package/src/index.ts +25 -4
  86. package/src/light.ts +18 -0
  87. package/src/math.ts +622 -0
  88. package/src/node/index.ts +17 -0
  89. package/src/node/playtest-run.ts +114 -18
  90. package/src/node/polish.ts +13 -3
  91. package/src/node/render.ts +291 -0
  92. package/src/node/run.ts +86 -5
  93. package/src/p2.ts +19 -1
  94. package/src/playtest.ts +57 -4
  95. package/src/runtime.ts +13 -3
  96. package/src/targets.ts +28 -0
  97. package/src/trace.ts +169 -0
  98. package/src/ui-kit/huds.ts +34 -34
  99. package/src/ui-kit/menus.ts +12 -12
  100. package/src/ui-kit/settings.ts +2 -2
  101. package/src/ui-kit/store.ts +18 -18
  102. package/src/ui-kit/story.ts +6 -4
  103. package/src/ui-kit/util.ts +8 -8
  104. package/src/web/shell.ts +58 -0
  105. package/src/web/worker.ts +103 -2
  106. package/types/bindings.json +9101 -6647
  107. package/types/index.d.ts +15503 -11564
@@ -8,7 +8,7 @@
8
8
 
9
9
  import type * as Api from "@origingame/engine";
10
10
  import { kit } from "./bind.ts";
11
- import { all, capsule, clock, fit, fmt, frame, grow, iconOr, lapTime, px, token } from "./util.ts";
11
+ import { all, hudBox, clock, fit, fmt, frame, grow, iconOr, lapTime, px, token } from "./util.ts";
12
12
 
13
13
  type Hud = { paused: boolean; action: string | null; moveX: number; moveY: number; held: string[] };
14
14
 
@@ -27,7 +27,7 @@ function pauseButton(out: Hud, label: string): void {
27
27
  function stat(id: string, value: string, caption: string, step: Api.TypeStepName = "xl3", align: Api.TextAlign = "start"): void {
28
28
  kit.column(id, { gap: 0, cross: align === "end" ? "end" : align === "center" ? "center" : "start" }, () => {
29
29
  kit.label("Value", value, { step, font: "mono", nowrap: true, align });
30
- kit.label("Caption", caption, { step: "meta", color: token("fgMuted"), nowrap: true, align });
30
+ kit.label("Caption", caption, { step: "meta", color: token("inkMuted"), nowrap: true, align });
31
31
  });
32
32
  }
33
33
 
@@ -58,36 +58,36 @@ export function platformerHud(id: string, desc: Api.PlatformerHudDesc = {}): Api
58
58
  const { t, touch } = frame();
59
59
  const out = hud();
60
60
  // What the game leaves out is not drawn: no hearts with maxLives 0 (and
61
- // no health bar), no coins or score capsule without coins or score.
61
+ // no health bar), no coins or score box without coins or score.
62
62
  const health = desc.health ?? null;
63
63
  const maxLives = desc.maxLives ?? 3;
64
64
  const coins = desc.coins ?? null;
65
65
  const score = desc.score ?? null;
66
66
  if (health !== null || maxLives > 0 || desc.level) {
67
67
  kit.anchor(`${id}.left`, { anchor: "topLeft", direction: "column", gap: t.sm }, () => {
68
- if (health !== null) capsule("Lives", () => kit.meter("Health", health, 1, { width: px(120) }));
68
+ if (health !== null) hudBox("Lives", () => kit.meter("Health", health, 1, { width: px(120) }));
69
69
  else if (maxLives > 0) {
70
- capsule("Lives", () => {
71
- for (let i = 0; i < maxLives; i++) iconOr(kit.idi("Life", i), "heart", 20, token(i < (desc.lives ?? 0) ? "danger" : "fillStrong"));
70
+ hudBox("Lives", () => {
71
+ for (let i = 0; i < maxLives; i++) iconOr(kit.idi("Life", i), "heart", 20, token(i < (desc.lives ?? 0) ? "danger" : "controlStrong"));
72
72
  });
73
73
  }
74
- if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("fgMuted"), nowrap: true });
74
+ if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("inkMuted"), nowrap: true });
75
75
  });
76
76
  }
77
77
  if (desc.timeLeft !== null && desc.timeLeft !== undefined) {
78
78
  const low = desc.timeLeft <= 10;
79
79
  kit.anchor(`${id}.top`, { anchor: "top" }, () => {
80
- capsule("Timer", () => kit.label("Time", clock(desc.timeLeft!), { step: "lg", font: "mono", color: token(low ? "danger" : "fg"), nowrap: true }));
80
+ hudBox("Timer", () => kit.label("Time", clock(desc.timeLeft!), { step: "lg", font: "mono", color: token(low ? "danger" : "ink"), nowrap: true }));
81
81
  });
82
82
  }
83
83
  kit.anchor(`${id}.right`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
84
84
  if (coins !== null) {
85
- capsule("Coins", () => {
86
- iconOr("Coin", "coin", 20, token("accent"));
85
+ hudBox("Coins", () => {
86
+ iconOr("Coin", "coin", 20, token("gold"));
87
87
  kit.label("Count", fmt(coins), { font: "mono", nowrap: true });
88
88
  });
89
89
  }
90
- if (score !== null) capsule("Score", () => kit.label("Points", fmt(score), { font: "mono", nowrap: true }));
90
+ if (score !== null) hudBox("Score", () => kit.label("Points", fmt(score), { font: "mono", nowrap: true }));
91
91
  pauseButton(out, desc.pauseLabel ?? "Pause");
92
92
  });
93
93
  if ((desc.touch ?? false) && touch) touchControls(out, { dpad: true, buttons: [{ id: "jump", label: desc.jumpLabel ?? "Jump", primary: true }] });
@@ -96,7 +96,7 @@ export function platformerHud(id: string, desc: Api.PlatformerHudDesc = {}): Api
96
96
 
97
97
  /** A crosshair at the centre: four short strokes and a gap. */
98
98
  function crosshair(): void {
99
- const ink = token("fg", 0.9);
99
+ const ink = token("ink", 0.9);
100
100
  const bar = (id: string, w: number, h: number): void => kit.image(id, { tint: ink, width: px(w), height: px(h) });
101
101
  kit.anchor("Crosshair", { anchor: "center", direction: "column", cross: "center", gap: 6 }, () => {
102
102
  bar("N", 2, 8);
@@ -121,9 +121,9 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
121
121
  });
122
122
  if (desc.map && !showTouch) kit.anchor(`${id}.map`, { anchor: "topLeft" }, () => kit.minimap("Map", desc.map!));
123
123
  kit.anchor(`${id}.top`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
124
- capsule("Kills", () => {
124
+ hudBox("Kills", () => {
125
125
  kit.label("Count", fmt(desc.kills ?? 0), { font: "mono", nowrap: true });
126
- kit.label("Unit", desc.killsLabel ?? "kills", { step: "meta", color: token("fgMuted"), nowrap: true });
126
+ kit.label("Unit", desc.killsLabel ?? "kills", { step: "meta", color: token("inkMuted"), nowrap: true });
127
127
  });
128
128
  pauseButton(out, desc.pauseLabel ?? "Pause");
129
129
  });
@@ -131,10 +131,10 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
131
131
  const mag = desc.magazine ?? 0;
132
132
  const ammo = desc.ammo ?? 0;
133
133
  kit.row("Rounds", { gap: t.xs, cross: "end" }, () => {
134
- kit.label("Ammo", `${ammo}`, { step: "xl5", font: "mono", color: token(mag > 0 && ammo <= Math.floor(mag / 4) ? "warn" : "fg"), nowrap: true });
135
- kit.label("Reserve", `/ ${desc.reserve ?? 0}`, { step: "lg", font: "mono", color: token("fgMuted"), nowrap: true });
134
+ kit.label("Ammo", `${ammo}`, { step: "xl5", font: "mono", color: token(mag > 0 && ammo <= Math.floor(mag / 4) ? "warn" : "ink"), nowrap: true });
135
+ kit.label("Reserve", `/ ${desc.reserve ?? 0}`, { step: "lg", font: "mono", color: token("inkMuted"), nowrap: true });
136
136
  });
137
- if (desc.weapon) kit.label("Weapon", desc.weapon, { step: "meta", color: token("fgMuted"), nowrap: true });
137
+ if (desc.weapon) kit.label("Weapon", desc.weapon, { step: "meta", color: token("inkMuted"), nowrap: true });
138
138
  });
139
139
  const abilities = desc.abilities ?? [];
140
140
  if (abilities.length > 0) {
@@ -158,20 +158,20 @@ export function puzzleHud(id: string, desc: Api.PuzzleHudDesc = {}): Api.HudResu
158
158
  const score = desc.score ?? 0;
159
159
  const marks = desc.stars && desc.stars.length > 0 ? desc.stars : [Math.round(target / 3), Math.round((2 * target) / 3), target];
160
160
  kit.anchor(`${id}.left`, { anchor: "topLeft", direction: "column", gap: t.xs }, () => {
161
- if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("fgMuted"), nowrap: true });
161
+ if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("inkMuted"), nowrap: true });
162
162
  if (desc.moves !== null && desc.moves !== undefined) stat("Moves", `${desc.moves}`, desc.movesLabel ?? "moves left", "xl3");
163
163
  });
164
164
  kit.anchor(`${id}.score`, { anchor: "top", direction: "column", cross: "center", gap: t.xs, width: px(260) }, () => {
165
165
  kit.label("Score", fmt(score), { step: "displaySm", font: "display", align: "center", width: grow() });
166
- kit.meter("Progress", score, target, { kind: "accent", width: grow(), segments: 3 });
166
+ kit.meter("Progress", score, target, { kind: "primary", width: grow(), segments: 3 });
167
167
  kit.row("Stars", { gap: t.sm, cross: "center" }, () => {
168
- marks.slice(0, 3).forEach((m, i) => iconOr(kit.idi("Star", i), "star", 20, token(score >= m ? "accent" : "fillStrong")));
169
- kit.label("Target", (desc.targetLabel ?? "Target {target}").replaceAll("{target}", fmt(target)), { step: "meta", color: token("fgMuted"), nowrap: true });
168
+ marks.slice(0, 3).forEach((m, i) => iconOr(kit.idi("Star", i), "star", 20, token(score >= m ? "primary" : "controlStrong")));
169
+ kit.label("Target", (desc.targetLabel ?? "Target {target}").replaceAll("{target}", fmt(target)), { step: "meta", color: token("inkMuted"), nowrap: true });
170
170
  });
171
171
  });
172
172
  kit.anchor(`${id}.right`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
173
173
  if (desc.timeLeft !== null && desc.timeLeft !== undefined) {
174
- capsule("Timer", () => kit.label("Time", clock(desc.timeLeft!), { font: "mono", color: token(desc.timeLeft! <= 10 ? "danger" : "fg"), nowrap: true }));
174
+ hudBox("Timer", () => kit.label("Time", clock(desc.timeLeft!), { font: "mono", color: token(desc.timeLeft! <= 10 ? "danger" : "ink"), nowrap: true }));
175
175
  }
176
176
  pauseButton(out, desc.pauseLabel ?? "Pause");
177
177
  });
@@ -185,8 +185,8 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
185
185
  const generators = desc.generators ?? [];
186
186
  kit.anchor(`${id}.wallet`, { anchor: "top", gap: t.sm, cross: "center" }, () => {
187
187
  currencies.forEach((c, i) => {
188
- capsule(c.id ?? kit.idi("Currency", i), () => {
189
- if (c.icon) iconOr("Icon", c.icon, 20, token("accent"));
188
+ hudBox(c.id ?? kit.idi("Currency", i), () => {
189
+ if (c.icon) iconOr("Icon", c.icon, 20, token("primary"));
190
190
  kit.label("Amount", fmt(c.amount ?? 0), { step: i === 0 ? "lg" : "body", font: "mono", nowrap: true });
191
191
  if ((c.perSecond ?? 0) !== 0) kit.label("Rate", (desc.rateLabel ?? "+{rate}/s").replaceAll("{rate}", fmt(c.perSecond ?? 0)), { step: "meta", color: token("success"), nowrap: true });
192
192
  }, { height: i === 0 ? t.controlLg : t.controlMd });
@@ -204,13 +204,13 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
204
204
  kit.panel("Generators", { width: grow(), padding: all(t.sm), gap: t.xs }, () => {
205
205
  kit.list("List", { count: generators.length, rowHeight: 64, gap: t.xs, height: fit(0, listHeight), width: grow() }, (i) => {
206
206
  const g = generators[i]!;
207
- if (g.icon) iconOr("Icon", g.icon, 32, token("fg"));
207
+ if (g.icon) iconOr("Icon", g.icon, 32, token("ink"));
208
208
  kit.column("Text", { width: grow(), gap: t.xxs }, () => {
209
209
  kit.row("Head", { gap: t.sm, cross: "center", width: grow() }, () => {
210
210
  kit.label("Name", g.name ?? g.id ?? "", { step: "lg", nowrap: true, width: grow() });
211
211
  kit.badge("Level", (desc.levelLabel ?? "Lv {level}").replaceAll("{level}", `${g.level ?? 0}`), "neutral");
212
212
  });
213
- kit.meter("Cycle", g.progress ?? 0, 1, { kind: "accent", height: 4, width: grow() });
213
+ kit.meter("Cycle", g.progress ?? 0, 1, { kind: "primary", height: 4, width: grow() });
214
214
  });
215
215
  const buy = kit.button("Buy", fmt(g.cost ?? 0), { size: "small", variant: g.affordable ? "primary" : "secondary", disabled: !g.affordable }) as Api.Response;
216
216
  if (buy.clicked) out.action = `buy:${g.id ?? i}`;
@@ -229,7 +229,7 @@ function deltaText(seconds: number): string {
229
229
 
230
230
  /** A delta's ink: ahead (negative) success, behind danger, even muted. */
231
231
  function deltaInk(seconds: number): Api.Paint {
232
- return token(seconds < 0 ? "success" : seconds > 0 ? "danger" : "fgMuted");
232
+ return token(seconds < 0 ? "success" : seconds > 0 ? "danger" : "inkMuted");
233
233
  }
234
234
 
235
235
  export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResult {
@@ -244,7 +244,7 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
244
244
  if (racers > 0) {
245
245
  kit.row("Position", { gap: t.xxs, cross: "end" }, () => {
246
246
  kit.label("Place", `${desc.position ?? 1}`, { step: "displayXl", font: "display", nowrap: true });
247
- kit.label("Of", `/${racers}`, { step: "xl", font: "mono", color: token("fgMuted"), nowrap: true });
247
+ kit.label("Of", `/${racers}`, { step: "xl", font: "mono", color: token("inkMuted"), nowrap: true });
248
248
  });
249
249
  }
250
250
  stat("Lap", `${Math.min(desc.lap ?? 1, desc.laps ?? 3)}/${desc.laps ?? 3}`, desc.lapLabel ?? "lap", "xl3");
@@ -258,16 +258,16 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
258
258
  if (delta !== null) kit.label("Delta", deltaText(delta), { step: "lg", font: "mono", color: deltaInk(delta), nowrap: true });
259
259
  if (desc.best !== null && desc.best !== undefined) {
260
260
  // A finished lap: m:ss.mmm as lapTime cuts it (30.717 s is 0:30.717, where the running clock's floor reads 0:30.716).
261
- kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", lapTime(desc.best)), { step: "meta", font: "mono", color: token("fgMuted"), nowrap: true });
261
+ kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", lapTime(desc.best)), { step: "meta", font: "mono", color: token("inkMuted"), nowrap: true });
262
262
  }
263
263
  const splits = desc.splits ?? [];
264
264
  if (splits.length > 0) {
265
265
  kit.column("Splits", { gap: 0, cross: "end" }, () => {
266
266
  splits.forEach((sp, i) => {
267
267
  kit.row(kit.idi("Split", i), { gap: t.sm, cross: "center" }, () => {
268
- kit.label("Label", sp.label ?? "", { step: "meta", color: token("fgMuted"), nowrap: true });
268
+ kit.label("Label", sp.label ?? "", { step: "meta", color: token("inkMuted"), nowrap: true });
269
269
  const time = sp.time ?? null;
270
- kit.label("Time", time === null ? "-:--.---" : lapTime(time), { step: "meta", font: "mono", color: token(time === null ? "fgSubtle" : "fg"), nowrap: true });
270
+ kit.label("Time", time === null ? "-:--.---" : lapTime(time), { step: "meta", font: "mono", color: token(time === null ? "inkFaint" : "ink"), nowrap: true });
271
271
  const d = sp.delta ?? null;
272
272
  if (d !== null) kit.label("Delta", deltaText(d), { step: "meta", font: "mono", color: deltaInk(d), nowrap: true });
273
273
  });
@@ -280,11 +280,11 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
280
280
  kit.row("Speed", { gap: t.sm, cross: "end", width: grow(), align: "end" }, () => {
281
281
  kit.label("Value", `${Math.round(desc.speed ?? 0)}`, { step: "displayXl2", font: "display", nowrap: true });
282
282
  kit.column("Side", { cross: "end" }, () => {
283
- kit.label("Gear", desc.gear ?? "N", { step: "xl", font: "mono", color: token("accentInk"), nowrap: true });
284
- kit.label("Unit", desc.unit ?? "km/h", { step: "meta", color: token("fgMuted"), nowrap: true });
283
+ kit.label("Gear", desc.gear ?? "N", { step: "xl", font: "mono", color: token("highlight"), nowrap: true });
284
+ kit.label("Unit", desc.unit ?? "km/h", { step: "meta", color: token("inkMuted"), nowrap: true });
285
285
  });
286
286
  });
287
- kit.meter("Rev", desc.speed ?? 0, desc.maxSpeed ?? 300, { kind: "accent", width: grow(), height: 6 });
287
+ kit.meter("Rev", desc.speed ?? 0, desc.maxSpeed ?? 300, { kind: "primary", width: grow(), height: 6 });
288
288
  if (boost !== null) kit.meter("Boost", boost, 1, { color: token("info"), label: boostLabel, width: grow(), segments: 4 });
289
289
  });
290
290
  if (showTouch) {
@@ -36,17 +36,17 @@ export function mainMenu(id: string, desc: Api.MainMenuDesc = {}): Api.MenuResul
36
36
  cross: "center",
37
37
  width: phone ? grow() : px(380),
38
38
  padding: all(phone ? t.lg : t.xl),
39
- background: token("materialThick"),
39
+ background: token("hud"),
40
40
  radius: t.panel,
41
41
  instant: desc.instant ?? false,
42
42
  }, () => {
43
43
  kit.column("Heading", { gap: t.xs, cross: "center", width: grow() }, () => {
44
44
  kit.label("Title", desc.title ?? "", { step: phone ? "displayMd" : "displayLg", font: "display", align: "center", width: grow(), heading: true });
45
- if (desc.subtitle) kit.label("Subtitle", desc.subtitle, { color: token("fgMuted"), align: "center", width: grow() });
45
+ if (desc.subtitle) kit.label("Subtitle", desc.subtitle, { color: token("inkMuted"), align: "center", width: grow() });
46
46
  });
47
47
  // A title menu is not a dialog, so it takes the focus itself: Enter and the pad's A answer at once.
48
48
  chosen = menuColumn("Items", items, grow(), true);
49
- if (desc.footer) kit.label("Footer", desc.footer, { step: "meta", color: token("fgSubtle"), align: "center", width: grow() });
49
+ if (desc.footer) kit.label("Footer", desc.footer, { step: "meta", color: token("inkFaint"), align: "center", width: grow() });
50
50
  });
51
51
  if (chosen !== null) desc.onSelect?.(chosen);
52
52
  return { selected: chosen };
@@ -68,7 +68,7 @@ export function confirmDialog(id: string, desc: Api.ConfirmDesc = {}): Api.Confi
68
68
  let confirmed = false;
69
69
  let cancelled = false;
70
70
  const m = kit.modal(id, desc.open ?? false, { title: desc.title ?? "" }, () => {
71
- if (desc.message) kit.richText("Message", desc.message, { step: "reading", color: token("fgMuted"), width: grow() });
71
+ if (desc.message) kit.richText("Message", desc.message, { step: "reading", color: token("inkMuted"), width: grow() });
72
72
  kit.row("Actions", { width: grow(), gap: t.sm, align: "end", padding: { left: 0, top: t.sm, right: 0, bottom: 0 } }, () => {
73
73
  if ((kit.button("Cancel", desc.cancelLabel ?? "Cancel", {}) as Api.Response).clicked) cancelled = true;
74
74
  if ((kit.button("Confirm", desc.confirmLabel ?? "Confirm", { variant: desc.danger ? "danger" : "primary" }) as Api.Response).clicked) confirmed = true;
@@ -82,7 +82,7 @@ export function alertDialog(id: string, desc: Api.AlertDesc = {}): boolean {
82
82
  const { t } = frame();
83
83
  let done = false;
84
84
  const m = kit.modal(id, desc.open ?? false, { title: desc.title ?? "" }, () => {
85
- if (desc.message) kit.richText("Message", desc.message, { step: "reading", color: token("fgMuted"), width: grow() });
85
+ if (desc.message) kit.richText("Message", desc.message, { step: "reading", color: token("inkMuted"), width: grow() });
86
86
  kit.row("Actions", { width: grow(), align: "end", padding: { left: 0, top: t.sm, right: 0, bottom: 0 } }, () => {
87
87
  if ((kit.button("Ok", desc.okLabel ?? "OK", { variant: "primary" }) as Api.Response).clicked) done = true;
88
88
  });
@@ -94,7 +94,7 @@ export function loadingScreen(id: string, desc: Api.LoadingDesc = {}): void {
94
94
  const { t } = frame();
95
95
  const tips = desc.tips ?? [];
96
96
  const tip = tips.length > 0 ? tips[Math.floor(kit.seconds() / Math.max(1, desc.tipSeconds ?? 4)) % tips.length]! : "";
97
- kit.modal(id, true, { locked: true, bare: true, backdrop: token("bg"), transition: "fade", width: px(Math.min(380, frame().viewport.width - 32)) }, () => {
97
+ kit.modal(id, true, { locked: true, bare: true, backdrop: token("backdrop"), transition: "fade", width: px(Math.min(380, frame().viewport.width - 32)) }, () => {
98
98
  kit.label("Title", desc.title ?? "Loading", { step: "xl", align: "center", width: grow(), heading: true });
99
99
  const p = desc.progress;
100
100
  if (p === null || p === undefined) {
@@ -103,10 +103,10 @@ export function loadingScreen(id: string, desc: Api.LoadingDesc = {}): void {
103
103
  // A real value only: the bar never runs ahead of what loaded.
104
104
  kit.row("Bar", { width: grow(), gap: t.md, cross: "center" }, () => {
105
105
  kit.progress("Progress", p, { width: grow() });
106
- kit.label("Percent", `${Math.floor(Math.max(0, Math.min(1, p)) * 100)}%`, { step: "meta", font: "mono", color: token("fgMuted"), nowrap: true });
106
+ kit.label("Percent", `${Math.floor(Math.max(0, Math.min(1, p)) * 100)}%`, { step: "meta", font: "mono", color: token("inkMuted"), nowrap: true });
107
107
  });
108
108
  }
109
- if (tip) kit.label("Tip", tip, { step: "reading", color: token("fgMuted"), align: "center", width: grow() });
109
+ if (tip) kit.label("Tip", tip, { step: "reading", color: token("inkMuted"), align: "center", width: grow() });
110
110
  });
111
111
  }
112
112
 
@@ -118,7 +118,7 @@ function stars(id: string, earned: number, max: number, since: number, size: num
118
118
  const shown = kit.seconds() - since >= 0.3 * (k + 1);
119
119
  const on = k < earned;
120
120
  kit.transition(kit.idi("Star", k), shown, { kind: "scale", instant: false }, () => {
121
- iconOr("Mark", "star", size, token(on ? "accent" : "fillStrong"));
121
+ iconOr("Mark", "star", size, token(on ? "primary" : "controlStrong"));
122
122
  if (on) kit.burst({ delayMs: 120 });
123
123
  });
124
124
  if (!shown) kit.spacer(kit.idi("Gap", k), px(size));
@@ -147,7 +147,7 @@ export function resultsScreen(id: string, desc: Api.ResultsDesc = {}): Api.MenuR
147
147
  const max = desc.maxStars ?? 3;
148
148
  if (max > 0) kit.row("StarRow", { width: grow(), align: "center" }, () => stars("Stars", desc.stars ?? 0, max, since, 44));
149
149
  kit.column("ScoreBlock", { width: grow(), cross: "center", gap: t.xxs }, () => {
150
- if (desc.scoreLabel) kit.label("ScoreLabel", desc.scoreLabel, { step: "meta", color: token("fgMuted"), align: "center", width: grow(), nowrap: true });
150
+ if (desc.scoreLabel) kit.label("ScoreLabel", desc.scoreLabel, { step: "meta", color: token("inkMuted"), align: "center", width: grow(), nowrap: true });
151
151
  if (time) {
152
152
  // A time counts up as a clock (ease-out cubic over the same 0.9 s), the headline in m:ss.mmm.
153
153
  const k = open ? Math.min(1, Math.max(0, (kit.seconds() - since) / 0.9)) : 1;
@@ -158,8 +158,8 @@ export function resultsScreen(id: string, desc: Api.ResultsDesc = {}): Api.MenuR
158
158
  }
159
159
  if (!time || best > 0 || desc.newBest) {
160
160
  kit.row("BestRow", { gap: t.sm, cross: "center" }, () => {
161
- if (!time || best > 0) kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", time ? lapTime(best) : fmt(best)), { step: "meta", color: token("fgMuted"), nowrap: true });
162
- if (desc.newBest) kit.transition("Reveal", landed, { kind: "pop" }, () => kit.badge("NewBest", desc.newBestLabel ?? "New best", "accent"));
161
+ if (!time || best > 0) kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", time ? lapTime(best) : fmt(best)), { step: "meta", color: token("inkMuted"), nowrap: true });
162
+ if (desc.newBest) kit.transition("Reveal", landed, { kind: "pop" }, () => kit.badge("NewBest", desc.newBestLabel ?? "New best", "primary"));
163
163
  });
164
164
  }
165
165
  });
@@ -40,7 +40,7 @@ function sliderRow(id: string, label: string, value: number, desc: Api.SliderDes
40
40
  kit.label("Label", label, { width: px(phone ? 96 : 140), nowrap: true });
41
41
  const r = kit.slider("Slider", value, desc) as Api.SliderResult;
42
42
  out = r.value;
43
- kit.label("Value", shown, { width: px(48), align: "end", font: "mono", color: token("fgMuted"), nowrap: true });
43
+ kit.label("Value", shown, { width: px(48), align: "end", font: "mono", color: token("inkMuted"), nowrap: true });
44
44
  });
45
45
  return out;
46
46
  }
@@ -82,7 +82,7 @@ export function settingsScreen(id: string, desc: Api.SettingsDesc = {}): Api.Set
82
82
  }
83
83
  case "graphics": {
84
84
  const quality = desc.qualityLabel ?? "Quality";
85
- kit.label("QualityLabel", quality, { step: "meta", color: token("fgMuted") });
85
+ kit.label("QualityLabel", quality, { step: "meta", color: token("inkMuted") });
86
86
  const levels = [desc.lowLabel ?? "Low", desc.mediumLabel ?? "Medium", desc.highLabel ?? "High"];
87
87
  const q = kit.radio("Quality", levels, QUALITIES.indexOf(s.quality), { horizontal: !phone, label: quality });
88
88
  s.quality = QUALITIES[q.selected] ?? "medium";
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type * as Api from "@origingame/engine";
6
6
  import { kit } from "./bind.ts";
7
- import { capsule, fit, fmt, frame, grow, iconOr, px, token } from "./util.ts";
7
+ import { hudBox, fit, fmt, frame, grow, iconOr, px, token } from "./util.ts";
8
8
 
9
9
  /** A screen panel: a dialog over the game, wide on desktop, full width on a phone. With `scrim` a press on the backdrop closes it too. */
10
10
  function screen(id: string, title: string, width: number, build: () => void, scrim = false): boolean {
@@ -16,7 +16,7 @@ function screen(id: string, title: string, width: number, build: () => void, scr
16
16
  /** An item's picture: its icon, else its initial on the slot. */
17
17
  function picture(id: string, icon: string | undefined, name: string, size: number): void {
18
18
  if (icon && kit.hasIcon(icon)) kit.icon(id, icon, { size });
19
- else kit.label(id, name.slice(0, 1).toUpperCase(), { step: "xl2", font: "display", color: token("fgMuted"), nowrap: true });
19
+ else kit.label(id, name.slice(0, 1).toUpperCase(), { step: "xl2", font: "display", color: token("inkMuted"), nowrap: true });
20
20
  }
21
21
 
22
22
  export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult {
@@ -37,10 +37,10 @@ export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult
37
37
  } else {
38
38
  kit.spacer("Gap", null);
39
39
  }
40
- capsule("Balance", () => {
41
- if (desc.currencyIcon) iconOr("Icon", desc.currencyIcon, 18, token("accent"));
40
+ hudBox("Balance", () => {
41
+ if (desc.currencyIcon) iconOr("Icon", desc.currencyIcon, 18, token("gold"));
42
42
  kit.counter("Amount", balance, { font: "mono", step: "base" });
43
- kit.label("Unit", desc.currency ?? "coins", { step: "base", color: token("fgMuted"), nowrap: true });
43
+ kit.label("Unit", desc.currency ?? "coins", { step: "base", color: token("inkMuted"), nowrap: true });
44
44
  });
45
45
  });
46
46
  const body = (): void => {
@@ -49,17 +49,17 @@ export function shopScreen(id: string, desc: Api.ShopDesc = {}): Api.ShopResult
49
49
  picture("Picture", it.icon, it.name ?? "", 36);
50
50
  kit.label("Name", it.name ?? "", { step: "base", nowrap: true });
51
51
  if (it.owned) kit.badge("Owned", desc.ownedLabel ?? "Owned", "success");
52
- else kit.label("Price", fmt(it.price ?? 0), { step: "meta", font: "mono", color: token(balance >= (it.price ?? 0) ? "accentInk" : "fgSubtle"), nowrap: true });
52
+ else kit.label("Price", fmt(it.price ?? 0), { step: "meta", font: "mono", color: token(balance >= (it.price ?? 0) ? "highlight" : "inkFaint"), nowrap: true });
53
53
  });
54
54
  if (g.clicked !== null && g.clicked !== undefined) sel = g.clicked;
55
55
  const it = items[sel];
56
56
  kit.column("Detail", { width: phone ? grow() : px(220), gap: t.sm }, () => {
57
57
  if (!it) {
58
- kit.label("Empty", desc.emptyLabel ?? "Nothing for sale here", { color: token("fgMuted") });
58
+ kit.label("Empty", desc.emptyLabel ?? "Nothing for sale here", { color: token("inkMuted") });
59
59
  return;
60
60
  }
61
61
  kit.label("Name", it.name ?? "", { step: "lg", width: grow() });
62
- if (it.description) kit.label("Description", it.description, { step: "meta", color: token("fgMuted"), width: grow() });
62
+ if (it.description) kit.label("Description", it.description, { step: "meta", color: token("inkMuted"), width: grow() });
63
63
  kit.keyValue("Price", desc.priceLabel ?? "Price", `${fmt(it.price ?? 0)} ${desc.currency ?? "coins"}`, true);
64
64
  const short = balance < (it.price ?? 0);
65
65
  // Short of the price, Buy still answers: it refuses, shaking with the error sound.
@@ -101,7 +101,7 @@ export function inventoryScreen(id: string, desc: Api.InventoryDesc = {}): Api.I
101
101
  const it = bySlot.get(i);
102
102
  if (!it) return;
103
103
  picture("Picture", it.icon, it.name ?? "", cell * 0.5);
104
- if ((it.count ?? 0) > 1) kit.label("Count", `${it.count}`, { step: "xs", font: "mono", color: token("fgMuted"), nowrap: true });
104
+ if ((it.count ?? 0) > 1) kit.label("Count", `${it.count}`, { step: "xs", font: "mono", color: token("inkMuted"), nowrap: true });
105
105
  });
106
106
  if (g.clicked !== null && g.clicked !== undefined) sel = g.clicked;
107
107
  if (g.dropFrom !== null && g.dropFrom !== undefined && g.dropTo !== null && g.dropTo !== undefined && bySlot.has(g.dropFrom)) {
@@ -111,11 +111,11 @@ export function inventoryScreen(id: string, desc: Api.InventoryDesc = {}): Api.I
111
111
  const it = sel >= 0 ? bySlot.get(sel) : undefined;
112
112
  kit.column("Detail", { width: phone ? grow() : px(200), gap: t.sm }, () => {
113
113
  if (!it) {
114
- kit.label("Hint", desc.hintLabel ?? "Select an item. Drag it to another slot to move it.", { step: "meta", color: token("fgMuted"), width: grow() });
114
+ kit.label("Hint", desc.hintLabel ?? "Select an item. Drag it to another slot to move it.", { step: "meta", color: token("inkMuted"), width: grow() });
115
115
  return;
116
116
  }
117
117
  kit.label("Name", it.name ?? "", { step: "lg", width: grow() });
118
- if (it.description) kit.label("Description", it.description, { step: "meta", color: token("fgMuted"), width: grow() });
118
+ if (it.description) kit.label("Description", it.description, { step: "meta", color: token("inkMuted"), width: grow() });
119
119
  if ((it.count ?? 0) > 1) kit.keyValue("Count", desc.countLabel ?? "Count", `${it.count}`, true);
120
120
  if (desc.useLabel !== "") {
121
121
  if ((kit.button("Use", desc.useLabel ?? "Use", { variant: "primary", width: grow() }) as Api.Response).clicked) out.used = it.id ?? null;
@@ -143,8 +143,8 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
143
143
  // Back from every input: the button, a press on the scrim, Escape, B.
144
144
  out.closed = screen(id, desc.title ?? "Levels", 720, () => {
145
145
  kit.row("Total", { gap: t.xs, cross: "center" }, () => {
146
- iconOr("Star", "star", 18, token("accent"), starsLabel(earned, levels.length * 3));
147
- kit.label("Count", `${earned} / ${levels.length * 3}`, { font: "mono", color: token("fgMuted"), nowrap: true });
146
+ iconOr("Star", "star", 18, token("primary"), starsLabel(earned, levels.length * 3));
147
+ kit.label("Count", `${earned} / ${levels.length * 3}`, { font: "mono", color: token("inkMuted"), nowrap: true });
148
148
  });
149
149
  // A numeral is the card's face, in display type on a square card; a
150
150
  // name takes a wider card and a smaller step, wrapped to two lines (cut
@@ -157,14 +157,14 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
157
157
  const l = levels[i]!;
158
158
  const name = l.name ?? `${i + 1}`;
159
159
  if (l.locked) {
160
- iconOr("Lock", "lock", 28, token("fgSubtle"), (desc.lockedLabel ?? "{name}, locked").replaceAll("{name}", name));
160
+ iconOr("Lock", "lock", 28, token("inkFaint"), (desc.lockedLabel ?? "{name}, locked").replaceAll("{name}", name));
161
161
  return;
162
162
  }
163
163
  if ([...name].length > 3) kit.label("Name", name, { step: phone ? "base" : "lg", font: "display", align: "center", width: grow(), maxLines: 2, ellipsis: true });
164
164
  else kit.label("Name", name, { step: "xl3", font: "display", nowrap: true });
165
165
  const stars = Math.min(3, l.stars ?? 0);
166
166
  kit.row("Stars", { gap: 2 }, () => {
167
- for (let k = 0; k < 3; k++) iconOr(kit.idi("Star", k), "star", 14, token(k < stars ? "accent" : "fillStrong"), k === 0 ? starsLabel(stars, 3) : "");
167
+ for (let k = 0; k < 3; k++) iconOr(kit.idi("Star", k), "star", 14, token(k < stars ? "primary" : "controlStrong"), k === 0 ? starsLabel(stars, 3) : "");
168
168
  });
169
169
  });
170
170
  if (g.clicked !== null && g.clicked !== undefined && !levels[g.clicked]?.locked) out.chosen = g.clicked;
@@ -176,7 +176,7 @@ export function levelSelect(id: string, desc: Api.LevelSelectDesc = {}): Api.Lev
176
176
  return out;
177
177
  }
178
178
 
179
- const MEDALS: readonly Api.ThemeColor[] = ["warn", "fgMuted", "accentInk"];
179
+ const MEDALS: readonly Api.ThemeColor[] = ["warn", "inkMuted", "highlight"];
180
180
 
181
181
  export function leaderboardScreen(id: string, desc: Api.LeaderboardDesc = {}): Api.LeaderboardResult {
182
182
  const { t, phone } = frame();
@@ -203,14 +203,14 @@ export function leaderboardScreen(id: string, desc: Api.LeaderboardDesc = {}): A
203
203
  });
204
204
  } else if (entries.length === 0) {
205
205
  kit.column("Empty", { width: grow(), height, align: "center", cross: "center" }, () => {
206
- kit.label("Message", desc.empty ?? "No scores yet", { color: token("fgMuted"), align: "center", width: grow() });
206
+ kit.label("Message", desc.empty ?? "No scores yet", { color: token("inkMuted"), align: "center", width: grow() });
207
207
  });
208
208
  } else {
209
209
  const player = entries.findIndex((e) => e.player);
210
210
  kit.list("Rows", { count: entries.length, rowHeight: t.row, height, width: grow(), selected: player >= 0 ? player : null }, (i) => {
211
211
  const e = entries[i]!;
212
212
  const rank = e.rank ?? i + 1;
213
- kit.label("Rank", `${rank}`, { width: px(40), font: "mono", step: rank <= 3 ? "lg" : "body", color: token(rank <= 3 ? MEDALS[rank - 1]! : "fgMuted"), nowrap: true });
213
+ kit.label("Rank", `${rank}`, { width: px(40), font: "mono", step: rank <= 3 ? "lg" : "body", color: token(rank <= 3 ? MEDALS[rank - 1]! : "inkMuted"), nowrap: true });
214
214
  kit.label("Name", e.name ?? "", { width: grow(), nowrap: true });
215
215
  kit.label("Score", e.score ?? "", { font: "mono", nowrap: true });
216
216
  });
@@ -15,8 +15,10 @@ export function cutscene(id: string, desc: Api.CutsceneDesc = {}): Api.CutsceneR
15
15
  kit.setNavigation(true);
16
16
  const letterbox = desc.letterbox ?? true;
17
17
  if (letterbox) {
18
+ // Cinematic bars are black in every style: not a token, which a light
19
+ // style (kestrel's paper) would turn into cream panels.
18
20
  for (const [name, anchor] of [["Top", "top"], ["Bottom", "bottom"]] as const) {
19
- kit.anchor(`${id}.${name}`, { anchor, margin: 0, width: grow(), height: pct(0.1), background: token("stage") }, () => {});
21
+ kit.anchor(`${id}.${name}`, { anchor, margin: 0, width: grow(), height: pct(0.1), background: { color: { r: 0, g: 0, b: 0 } } }, () => {});
20
22
  }
21
23
  }
22
24
  const line = lines[index]!;
@@ -68,13 +70,13 @@ export function tutorialOverlay(id: string, desc: Api.TutorialDesc = {}): Api.Tu
68
70
  direction: "column",
69
71
  gap: t.sm,
70
72
  padding: all(t.lg),
71
- background: token("surfaceRaised"),
73
+ background: token("panelRaised"),
72
74
  radius: t.panel,
73
75
  }, () => {
74
76
  const count = (desc.stepLabel ?? "{step} of {count}").replaceAll("{step}", `${index + 1}`).replaceAll("{count}", `${steps.length}`);
75
- kit.label("Count", count, { step: "meta", color: token("fgMuted") });
77
+ kit.label("Count", count, { step: "meta", color: token("inkMuted") });
76
78
  if (step.title) kit.label("Title", step.title, { step: "lg", width: grow() });
77
- if (step.text) kit.label("Text", step.text, { step: "reading", color: token("fgMuted"), width: grow() });
79
+ if (step.text) kit.label("Text", step.text, { step: "reading", color: token("inkMuted"), width: grow() });
78
80
  kit.row("Actions", { width: grow(), gap: t.sm, align: "spaceBetween", cross: "center" }, () => {
79
81
  if ((kit.button("Skip", desc.skipLabel ?? "Skip", { variant: "quiet", size: "small" }) as Api.Response).clicked) out.skipped = true;
80
82
  const lastStep = index + 1 >= steps.length;
@@ -1,5 +1,5 @@
1
1
  // Small helpers the screen templates share: sizes, insets, the frame's
2
- // tokens, number and time formats, and HUD capsules. Nothing here keeps
2
+ // tokens, number and time formats, and HUD boxes. Nothing here keeps
3
3
  // state: what must persist lives in the UI's memory (og.ui.memory).
4
4
 
5
5
  import type * as Api from "@origingame/engine";
@@ -25,7 +25,7 @@ export function frame(): { theme: Api.UiTheme; viewport: Api.Viewport; t: Tokens
25
25
 
26
26
  export interface Tokens {
27
27
  xxs: number; xs: number; sm: number; md: number; lg: number; xl: number; xxl: number; xl3: number;
28
- card: number; panel: number; block: number; button: number;
28
+ small: number; control: number; bar: number; panel: number;
29
29
  controlSm: number; controlMd: number; controlLg: number; gutter: number; row: number;
30
30
  }
31
31
 
@@ -34,7 +34,7 @@ function tokens(t: Api.UiTheme): Tokens {
34
34
  const r = (k: string): number => t.radius[k] ?? 0;
35
35
  return {
36
36
  xxs: s("xxs"), xs: s("xs"), sm: s("sm"), md: s("md"), lg: s("lg"), xl: s("xl"), xxl: s("xxl"), xl3: s("xl3"),
37
- card: r("card"), panel: r("panel"), block: r("block"), button: r("button"),
37
+ small: r("small"), control: r("control"), bar: r("bar"), panel: r("panel"),
38
38
  controlSm: t.sizes.controlSm, controlMd: t.sizes.controlMd, controlLg: t.sizes.controlLg, gutter: t.sizes.gutter, row: t.sizes.row,
39
39
  };
40
40
  }
@@ -73,17 +73,17 @@ export function lapTime(seconds: number): string {
73
73
  return `${m}:${s.toString().padStart(2, "0")}.${(ms % 1000).toString().padStart(3, "0")}`;
74
74
  }
75
75
 
76
- /** A HUD capsule: a translucent dark pill (material-thick) that keeps text legible over any art. */
77
- export function capsule(id: string, build: () => void, desc: { gap?: number; padX?: number; height?: number } = {}): Api.Response {
76
+ /** A HUD box: the style's `hud` fill and frame, cornered by its control radius, that keeps text legible over any art. */
77
+ export function hudBox(id: string, build: () => void, desc: { gap?: number; padX?: number; height?: number } = {}): Api.Response {
78
78
  const { t } = frame();
79
79
  return kit.row(id, {
80
80
  padding: xy(desc.padX ?? t.md, 0),
81
81
  height: px(desc.height ?? t.controlMd),
82
82
  gap: desc.gap ?? t.sm,
83
83
  cross: "center",
84
- background: token("materialThick"),
85
- radius: t.button,
86
- frame: "capsule",
84
+ background: token("hud"),
85
+ radius: t.control,
86
+ frame: "hud",
87
87
  }, build) as Api.Response;
88
88
  }
89
89