@origingame/engine 0.1.1 → 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 (109) 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 +4 -4
  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 +21 -11
  36. package/knowledge/skills/og-build/SKILL.md +25 -6
  37. package/knowledge/skills/og-core/SKILL.md +11 -0
  38. package/knowledge/skills/og-feel/SKILL.md +8 -1
  39. package/knowledge/skills/og-game/SKILL.md +994 -21
  40. package/knowledge/skills/og-i18n/SKILL.md +12 -3
  41. package/knowledge/skills/og-platform-sdk/SKILL.md +4 -1
  42. package/knowledge/skills/og-taste/SKILL.md +82 -73
  43. package/knowledge/skills/og-ui/SKILL.md +82 -54
  44. package/knowledge/wiki/ai.md +20 -20
  45. package/knowledge/wiki/anim.md +384 -41
  46. package/knowledge/wiki/atmosphere.md +36 -13
  47. package/knowledge/wiki/audio.md +18 -18
  48. package/knowledge/wiki/camera.md +19 -13
  49. package/knowledge/wiki/commands.md +19 -13
  50. package/knowledge/wiki/debug.md +414 -0
  51. package/knowledge/wiki/game.md +8 -8
  52. package/knowledge/wiki/i18n.md +7 -7
  53. package/knowledge/wiki/index.md +265 -7
  54. package/knowledge/wiki/input.md +47 -47
  55. package/knowledge/wiki/load.md +236 -68
  56. package/knowledge/wiki/math.md +2614 -7
  57. package/knowledge/wiki/nav.md +34 -34
  58. package/knowledge/wiki/net.md +340 -0
  59. package/knowledge/wiki/path.md +402 -0
  60. package/knowledge/wiki/physics.md +124 -106
  61. package/knowledge/wiki/platform.md +465 -31
  62. package/knowledge/wiki/probe.md +681 -67
  63. package/knowledge/wiki/reflect.md +15 -15
  64. package/knowledge/wiki/render.md +3904 -1897
  65. package/knowledge/wiki/rng.md +14 -14
  66. package/knowledge/wiki/stream.md +15 -15
  67. package/knowledge/wiki/system.md +8 -8
  68. package/knowledge/wiki/systems.md +2 -2
  69. package/knowledge/wiki/terrain.md +19 -19
  70. package/knowledge/wiki/tilemap.md +19 -15
  71. package/knowledge/wiki/time.md +4 -4
  72. package/knowledge/wiki/timeline.md +396 -0
  73. package/knowledge/wiki/transform.md +8 -8
  74. package/knowledge/wiki/tween.md +17 -17
  75. package/knowledge/wiki/ui.md +603 -629
  76. package/knowledge/wiki/video.md +157 -0
  77. package/knowledge/wiki/world.md +67 -37
  78. package/package.json +7 -6
  79. package/schema/api.schema.json +21071 -13964
  80. package/shell/page.ts +26 -2
  81. package/shell/platform.ts +46 -0
  82. package/shell/player.ts +7 -0
  83. package/src/depth.ts +71 -0
  84. package/src/e6.ts +32 -1
  85. package/src/env.d.ts +5 -1
  86. package/src/generated/abi.ts +189 -3
  87. package/src/index.ts +26 -4
  88. package/src/light.ts +18 -0
  89. package/src/math.ts +622 -0
  90. package/src/node/index.ts +17 -0
  91. package/src/node/playtest-run.ts +114 -18
  92. package/src/node/polish.ts +15 -4
  93. package/src/node/render.ts +291 -0
  94. package/src/node/run.ts +91 -6
  95. package/src/p2.ts +19 -1
  96. package/src/playtest.ts +57 -4
  97. package/src/runtime.ts +15 -3
  98. package/src/targets.ts +28 -0
  99. package/src/trace.ts +169 -0
  100. package/src/ui-kit/huds.ts +49 -45
  101. package/src/ui-kit/menus.ts +12 -12
  102. package/src/ui-kit/settings.ts +2 -2
  103. package/src/ui-kit/store.ts +18 -18
  104. package/src/ui-kit/story.ts +6 -4
  105. package/src/ui-kit/util.ts +8 -8
  106. package/src/web/shell.ts +58 -0
  107. package/src/web/worker.ts +103 -2
  108. package/types/bindings.json +8945 -6456
  109. package/types/index.d.ts +15507 -11437
@@ -2,11 +2,13 @@
2
2
  // set of anchored groups over the game in the safe area, plus the touch
3
3
  // controls they show on phones and tablets. HUDs turn keyboard and gamepad
4
4
  // navigation off (the keys move the player); Back still answers `paused`,
