@origingame/engine 0.2.0 → 0.4.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 (118) hide show
  1. package/bin/og.mjs +1 -1
  2. package/dist/web/origin-engine.d.ts +255 -5
  3. package/dist/web/origin-engine.js +1584 -125
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
  6. package/dist/web-publish/origin-engine-lanes.js +1288 -82
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +199 -6
  9. package/dist/web-publish/origin-engine.js +1285 -79
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
  12. package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
  13. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  14. package/dist/web-publish-2d/origin-engine.d.ts +198 -5
  15. package/dist/web-publish-2d/origin-engine.js +1282 -76
  16. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
  18. package/knowledge/README.md +2 -1
  19. package/knowledge/guides/three-to-og.md +393 -0
  20. package/knowledge/knowledge.json +2 -1
  21. package/knowledge/looks/catalogue.json +40 -40
  22. package/knowledge/package.json +3 -2
  23. package/knowledge/scaffold/distribution.json +14 -0
  24. package/knowledge/scaffold/origin-engine.json +2 -2
  25. package/knowledge/scaffold/template/README.md +2 -1
  26. package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
  27. package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
  28. package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
  29. package/knowledge/scaffold/template/host.d.ts +46 -0
  30. package/knowledge/scaffold/template/package.json +1 -1
  31. package/knowledge/scaffold/template/project.ogt +1 -1
  32. package/knowledge/scaffold/template/src/main.ts +1 -1
  33. package/knowledge/scaffold/template/tests/frames.ts +134 -0
  34. package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
  35. package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
  36. package/knowledge/scaffold/template/tsconfig.json +1 -1
  37. package/knowledge/scripts/mount.ts +2 -1
  38. package/knowledge/scripts/validate.ts +159 -31
  39. package/knowledge/skills/og-audio/SKILL.md +2 -2
  40. package/knowledge/skills/og-build/SKILL.md +29 -7
  41. package/knowledge/skills/og-core/SKILL.md +31 -5
  42. package/knowledge/skills/og-feel/SKILL.md +2 -2
  43. package/knowledge/skills/og-game/SKILL.md +1331 -16
  44. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  45. package/knowledge/skills/og-model/SKILL.md +374 -0
  46. package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
  47. package/knowledge/skills/og-taste/SKILL.md +82 -73
  48. package/knowledge/skills/og-ui/SKILL.md +80 -58
  49. package/knowledge/wiki/ai.md +20 -20
  50. package/knowledge/wiki/anim.md +392 -41
  51. package/knowledge/wiki/atmosphere.md +36 -13
  52. package/knowledge/wiki/audio.md +18 -18
  53. package/knowledge/wiki/camera.md +19 -13
  54. package/knowledge/wiki/commands.md +19 -13
  55. package/knowledge/wiki/debug.md +414 -0
  56. package/knowledge/wiki/game.md +8 -8
  57. package/knowledge/wiki/i18n.md +7 -7
  58. package/knowledge/wiki/index.md +317 -8
  59. package/knowledge/wiki/input.md +47 -47
  60. package/knowledge/wiki/load.md +238 -70
  61. package/knowledge/wiki/math.md +2607 -0
  62. package/knowledge/wiki/nav.md +34 -34
  63. package/knowledge/wiki/net.md +340 -0
  64. package/knowledge/wiki/path.md +402 -0
  65. package/knowledge/wiki/physics.md +95 -95
  66. package/knowledge/wiki/platform.md +463 -29
  67. package/knowledge/wiki/probe.md +681 -67
  68. package/knowledge/wiki/reflect.md +29 -17
  69. package/knowledge/wiki/render.md +4993 -1998
  70. package/knowledge/wiki/rng.md +14 -14
  71. package/knowledge/wiki/shapes.md +727 -0
  72. package/knowledge/wiki/stream.md +15 -15
  73. package/knowledge/wiki/system.md +8 -8
  74. package/knowledge/wiki/systems.md +2 -2
  75. package/knowledge/wiki/terrain.md +24 -20
  76. package/knowledge/wiki/tilemap.md +13 -13
  77. package/knowledge/wiki/time.md +4 -4
  78. package/knowledge/wiki/timeline.md +396 -0
  79. package/knowledge/wiki/transform.md +8 -8
  80. package/knowledge/wiki/tween.md +17 -17
  81. package/knowledge/wiki/ui.md +560 -654
  82. package/knowledge/wiki/video.md +157 -0
  83. package/knowledge/wiki/world.md +36 -36
  84. package/package.json +4 -3
  85. package/schema/api.schema.json +28151 -19640
  86. package/shell/page.ts +26 -2
  87. package/shell/platform.ts +51 -0
  88. package/shell/player.ts +15 -2
  89. package/shell/transport.ts +6 -0
  90. package/src/depth.ts +71 -0
  91. package/src/e6.ts +32 -1
  92. package/src/env.d.ts +5 -1
  93. package/src/generated/abi.ts +226 -4
  94. package/src/geom.ts +260 -0
  95. package/src/index.ts +32 -4
  96. package/src/light.ts +18 -0
  97. package/src/math.ts +622 -0
  98. package/src/node/index.ts +17 -0
  99. package/src/node/playtest-run.ts +115 -18
  100. package/src/node/polish.ts +13 -3
  101. package/src/node/render.ts +291 -0
  102. package/src/node/run.ts +86 -5
  103. package/src/p2.ts +19 -1
  104. package/src/platform-local.ts +2 -0
  105. package/src/playtest.ts +65 -4
  106. package/src/runtime.ts +100 -9
  107. package/src/targets.ts +28 -0
  108. package/src/trace.ts +169 -0
  109. package/src/ui-kit/huds.ts +34 -34
  110. package/src/ui-kit/menus.ts +12 -12
  111. package/src/ui-kit/settings.ts +11 -2
  112. package/src/ui-kit/store.ts +18 -18
  113. package/src/ui-kit/story.ts +6 -4
  114. package/src/ui-kit/util.ts +8 -8
  115. package/src/web/shell.ts +59 -1
  116. package/src/web/worker.ts +105 -4
  117. package/types/bindings.json +9368 -6619
  118. package/types/index.d.ts +16377 -11520
