@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.
- package/bin/og.mjs +1 -1
- package/dist/web/origin-engine.d.ts +255 -5
- package/dist/web/origin-engine.js +1584 -125
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +80 -3
- package/dist/web-publish/origin-engine-lanes.js +1288 -82
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +199 -6
- package/dist/web-publish/origin-engine.js +1285 -79
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +65 -4
- package/dist/web-publish-2d/origin-engine-lanes.js +1285 -79
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +198 -5
- package/dist/web-publish-2d/origin-engine.js +1282 -76
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +64 -3
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/looks/catalogue.json +40 -40
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/distribution.json +14 -0
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/README.md +2 -1
- package/knowledge/scaffold/template/assets/audio/sting.ogt +10 -0
- package/knowledge/scaffold/template/assets/timelines/intro.ogt +101 -0
- package/knowledge/scaffold/template/assets/timelines/narration.srt +11 -0
- package/knowledge/scaffold/template/host.d.ts +46 -0
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scaffold/template/src/main.ts +1 -1
- package/knowledge/scaffold/template/tests/frames.ts +134 -0
- package/knowledge/scaffold/template/tests/intro.og.ts +65 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +30 -36
- package/knowledge/scaffold/template/tsconfig.json +1 -1
- package/knowledge/scripts/mount.ts +2 -1
- package/knowledge/scripts/validate.ts +159 -31
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +29 -7
- package/knowledge/skills/og-core/SKILL.md +31 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +1331 -16
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +2 -1
- package/knowledge/skills/og-taste/SKILL.md +82 -73
- package/knowledge/skills/og-ui/SKILL.md +80 -58
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +392 -41
- package/knowledge/wiki/atmosphere.md +36 -13
- package/knowledge/wiki/audio.md +18 -18
- package/knowledge/wiki/camera.md +19 -13
- package/knowledge/wiki/commands.md +19 -13
- package/knowledge/wiki/debug.md +414 -0
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +317 -8
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +238 -70
- package/knowledge/wiki/math.md +2607 -0
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +340 -0
- package/knowledge/wiki/path.md +402 -0
- package/knowledge/wiki/physics.md +95 -95
- package/knowledge/wiki/platform.md +463 -29
- package/knowledge/wiki/probe.md +681 -67
- package/knowledge/wiki/reflect.md +29 -17
- package/knowledge/wiki/render.md +4993 -1998
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +396 -0
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +560 -654
- package/knowledge/wiki/video.md +157 -0
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -3
- package/schema/api.schema.json +28151 -19640
- package/shell/page.ts +26 -2
- package/shell/platform.ts +51 -0
- package/shell/player.ts +15 -2
- package/shell/transport.ts +6 -0
- package/src/depth.ts +71 -0
- package/src/e6.ts +32 -1
- package/src/env.d.ts +5 -1
- package/src/generated/abi.ts +226 -4
- package/src/geom.ts +260 -0
- package/src/index.ts +32 -4
- package/src/light.ts +18 -0
- package/src/math.ts +622 -0
- package/src/node/index.ts +17 -0
- package/src/node/playtest-run.ts +115 -18
- package/src/node/polish.ts +13 -3
- package/src/node/render.ts +291 -0
- package/src/node/run.ts +86 -5
- package/src/p2.ts +19 -1
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +65 -4
- package/src/runtime.ts +100 -9
- package/src/targets.ts +28 -0
- package/src/trace.ts +169 -0
- package/src/ui-kit/huds.ts +34 -34
- package/src/ui-kit/menus.ts +12 -12
- package/src/ui-kit/settings.ts +11 -2
- package/src/ui-kit/store.ts +18 -18
- package/src/ui-kit/story.ts +6 -4
- package/src/ui-kit/util.ts +8 -8
- package/src/web/shell.ts +59 -1
- package/src/web/worker.ts +105 -4
- package/types/bindings.json +9368 -6619
- 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
|
+
}
|
package/src/ui-kit/huds.ts
CHANGED
|
@@ -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,
|
|
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("
|
|
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
|
|
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)
|
|
68
|
+
if (health !== null) hudBox("Lives", () => kit.meter("Health", health, 1, { width: px(120) }));
|
|
69
69
|
else if (maxLives > 0) {
|
|
70
|
-
|
|
71
|
-
for (let i = 0; i < maxLives; i++) iconOr(kit.idi("Life", i), "heart", 20, token(i < (desc.lives ?? 0) ? "danger" : "
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
iconOr("Coin", "coin", 20, token("
|
|
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)
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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" : "
|
|
135
|
-
kit.label("Reserve", `/ ${desc.reserve ?? 0}`, { step: "lg", font: "mono", color: token("
|
|
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("
|
|
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("
|
|
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: "
|
|
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 ? "
|
|
169
|
-
kit.label("Target", (desc.targetLabel ?? "Target {target}").replaceAll("{target}", fmt(target)), { step: "meta", color: token("
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
if (c.icon) iconOr("Icon", c.icon, 20, token("
|
|
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("
|
|
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: "
|
|
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" : "
|
|
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("
|
|
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("
|
|
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("
|
|
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 ? "
|
|
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("
|
|
284
|
-
kit.label("Unit", desc.unit ?? "km/h", { step: "meta", color: token("
|
|
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: "
|
|
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) {
|
package/src/ui-kit/menus.ts
CHANGED
|
@@ -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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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 ? "
|
|
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("
|
|
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("
|
|
162
|
-
if (desc.newBest) kit.transition("Reveal", landed, { kind: "pop" }, () => kit.badge("NewBest", desc.newBestLabel ?? "New best", "
|
|
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
|
});
|
package/src/ui-kit/settings.ts
CHANGED
|
@@ -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("
|
|
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("
|
|
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) {
|