5
- // except under an open dialog, which keeps it (og.ui.back).
5
+ // except under an open dialog, which keeps it (og.ui.back). Every word a HUD
6
+ // draws or names is a field of its desc with the English default (og-i18n
7
+ // §2), so a game in another language has none of the kit's English left.
6
8
 
7
9
  import type * as Api from "@origingame/engine";
8
10
  import { kit } from "./bind.ts";
9
- 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";
10
12
 
11
13
  type Hud = { paused: boolean; action: string | null; moveX: number; moveY: number; held: string[] };
12
14
 
@@ -15,7 +17,8 @@ function hud(): Hud {
15
17
  return { paused: kit.back(), action: null, moveX: 0, moveY: 0, held: [] };
16
18
  }
17
19
 
18
- function pauseButton(out: Hud, label = "Pause"): void {
20
+ /** The pause button: its icon, named by the desc's `pauseLabel` (what a screen reader says). */
21
+ function pauseButton(out: Hud, label: string): void {
19
22
  const r = kit.actionButton("Pause", label, { size: 44, icon: "pause" });
20
23
  if (r.clicked) out.paused = true;
21
24
  }
@@ -24,7 +27,7 @@ function pauseButton(out: Hud, label = "Pause"): void {
24
27
  function stat(id: string, value: string, caption: string, step: Api.TypeStepName = "xl3", align: Api.TextAlign = "start"): void {
25
28
  kit.column(id, { gap: 0, cross: align === "end" ? "end" : align === "center" ? "center" : "start" }, () => {
26
29
  kit.label("Value", value, { step, font: "mono", nowrap: true, align });
27
- 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 });
28
31
  });
29
32
  }
30
33
 
@@ -55,45 +58,45 @@ export function platformerHud(id: string, desc: Api.PlatformerHudDesc = {}): Api
55
58
  const { t, touch } = frame();
56
59
  const out = hud();
57
60
  // What the game leaves out is not drawn: no hearts with maxLives 0 (and
58
- // 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.
59
62
  const health = desc.health ?? null;
60
63
  const maxLives = desc.maxLives ?? 3;
61
64
  const coins = desc.coins ?? null;
62
65
  const score = desc.score ?? null;
63
66
  if (health !== null || maxLives > 0 || desc.level) {
64
67
  kit.anchor(`${id}.left`, { anchor: "topLeft", direction: "column", gap: t.sm }, () => {
65
- 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) }));
66
69
  else if (maxLives > 0) {
67
- capsule("Lives", () => {
68
- 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"));
69
72
  });
70
73
  }
71
- 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 });
72
75
  });
73
76
  }
74
77
  if (desc.timeLeft !== null && desc.timeLeft !== undefined) {
75
78
  const low = desc.timeLeft <= 10;
76
79
  kit.anchor(`${id}.top`, { anchor: "top" }, () => {
77
- 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 }));
78
81
  });
79
82
  }
80
83
  kit.anchor(`${id}.right`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
81
84
  if (coins !== null) {
82
- capsule("Coins", () => {
83
- iconOr("Coin", "coin", 20, token("accent"));
85
+ hudBox("Coins", () => {
86
+ iconOr("Coin", "coin", 20, token("gold"));
84
87
  kit.label("Count", fmt(coins), { font: "mono", nowrap: true });
85
88
  });
86
89
  }
87
- if (score !== null) capsule("Score", () => kit.label("Points", fmt(score), { font: "mono", nowrap: true }));
88
- pauseButton(out);
90
+ if (score !== null) hudBox("Score", () => kit.label("Points", fmt(score), { font: "mono", nowrap: true }));
91
+ pauseButton(out, desc.pauseLabel ?? "Pause");
89
92
  });
90
- if ((desc.touch ?? false) && touch) touchControls(out, { dpad: true, buttons: [{ id: "jump", label: "Jump", primary: true }] });
93
+ if ((desc.touch ?? false) && touch) touchControls(out, { dpad: true, buttons: [{ id: "jump", label: desc.jumpLabel ?? "Jump", primary: true }] });
91
94
  return out;
92
95
  }
93
96
 
94
97
  /** A crosshair at the centre: four short strokes and a gap. */