package/src/trace.ts ADDED
@@ -0,0 +1,169 @@
1
+ // The run's trace in the Chrome trace format (the Trace Event Format's JSON
2
+ // object form, which Perfetto and chrome://tracing open), the same document
3
+ // on both hosts: native `og run --trace` (src/node/run.ts) and the web
4
+ // Worker (src/web/worker.ts, read through the shell's `trace()`).
5
+ //
6
+ // The engine records every span where it ran and hands them over as Chrome
7
+ // trace events on its own clock (crates/og-runtime/src/trace.rs: `{ now,
8
+ // events }`, `ts` in µs); a take is placed on the document's origin (Unix
9
+ // milliseconds) by the difference between the host's time and the engine's
10
+ // clock read together. Threads of process 1: 1 engine, 2 render, 3 load,
11
+ // 4 cook (og's, natively), 5 gpu, 16 + n og-lanes' pool thread n, 48 + n
12
+ // the cook's thread n; threads that appear mid-run arrive with their names.
13
+
14
+ export type TraceEvent = Record<string, unknown> & { ph: string; pid: number; tid: number; name: string; ts?: number; dur?: number };
15
+
16
+ /** A Chrome trace document as written to a file. */
17
+ export type TraceDocument = {
18
+ traceEvents: TraceEvent[];
19
+ displayTimeUnit: "ms";
20
+ otherData: { originUnixMs: number; producer: string; [k: string]: unknown };
21
+ };
22
+
23
+ /** The fixed threads and their names. */
24
+ export const TRACE_THREADS: readonly (readonly [number, string])[] = [
25
+ [1, "engine"],
26
+ [2, "render"],
27
+ [3, "load"],
28
+ [4, "cook"],
29
+ [5, "gpu"],
30
+ ];
31
+
32
+ /** The process and fixed threads' metadata events. */
33
+ export function traceMetadata(process: string): TraceEvent[] {
34
+ const out: TraceEvent[] = [{ name: "process_name", ph: "M", pid: 1, tid: 0, args: { name: process } }];
35
+ for (const [tid, name] of TRACE_THREADS) {
36
+ out.push({ name: "thread_name", ph: "M", pid: 1, tid, args: { name } });
37
+ out.push({ name: "thread_sort_index", ph: "M", pid: 1, tid, args: { sort_index: tid } });
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** Unix milliseconds, to the microsecond where the clock has it. */
43
+ export function unixMs(): number {
44
+ return performance.timeOrigin + performance.now();
45
+ }
46
+
47
+ /** A trace being recorded: takes appended as they come, the document at the end. */
48
+ export class Trace {
49
+ readonly events: TraceEvent[];
50
+ readonly origin: number;
51
+ readonly producer: string;
52
+ /** Thread names already in the document (a take names a thread once per engine trace; a document may hold several). */
53
+ private named = new Set<number>();
54
+ /** µs from the engine's clock to the document's origin, set by the first take. */
55
+ private shift: number | undefined;
56
+
57
+ /** A new document from `origin` (Unix ms), or one continued from `prior` (og's prelude: the cook). */
58
+ constructor(opts: { origin?: number; prior?: Partial<TraceDocument>; producer: string; process?: string }) {
59
+ this.producer = opts.producer;
60
+ if (opts.prior?.traceEvents) {
61
+ this.events = [...opts.prior.traceEvents];
62
+ this.origin = opts.prior.otherData?.originUnixMs ?? opts.origin ?? unixMs();
63
+ } else {
64
+ this.events = traceMetadata(opts.process ?? opts.producer);
65
+ this.origin = opts.origin ?? unixMs();
66
+ }
67
+ for (const e of this.events) if (e.ph === "M" && e.name === "thread_name") this.named.add(e.tid);
68
+ }
69
+
70
+ /**
71
+ * Places one take of the engine (`og_*_trace_take`'s JSON) on the
72
+ * document's origin: `hostNow` is the host's Unix ms read beside the take.
73
+ */
74
+ append(taken: string | { now: number; events: TraceEvent[] }, hostNow: number = unixMs()): void {
75
+ const t = typeof taken === "string" ? (JSON.parse(taken) as { now: number; events: TraceEvent[] }) : taken;
76
+ // One shift for the whole trace, from the first take: both clocks are
77
+ // monotonic at the same rate, and a shift read again at every take
78
+ // would move spans that abut across two takes by its jitter (tens of
79
+ // µs), so that they overlap.
80
+ this.shift ??= Math.round((hostNow - t.now - this.origin) * 1000);
81
+ const shift = this.shift;
82
+ for (const e of t.events) {
83
+ if (e.ph === "M") {
84
+ if (e.name === "thread_name") {
85
+ if (this.named.has(e.tid)) continue;
86
+ this.named.add(e.tid);
87
+ }
88
+ this.events.push(e);
89
+ continue;
90
+ }
91
+ this.events.push({ ...e, ts: (e.ts ?? 0) + shift });
92
+ }
93
+ }
94
+
95
+ document(extra: Record<string, unknown> = {}): TraceDocument {
96
+ return { traceEvents: this.events, displayTimeUnit: "ms", otherData: { originUnixMs: this.origin, producer: this.producer, ...extra } };
97
+ }
98
+ }
99
+
100
+ /** What {@link checkTrace} found. */
101
+ export type TraceCheck = { errors: string[]; spans: number; threads: Record<number, string> };
102
+
103
+ /**
104
+ * Holds a document to the format as Perfetto's importer reads it, and to
105
+ * what this engine's traces promise: `traceEvents` a list; every `X` event
106
+ * a name, integer `ts`, `dur` ≥ 0 and integer `pid`/`tid`; metadata with
107
+ * `args.name`; every span on a thread nested inside the one it starts in
108
+ * (never overlapping it); and spans that lie where they ran. A thread whose
109
+ * children are laid end to end from their parent's start in every parent
110
+ * (the first child at the parent's `ts`, each next one where the previous
111
+ * ended), over at least `endToEndAfter` parents with two or more children,
112
+ * is reported: recorded starts leave gaps somewhere. The GPU's passes
113
+ * (`cat` "gpu") are exempt: one timestamp ends a pass and begins the next,
114
+ * so they are back to back by the measurement's construction.
115
+ */
116
+ export function checkTrace(doc: unknown, endToEndAfter = 8): TraceCheck {
117
+ const errors: string[] = [];
118
+ const threads: Record<number, string> = {};
119
+ const d = doc as Partial<TraceDocument> | null;
120
+ if (!d || !Array.isArray(d.traceEvents)) return { errors: ["traceEvents is not a list"], spans: 0, threads };
121
+ const byTid = new Map<number, { ts: number; dur: number; name: string }[]>();
122
+ const backToBack = new Set<number>();
123
+ for (const e of d.traceEvents) {
124
+ if (!Number.isInteger(e.pid) || !Number.isInteger(e.tid)) errors.push(`${e.name}: pid/tid not integers`);
125
+ if (e.ph === "M") {
126
+ const name = (e.args as { name?: unknown } | undefined)?.name;
127
+ if ((e.name === "thread_name" || e.name === "process_name") && typeof name !== "string") errors.push(`${e.name} metadata without args.name`);
128
+ if (e.name === "thread_name" && typeof name === "string") threads[e.tid] = name;
129
+ continue;
130
+ }
131
+ if (e.ph !== "X") continue;
132
+ if (typeof e.name !== "string" || !Number.isInteger(e.ts) || !Number.isInteger(e.dur) || (e.dur as number) < 0) {
133
+ errors.push(`malformed span ${JSON.stringify(e).slice(0, 120)}`);
134
+ continue;
135
+ }
136
+ let list = byTid.get(e.tid);
137
+ if (!list) byTid.set(e.tid, (list = []));
138
+ list.push({ ts: e.ts as number, dur: e.dur as number, name: e.name });
139
+ if (e.cat === "gpu") backToBack.add(e.tid);
140
+ }
141
+ let spans = 0;
142
+ for (const [tid, list] of byTid) {
143
+ if (!(tid in threads)) errors.push(`thread ${tid} has spans and no name`);
144
+ spans += list.length;
145
+ // Parents first: earlier start, then the longer one.
146
+ list.sort((a, b) => a.ts - b.ts || b.dur - a.dur);
147
+ const stack: { ts: number; end: number; name: string; children: { ts: number; end: number }[] }[] = [];
148
+ const closed: { ts: number; children: { ts: number; end: number }[] }[] = [];
149
+ const close = () => closed.push(stack.pop()!);
150
+ for (const s of list) {
151
+ const end = s.ts + s.dur;
152
+ while (stack.length && stack[stack.length - 1]!.end <= s.ts) close();
153
+ const top = stack[stack.length - 1];
154
+ if (top && end > top.end) {
155
+ errors.push(`thread ${tid}: ${s.name} [${s.ts}, ${end}] overlaps ${top.name} [${top.ts}, ${top.end}] without nesting`);
156
+ continue;
157
+ }
158
+ top?.children.push({ ts: s.ts, end });
159
+ stack.push({ ts: s.ts, end, name: s.name, children: [] });
160
+ }
161
+ while (stack.length) close();
162
+ const parents = closed.filter((p) => p.children.length >= 2);
163
+ const laid = parents.filter((p) => p.children[0]!.ts === p.ts && p.children.every((c, i) => i === 0 || c.ts === p.children[i - 1]!.end));
164
+ if (!backToBack.has(tid) && parents.length >= endToEndAfter && laid.length === parents.length) {
165
+ errors.push(`thread ${tid} (${threads[tid] ?? "?"}): the children of all ${parents.length} parents are laid end to end from the parent's start: starts were derived, not recorded`);
166
+ }
167
+ }
168
+ return { errors, spans, threads };
169
+ }
@@ -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
  });
@@ -2,8 +2,11 @@
2
2
  // game's GameSettings. The game owns the values (a resource, a save); the
3
3
  // screen answers the new ones every tick and calls the hooks: onChange on
4
4
  // every change (apply it at once), onSave when it closes after changes.
5
+ // A new graphics preset is applied to SettingsDesc.world by the screen
6
+ // itself (og.render.applyQuality) before onChange.
5
7
 
6
8
  import type * as Api from "@origingame/engine";
9
+ import { render } from "../runtime.ts";
7
10
  import { kit } from "./bind.ts";
8
11
  import { frame, grow, px, token } from "./util.ts";
9
12
 
@@ -40,7 +43,7 @@ function sliderRow(id: string, label: string, value: number, desc: Api.SliderDes
40
43
  kit.label("Label", label, { width: px(phone ? 96 : 140), nowrap: true });
41
44
  const r = kit.slider("Slider", value, desc) as Api.SliderResult;
42
45
  out = r.value;
43
- kit.label("Value", shown, { width: px(48), align: "end", font: "mono", color: token("fgMuted"), nowrap: true });
46
+ kit.label("Value", shown, { width: px(48), align: "end", font: "mono", color: token("inkMuted"), nowrap: true });
44
47
  });
45
48
  return out;
46
49
  }