95
98
  function crosshair(): void {
96
- const ink = token("fg", 0.9);
99
+ const ink = token("ink", 0.9);
97
100
  const bar = (id: string, w: number, h: number): void => kit.image(id, { tint: ink, width: px(w), height: px(h) });
98
101
  kit.anchor("Crosshair", { anchor: "center", direction: "column", cross: "center", gap: 6 }, () => {
99
102
  bar("N", 2, 8);
@@ -118,20 +121,20 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
118
121
  });
119
122
  if (desc.map && !showTouch) kit.anchor(`${id}.map`, { anchor: "topLeft" }, () => kit.minimap("Map", desc.map!));
120
123
  kit.anchor(`${id}.top`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
121
- capsule("Kills", () => {
124
+ hudBox("Kills", () => {
122
125
  kit.label("Count", fmt(desc.kills ?? 0), { font: "mono", nowrap: true });
123
- kit.label("Unit", "kills", { step: "meta", color: token("fgMuted"), nowrap: true });
126
+ kit.label("Unit", desc.killsLabel ?? "kills", { step: "meta", color: token("inkMuted"), nowrap: true });
124
127
  });
125
- pauseButton(out);
128
+ pauseButton(out, desc.pauseLabel ?? "Pause");
126
129
  });
127
130
  kit.anchor(`${id}.ammo`, { anchor: showTouch ? "topRight" : "bottomRight", offsetY: showTouch ? 56 : 0, direction: "column", cross: "end", gap: t.xs }, () => {
128
131
  const mag = desc.magazine ?? 0;
129
132
  const ammo = desc.ammo ?? 0;
130
133
  kit.row("Rounds", { gap: t.xs, cross: "end" }, () => {
131
- kit.label("Ammo", `${ammo}`, { step: "xl5", font: "mono", color: token(mag > 0 && ammo <= Math.floor(mag / 4) ? "warn" : "fg"), nowrap: true });
132
- 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 });
133
136
  });
134
- 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 });
135
138
  });
136
139
  const abilities = desc.abilities ?? [];
137
140
  if (abilities.length > 0) {
@@ -144,7 +147,7 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
144
147
  });
145
148
  }
146
149
  if (desc.crosshair ?? true) crosshair();
147
- if (showTouch) touchControls(out, { buttons: [{ id: "fire", label: "Fire", primary: true }] });
150
+ if (showTouch) touchControls(out, { buttons: [{ id: "fire", label: desc.fireLabel ?? "Fire", primary: true }] });
148
151
  return out;
149
152
  }
150
153
 
@@ -155,22 +158,22 @@ export function puzzleHud(id: string, desc: Api.PuzzleHudDesc = {}): Api.HudResu
155
158
  const score = desc.score ?? 0;
156
159
  const marks = desc.stars && desc.stars.length > 0 ? desc.stars : [Math.round(target / 3), Math.round((2 * target) / 3), target];
157
160
  kit.anchor(`${id}.left`, { anchor: "topLeft", direction: "column", gap: t.xs }, () => {
158
- if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("fgMuted"), nowrap: true });
159
- if (desc.moves !== null && desc.moves !== undefined) stat("Moves", `${desc.moves}`, "moves left", "xl3");
161
+ if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("inkMuted"), nowrap: true });
162
+ if (desc.moves !== null && desc.moves !== undefined) stat("Moves", `${desc.moves}`, desc.movesLabel ?? "moves left", "xl3");
160
163
  });