@@ -82,7 +85,7 @@ export function settingsScreen(id: string, desc: Api.SettingsDesc = {}): Api.Set
82
85
  }
83
86
  case "graphics": {
84
87
  const quality = desc.qualityLabel ?? "Quality";
85
- kit.label("QualityLabel", quality, { step: "meta", color: token("fgMuted") });
88
+ kit.label("QualityLabel", quality, { step: "meta", color: token("inkMuted") });
86
89
  const levels = [desc.lowLabel ?? "Low", desc.mediumLabel ?? "Medium", desc.highLabel ?? "High"];
87
90
  const q = kit.radio("Quality", levels, QUALITIES.indexOf(s.quality), { horizontal: !phone, label: quality });
88
91
  s.quality = QUALITIES[q.selected] ?? "medium";
@@ -119,6 +122,12 @@ export function settingsScreen(id: string, desc: Api.SettingsDesc = {}): Api.Set
119
122
  const changed = JSON.stringify(s) !== JSON.stringify(before);
120
123
  if (changed) {
121
124
  dirty = true;
125
+ // The preset is RenderSettings' quality fields (og.render.applyQuality):
126
+ // the next frame draws it, and the probe reads what is drawn.
127
+ if (s.quality !== before.quality && desc.world) {
128
+ const r = render.applyQuality(desc.world as unknown as Parameters<typeof render.applyQuality>[0], s.quality) as Api.Result<void>;
129
+ if (!r.ok) throw new Error(`og.ui.settingsScreen: ${r.reason}`);
130
+ }
122
131
  desc.onChange?.(s);
123
132
  }
124
133
  if (closed && dirty) {