161
164
  kit.anchor(`${id}.score`, { anchor: "top", direction: "column", cross: "center", gap: t.xs, width: px(260) }, () => {
162
165
  kit.label("Score", fmt(score), { step: "displaySm", font: "display", align: "center", width: grow() });
163
- kit.meter("Progress", score, target, { kind: "accent", width: grow(), segments: 3 });
166
+ kit.meter("Progress", score, target, { kind: "primary", width: grow(), segments: 3 });
164
167
  kit.row("Stars", { gap: t.sm, cross: "center" }, () => {
165
- marks.slice(0, 3).forEach((m, i) => iconOr(kit.idi("Star", i), "star", 20, token(score >= m ? "accent" : "fillStrong")));
166
- kit.label("Target", `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 });
167
170
  });
168
171
  });
169
172
  kit.anchor(`${id}.right`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
170
173
  if (desc.timeLeft !== null && desc.timeLeft !== undefined) {
171
- 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 }));
172
175
  }
173
- pauseButton(out);
176
+ pauseButton(out, desc.pauseLabel ?? "Pause");
174
177
  });
175
178
  return out;
176
179
  }
@@ -182,13 +185,13 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
182
185
  const generators = desc.generators ?? [];
183
186
  kit.anchor(`${id}.wallet`, { anchor: "top", gap: t.sm, cross: "center" }, () => {
184
187
  currencies.forEach((c, i) => {
185
- capsule(c.id ?? kit.idi("Currency", i), () => {
186
- 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"));
187
190
  kit.label("Amount", fmt(c.amount ?? 0), { step: i === 0 ? "lg" : "body", font: "mono", nowrap: true });
188
- if ((c.perSecond ?? 0) !== 0) kit.label("Rate", `+${fmt(c.perSecond ?? 0)}/s`, { step: "meta", color: token("success"), nowrap: true });
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 });
189
192
  }, { height: i === 0 ? t.controlLg : t.controlMd });
190
193
  });
191
- pauseButton(out);
194
+ pauseButton(out, desc.pauseLabel ?? "Pause");
192
195
  });
193
196
  if (desc.tapLabel !== "") {
194
197
  kit.anchor(`${id}.tap`, { anchor: phone ? "top" : "center", offsetY: phone ? 96 : 0, offsetX: phone ? 0 : -150 }, () => {
@@ -201,13 +204,13 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
201
204
  kit.panel("Generators", { width: grow(), padding: all(t.sm), gap: t.xs }, () => {
202
205
  kit.list("List", { count: generators.length, rowHeight: 64, gap: t.xs, height: fit(0, listHeight), width: grow() }, (i) => {
203
206
  const g = generators[i]!;
204
- if (g.icon) iconOr("Icon", g.icon, 32, token("fg"));
207
+ if (g.icon) iconOr("Icon", g.icon, 32, token("ink"));
205
208
  kit.column("Text", { width: grow(), gap: t.xxs }, () => {
206
209
  kit.row("Head", { gap: t.sm, cross: "center", width: grow() }, () => {
207
210
  kit.label("Name", g.name ?? g.id ?? "", { step: "lg", nowrap: true, width: grow() });
208
- kit.badge("Level", `Lv ${g.level ?? 0}`, "neutral");
211
+ kit.badge("Level", (desc.levelLabel ?? "Lv {level}").replaceAll("{level}", `${g.level ?? 0}`), "neutral");
209
212
  });
210
- 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() });
211
214
  });
212
215
  const buy = kit.button("Buy", fmt(g.cost ?? 0), { size: "small", variant: g.affordable ? "primary" : "secondary", disabled: !g.affordable }) as Api.Response;
213
216
  if (buy.clicked) out.action = `buy:${g.id ?? i}`;
@@ -226,7 +229,7 @@ function deltaText(seconds: number): string {
226
229
 
227
230
  /** A delta's ink: ahead (negative) success, behind danger, even muted. */
228
231
  function deltaInk(seconds: number): Api.Paint {
229
- return token(seconds < 0 ? "success" : seconds > 0 ? "danger" : "fgMuted");
232
+ return token(seconds < 0 ? "success" : seconds > 0 ? "danger" : "inkMuted");
230
233
  }
231
234
 
232
235
  export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResult {
@@ -241,7 +244,7 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
241
244
  if (racers > 0) {
242
245
  kit.row("Position", { gap: t.xxs, cross: "end" }, () => {
243
246
  kit.label("Place", `${desc.position ?? 1}`, { step: "displayXl", font: "display", nowrap: true });
244
- 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 });
245
248
  });
246
249
  }
247
250
  stat("Lap", `${Math.min(desc.lap ?? 1, desc.laps ?? 3)}/${desc.laps ?? 3}`, desc.lapLabel ?? "lap", "xl3");
@@ -254,16 +257,17 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
254
257
  const delta = desc.delta ?? null;
255
258
  if (delta !== null) kit.label("Delta", deltaText(delta), { step: "lg", font: "mono", color: deltaInk(delta), nowrap: true });
256
259
  if (desc.best !== null && desc.best !== undefined) {
257
- kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", clock(desc.best, true)), { step: "meta", font: "mono", color: token("fgMuted"), nowrap: true });
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("inkMuted"), nowrap: true });
258
262
  }
259
263
  const splits = desc.splits ?? [];
260
264
  if (splits.length > 0) {
261
265
  kit.column("Splits", { gap: 0, cross: "end" }, () => {
262
266
  splits.forEach((sp, i) => {
263
267
  kit.row(kit.idi("Split", i), { gap: t.sm, cross: "center" }, () => {
264
- 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 });
265
269
  const time = sp.time ?? null;
266
- 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 });
267
271
  const d = sp.delta ?? null;
268
272
  if (d !== null) kit.label("Delta", deltaText(d), { step: "meta", font: "mono", color: deltaInk(d), nowrap: true });
269
273
  });
@@ -276,11 +280,11 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
276
280
  kit.row("Speed", { gap: t.sm, cross: "end", width: grow(), align: "end" }, () => {
277
281
  kit.label("Value", `${Math.round(desc.speed ?? 0)}`, { step: "displayXl2", font: "display", nowrap: true });
278
282
  kit.column("Side", { cross: "end" }, () => {
279
- kit.label("Gear", desc.gear ?? "N", { step: "xl", font: "mono", color: token("accentInk"), nowrap: true });
280
- 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 });
281
285
  });
282
286
  });
283
- 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 });
284
288
  if (boost !== null) kit.meter("Boost", boost, 1, { color: token("info"), label: boostLabel, width: grow(), segments: 4 });
285
289
  });
286
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