@origingame/engine 0.0.0-stage → 0.1.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 (158) hide show
  1. package/bin/og.mjs +25 -0
  2. package/dist/web/origin-engine.d.ts +1233 -0
  3. package/dist/web/origin-engine.js +8628 -0
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
  6. package/dist/web-publish/origin-engine-lanes.js +8509 -0
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +1176 -0
  9. package/dist/web-publish/origin-engine.js +8313 -0
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
  12. package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  13. package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
  14. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  15. package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
  16. package/dist/web-publish-2d/origin-engine.js +8313 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  18. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
  19. package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  20. package/knowledge/README.md +31 -0
  21. package/knowledge/knowledge.json +13 -0
  22. package/knowledge/looks/catalogue.json +1871 -0
  23. package/knowledge/package.json +33 -0
  24. package/knowledge/rules/platform.json +329 -0
  25. package/knowledge/scaffold/README.md +27 -0
  26. package/knowledge/scaffold/distribution.json +48 -0
  27. package/knowledge/scaffold/origin-engine.json +19 -0
  28. package/knowledge/scaffold/template/.origin/setup +14 -0
  29. package/knowledge/scaffold/template/LICENSES.md +23 -0
  30. package/knowledge/scaffold/template/README.md +37 -0
  31. package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
  32. package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
  33. package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  34. package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
  35. package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
  36. package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
  37. package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
  38. package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
  39. package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
  40. package/knowledge/scaffold/template/gitattributes +22 -0
  41. package/knowledge/scaffold/template/package.json +18 -0
  42. package/knowledge/scaffold/template/project.ogt +25 -0
  43. package/knowledge/scaffold/template/src/main.ts +246 -0
  44. package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
  45. package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
  46. package/knowledge/scaffold/template/tsconfig.json +17 -0
  47. package/knowledge/scripts/lib.ts +203 -0
  48. package/knowledge/scripts/mount.ts +210 -0
  49. package/knowledge/scripts/pack.ts +24 -0
  50. package/knowledge/scripts/scaffold.ts +72 -0
  51. package/knowledge/scripts/validate.ts +749 -0
  52. package/knowledge/skills/og-ai/SKILL.md +386 -0
  53. package/knowledge/skills/og-audio/SKILL.md +191 -0
  54. package/knowledge/skills/og-build/SKILL.md +330 -0
  55. package/knowledge/skills/og-core/SKILL.md +285 -0
  56. package/knowledge/skills/og-feel/SKILL.md +385 -0
  57. package/knowledge/skills/og-game/SKILL.md +1479 -0
  58. package/knowledge/skills/og-i18n/SKILL.md +220 -0
  59. package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
  60. package/knowledge/skills/og-taste/SKILL.md +537 -0
  61. package/knowledge/skills/og-ui/SKILL.md +444 -0
  62. package/knowledge/wiki/ai.md +627 -0
  63. package/knowledge/wiki/anim.md +1187 -0
  64. package/knowledge/wiki/atmosphere.md +636 -0
  65. package/knowledge/wiki/audio.md +732 -0
  66. package/knowledge/wiki/camera.md +711 -0
  67. package/knowledge/wiki/commands.md +357 -0
  68. package/knowledge/wiki/game.md +220 -0
  69. package/knowledge/wiki/i18n.md +213 -0
  70. package/knowledge/wiki/index.md +1027 -0
  71. package/knowledge/wiki/input.md +1714 -0
  72. package/knowledge/wiki/load.md +1280 -0
  73. package/knowledge/wiki/math.md +166 -0
  74. package/knowledge/wiki/nav.md +1154 -0
  75. package/knowledge/wiki/physics.md +3142 -0
  76. package/knowledge/wiki/platform.md +857 -0
  77. package/knowledge/wiki/probe.md +1692 -0
  78. package/knowledge/wiki/reflect.md +285 -0
  79. package/knowledge/wiki/render.md +3511 -0
  80. package/knowledge/wiki/rng.md +393 -0
  81. package/knowledge/wiki/stream.md +376 -0
  82. package/knowledge/wiki/system.md +197 -0
  83. package/knowledge/wiki/systems.md +43 -0
  84. package/knowledge/wiki/terrain.md +727 -0
  85. package/knowledge/wiki/tilemap.md +422 -0
  86. package/knowledge/wiki/time.md +150 -0
  87. package/knowledge/wiki/transform.md +230 -0
  88. package/knowledge/wiki/tween.md +580 -0
  89. package/knowledge/wiki/ui.md +8000 -0
  90. package/knowledge/wiki/world.md +909 -0
  91. package/package.json +68 -4
  92. package/schema/api.schema.json +29675 -0
  93. package/shell/README.md +22 -0
  94. package/shell/brand/README.md +23 -0
  95. package/shell/brand/icon-192.png +0 -0
  96. package/shell/brand/icon-512.png +0 -0
  97. package/shell/brand/mark-on-paper.svg +6 -0
  98. package/shell/brand/origin-mark.svg +5 -0
  99. package/shell/brand/origin-motion.css +251 -0
  100. package/shell/brand/tokens.css +20 -0
  101. package/shell/brand.ts +132 -0
  102. package/shell/page.ts +715 -0
  103. package/shell/platform.ts +203 -0
  104. package/shell/player.ts +238 -0
  105. package/shell/shell.css +185 -0
  106. package/shell/sw.ts +177 -0
  107. package/shell/transport.ts +515 -0
  108. package/shell/wire.ts +369 -0
  109. package/src/e6.ts +69 -0
  110. package/src/env.d.ts +100 -0
  111. package/src/generated/abi.ts +980 -0
  112. package/src/index.ts +193 -0
  113. package/src/node/audio.ts +86 -0
  114. package/src/node/build.ts +109 -0
  115. package/src/node/device.ts +981 -0
  116. package/src/node/doctor.ts +60 -0
  117. package/src/node/index.ts +102 -0
  118. package/src/node/platform.ts +24 -0
  119. package/src/node/playtest-run.ts +249 -0
  120. package/src/node/png.ts +130 -0
  121. package/src/node/polish.ts +976 -0
  122. package/src/node/run.ts +373 -0
  123. package/src/node/web-build.ts +808 -0
  124. package/src/p2.ts +35 -0
  125. package/src/platform-local.ts +211 -0
  126. package/src/playtest.ts +131 -0
  127. package/src/runtime.ts +1509 -0
  128. package/src/systems/cameras.ts +247 -0
  129. package/src/ui-kit/bind.ts +141 -0
  130. package/src/ui-kit/huds.ts +303 -0
  131. package/src/ui-kit/index.ts +36 -0
  132. package/src/ui-kit/menus.ts +187 -0
  133. package/src/ui-kit/settings.ts +131 -0
  134. package/src/ui-kit/store.ts +222 -0
  135. package/src/ui-kit/story.ts +89 -0
  136. package/src/ui-kit/util.ts +106 -0
  137. package/src/web/a11y.ts +429 -0
  138. package/src/web/audio-worklet.ts +193 -0
  139. package/src/web/audio.ts +307 -0
  140. package/src/web/basis.ts +239 -0
  141. package/src/web/gamepad.ts +123 -0
  142. package/src/web/host.ts +166 -0
  143. package/src/web/input.ts +237 -0
  144. package/src/web/lanes.ts +21 -0
  145. package/src/web/play.ts +20 -0
  146. package/src/web/rpc-ring.ts +110 -0
  147. package/src/web/settle.ts +122 -0
  148. package/src/web/shell.ts +643 -0
  149. package/src/web/text-input.ts +248 -0
  150. package/src/web/worker.ts +1002 -0
  151. package/types/bindings.json +13654 -0
  152. package/types/index.d.ts +20090 -0
  153. package/vendor/basis/LICENSE +201 -0
  154. package/vendor/basis/NOTICE +15 -0
  155. package/vendor/basis/README.md +18 -0
  156. package/vendor/basis/basis_transcoder.js +19 -0
  157. package/vendor/basis/basis_transcoder.wasm +0 -0
  158. package/README.md +0 -3
@@ -0,0 +1,444 @@
1
+ ---
2
+ name: og-ui
3
+ description: Game screens and HUDs on Origin Engine with og.ui — which screen template to pick (title, pause, settings, five HUDs, cutscene, shop, inventory, level select, results, leaderboard, tutorial, loading, dialogs, toasts), how to feed it data and read what it answers, the widget set beneath, focus for keyboard and gamepad, accessibility (the screen-reader mirror), phones, touch gestures and the safe area, text (fields, text areas, selection and the clipboard, input methods, ellipsis, kerning, shaping, font fallback), and the look: skins (style packs) and how their frames tile, count-ups, bursts, shakes, screen transitions and UI sounds. Use when building or changing menus, HUDs or any UI in a project with a project.ogt.
4
+ ---
5
+
6
+ `og.ui` is the engine's immediate-mode UI: a system describes the tree every
7
+ tick, the engine lays it out, draws it in the platform's design tokens and
8
+ answers the probe (`dump_ui`, `touch_move_to`, `move_to_text`). The API
9
+ reference is `reference/ui.md`; this skill is how to use it well.
10
+
11
+ ## What works
12
+
13
+ Two games written from this package alone (`examples/lantern-vale`,
14
+ `examples/hollowbell`) used the main menu, pause, cutscene, shop and
15
+ results templates (the results with a localised Best and New best) and a
16
+ text field with input-method composition, and none needed wiring beyond
17
+ its desc; a skin pack copied into the game restyled every screen, and
18
+ `og polish` measured taste 99 for both. The Kestrel Stair
19
+ (`examples/kestrel-stair`) used the cutscene, tutorial overlay,
20
+ inventory with drag and drop, level select, leaderboard with its honest
21
+ states, settings, results and pause: each worked from its desc, the paper pack
22
+ restyled them all, and every word they draw came from its string tables.
23
+
24
+ ## 1. The rule of the kit
25
+
26
+ Every template and widget is **one call per tick, data in, answer out**:
27
+
28
+ ```ts
29
+ const r = og.ui.shopScreen("Shop", { balance: run.coins, items: SHOP, categories: ["Gear", "Items"] });
30
+ if (r.bought !== null) { /* take the price, grant the item */ }
31
+ if (r.closed) state.screen = "map";
32
+ ```
33
+
34
+ - Keep game state (open flags, the current line, the settings, slots) in a
35
+ component or resource, pass it in every tick, store what comes back. Never
36
+ keep UI state in module variables. A field's or an area's text is game
37
+ state too: keep it in a `str(capacity)` field (og-core §4), four bytes per
38
+ character of the widget's `maxChars` (`name: str(64)` for `maxChars: 16`),
39
+ so the text always fits.
40
+ - What only the UI needs between ticks (hover, focus, scroll, a menu being
41
+ open, the chosen tab of a template, animation progress, toasts) is UI state:
42
+ snapshots and replays carry it. A template that needs a number of its own
43
+ uses `og.ui.memory(id, initial)` / `og.ui.remember(id, value)`.
44
+ - Call UI only inside a system, and read and write the state you pass it
45
+ through the archetype's column views (og-core §4;
46
+ `templates/ui-kit/src/main.ts` shows the pattern).
47
+
48
+ ## 2. Pick a template
49
+
50
+ Screenshots of every template at desktop (1280×720) and on a phone in portrait
51
+ (390×844 at 3×) are in the engine repository under `templates/ui-kit/screens/`;
52
+ the full-size reference frames are `crates/og-ui/references/metal/kit-*.png`,
53
+ and every style pack's screens on a phone `kit-skin-<pack>-phone.png`.
54
+
55
+ | The game needs | Call | It answers |
56
+ |---|---|---|
57
+ | A title screen | `og.ui.mainMenu(id, { title, subtitle, items, footer, instant })` (`instant` while it is the screen the game opens on: §6) | `selected`: the item id |
58
+ | Pause | `og.ui.pauseMenu(id, { open, items })` (Resume, Settings, Quit by default) | `selected`; Back answers `resume` |
59
+ | Settings | `og.ui.settingsScreen(id, { open, settings, languages, onChange, onSave })` | the new `GameSettings`, `changed`, `closed` |
60
+ | A platformer HUD | `og.ui.platformerHud(id, { lives, maxLives, coins, score, level, timeLeft, touch })` (what it is not given it does not draw: `maxLives: 0` no hearts, no `coins` or `score` no capsule for them) | `paused`, `moveX`, `action: "jump"` |
61
+ | A shooter HUD | `og.ui.shooterHud(id, { health, armor, ammo, reserve, abilities, map, touch })` | `paused`, `action` (ability id, `fire`), stick |
62
+ | A puzzle HUD | `og.ui.puzzleHud(id, { level, score, target, stars, moves, timeLeft })` | `paused` |
63
+ | An idle or incremental HUD | `og.ui.idleHud(id, { currencies, generators, tapLabel })` | `action: "tap"` or `"buy:<id>"` |
64
+ | A racing HUD | `og.ui.racingHud(id, { speed, gear, lap, laps, position, racers, time, delta, best, splits, boost, map, touch })` (a time trial: `racers: 0` hides the place, `boostMeter: false` the meter, `delta` and `splits: [{ label, time, delta }]` show its sectors; the words `lapLabel`, `bestLabel`, `pauseLabel`, `boostLabel`, `brakeLabel`, `leftLabel`, `rightLabel`, `unit` from the string table, og-i18n §2) | `paused`, steering, `held` |
65
+ | Dialogue or a cutscene | `og.ui.cutscene(id, { open, lines, index })` | the next `index`, `finished`, `skipped` |
66
+ | A shop | `og.ui.shopScreen(id, { balance, currency, items, categories })` | `bought`, `refused` (Buy pressed short of the price: it shook with the error sound), `closed` |
67
+ | An inventory | `og.ui.inventoryScreen(id, { slots, items })` | `selected`, `moved` (drag and drop), `used`, `closed` |
68
+ | Level select | `og.ui.levelSelect(id, { levels, backLabel, starsLabel, lockedLabel })` (a card is named for a screen reader by its level and `starsLabel`, "2 of 3 stars", a locked one by `lockedLabel`, "3, locked") | `chosen`; `closed` from its Back button, a tap on the scrim, Escape or B |
69
+ | Results | `og.ui.resultsScreen(id, { open, score, best, newBest, stars, stats, shareText, onShare, scoreLabel, bestLabel, newBestLabel })` (the labels from the string table, og-i18n §2); a time trial passes `format: "time"` with the score and best in seconds: the headline counts up as m:ss.mmm (1:36.233) and the lower time is the best | `selected` (`retry`, `next`, `menu`, `share`) |
70
+ | A leaderboard | `og.ui.leaderboardScreen(id, { scopes, entries, loading, error })` | `scope`, `changed`, `retry`, `closed` |
71
+ | Onboarding | `og.ui.tutorialOverlay(id, { open, steps, index })` (each step names an element id) | the next `index`, `finished`, `skipped` |
72
+ | Loading | `og.ui.loadingScreen(id, { title, progress, tips })` | nothing |
73
+ | A yes/no question | `og.ui.confirmDialog(id, { open, title, message, confirmLabel, danger })` | `confirmed`, `cancelled` |
74
+ | A notice | `og.ui.alertDialog(id, { open, title, message })` | true when dismissed |
75
+ | A short message | `og.ui.toast(world, text, { tone })` (anywhere: systems, commands) | its id |
76
+
77
+ Compose them: a HUD with `pauseMenu` over it, `settingsScreen` opened from
78
+ pause, `confirmDialog` over settings. Dialogs stack and the topmost owns focus.
79
+
80
+ Honest data: `loadingScreen` shows a spinner when progress is unknown, never a
81
+ bar that runs ahead; `leaderboardScreen` shows skeletons while loading, the
82
+ error with Retry, and its empty text; never invent scores or counts.
83
+
84
+ ## 3. Below the templates: the widget set
85
+
86
+ Layout (`row`, `column`, `panel`, `scroll`, `anchor`), text and controls
87
+ (`label`, `button`, `toggle`, `slider`, `textField`, `textArea`, `checkbox`,
88
+ `checkboxList`, `radio`, `tabs`, `select`, `stepper`, `chip`, `badge`,
89
+ `keyValue`, `divider`, `spacer`, `icon`, `image`, `progress`, `skeleton`,
90
+ `spinner`), HUD parts (`meter` for health, mana and xp with a trail that
91
+ follows a drop; `cooldown` rings; `minimap`; `dialogue` with typed text),
92
+ touch (`joystick`, `dpad`, `actionButton`, each at least 44 CSS px), lists
93
+ (`list` and `grid`, virtualised: only rows in view are built; items report
94
+ drag and drop; `scroll` with `axes: "horizontal"` or `"both"` for a strip
95
+ of cards or a map, which hands the wheel and drags it cannot use to the
96
+ scroller around it, keeps a flick's momentum and has grabbable
97
+ scrollbars; two fingers on a scroller scroll it on a phone, where one
98
+ finger would press a row), `zoomView` for a map or an artwork two fingers
99
+ pinch and pan (the wheel zooms it on a desktop), `aspect` on `image` (and
100
+ any element) to keep a picture's proportions in a growing column,
101
+ layers (`anchor` for HUD corners, `modal` for dialogs,
102
+ `transition` for fade, slide and scale by the tick, `spotlight`) and
103
+ `toast`. Ids are stable strings: give every control a meaningful id so a
104
+ playtest can `touch_move_to` it (`Shop/Panel/Detail/Buy` resolves as
105
+ `Detail/Buy`).
106
+
107
+ Mixed styles in one paragraph are `richText(id, markup)`: `[b]`,
108
+ `[color=danger]` (a token, or `#rrggbb`), `[size=lg]`, `[font=mono]`,
109
+ `[icon=coin]`, `[link=shop]…[/link]` and the per-glyph `[wave]`, `[shake]`,
110
+ `[pulse]`. Write the markup in the string table, not in code, so each
111
+ translation places its own emphasis (`confirmDialog` and `alertDialog`
112
+ messages are markup). A link is a child element (`Hint/shop`, kind `link`)
113
+ that keys, pads and touch reach like a button; the result's `link` names
114
+ the one clicked. A typo in a tag stays visible as text and warns.
115
+
116
+ `textField` and `textArea` edit as a player's system does, with nothing to
117
+ wire. On a Mac (`Viewport.macKeys`: the web shell sets it from the
118
+ browser's platform, a native host on macOS) Cmd+A, C, X, Z are the
119
+ shortcuts, Cmd+←/→ go to the line's ends, Option+←/→ (and
120
+ Option+Backspace or Delete) move by words, and Ctrl takes the emacs keys
121
+ (A/E the line's ends, F/B a character, N/P a line, D/H delete one, K to
122
+ the paragraph's end, U to its start, W the word before); elsewhere Ctrl
123
+ takes the shortcuts and moves by words. Words are Unicode's (UAX #29):
124
+ `can't` is one, each Chinese character is one, Thai moves a cluster at a
125
+ time. The caret and Backspace step over a whole grapheme cluster (`é`
126
+ typed as e and an accent, an emoji with its skin tone), and ←/→ move as
127
+ they point: in Hebrew or Arabic ← goes on in the text. A double click selects a word and a triple
128
+ click the line, Shift+Z or Y redoes (typing undoes a word at a time, an
129
+ input method's commit at a time), and a drag past an area's edge scrolls
130
+ it. A text the game sets starts the field's undo history again. Script a
131
+ chord with the modifier before the key: `key_down:Meta+ArrowLeft`
132
+ (Command), `key_down:Ctrl+Shift+KeyZ`. A native run on macOS takes the
133
+ Mac's keys, so a script that holds Ctrl or Meta and must edit the same on
134
+ every OS pins them with `og.ui.previewViewport` (`macKeys`).
135
+
136
+ Scripts: a label mixing scripts (`"Level 3 · 古い水車小屋"`) needs no font
137
+ of its own; which face draws which part is one rule, §7 (Fonts).
138
+
139
+ Icons: an icon set cooked from SVGs (an `.ogt` of `type icons` naming
140
+ them; the UI kit ships 72 iconoir icons as `icons/iconoir.ogt`, also under
141
+ its own names `coin`, `gear`, `fire`…, and every kit screen draws them) loads with
142
+ `og.load.font` and becomes the atlas with `og.ui.configure(world, { icons:
143
+ { font } })`; `og.ui.icon(id, "heart", { size, tint })` and `[icon=heart]`
144
+ then draw crisp distance-field glyphs at any size in their ink. A texture
145
+ grid (`icons: { texture, columns, rows, names }`) still works for painted
146
+ icons. Icons sit in `fgMuted` or `fgSubtle` (design language: icons are
147
+ neutral). Sets name their icons differently (iconoir's `coins` and
148
+ `fire-flame`, the skin grids' `coin` and `fire`): an unknown name draws an
149
+ empty slot and warns, so a template that may meet either asks
150
+ `og.ui.hasIcon(name)` first and draws its fallback, or names an icon twice
151
+ in its set (`icon "coin" "coins"`). A path's `stroke-linejoin` is kept
152
+ (a miter stays sharp); a set whose SVGs leave it out says `join round` (or
153
+ `miter`, `bevel`) once.
154
+
155
+ ## 4. Focus: keyboard and gamepad
156
+
157
+ Every element carries an accessibility role, name, value and states
158
+ (`dump_ui`'s `role`, `name`, `value`, `states`; `og.ui.a11y()` answers the
159
+ tree with its focus order). Give every control visible text or a meaningful
160
+ id: an icon-only button is named by its icon, a card by the text inside it,
161
+ else by its id, and a playtest can require that no `focusable` element has
162
+ an empty name (ui-kit's `tests/a11y.og.ts`). Tab walks the tree's order.
163
+ On the web the shell mirrors this tree into the page for screen readers
164
+ (invisible ARIA elements over the canvas, toasts announced, the reader's
165
+ clicks pressing the control), so the names you give are what a blind
166
+ player hears. The page's `prefers-reduced-motion` and `prefers-contrast:
167
+ more` turn on the reduced-motion and high-contrast themes on top of the
168
+ game's own settings: nothing to wire. A stepper is adjusted as a whole
169
+ (VoiceOver's swipe up or down on it steps it), like a slider, and both
170
+ tell the reader their bounds (a node's `valueMin`, `valueMax`, `valueNow`:
171
+ a slider's `min` and `max`, a stepper's when it has them).
172
+
173
+ There are no role fields to set: a node's `role` comes from the
174
+ widget (button, link, text, heading, image, checkbox, slider, textbox,
175
+ progressbar, dialog, status…; layout containers have none) and its `name`
176
+ from its text, then the text inside it, then the label just before it in
177
+ its row, then its id. A group a player never reads as text names itself
178
+ from its desc's `label` (`tabs`, `radio`, `select`: "Apartado", not the id
179
+ `Section`), and an icon or image from its `label`: an icon left without one
180
+ is named by its atlas name, an English word (`star`), so a card of a name
181
+ and three stars reads "1. La escalera star star star" in every language.
182
+ Name the meaningful one in the game's words and leave the rest `""`
183
+ (decoration): `og.ui.icon("Star#0", "star", { label: og.i18n.t(world,
184
+ "stars", { n: 2 }) })`, then `{ label: "" }` for the others. A text field is named by that label (put the label
185
+ and the field in one `row`), else by its placeholder, and the placeholder
186
+ is its `description`: `"Lamplighter"`, then `"Your name"`, never the text
187
+ typed in it (that is its `value`). So the work is in the ids and texts:
188
+ `"Buy"` reads well, `"btn3"` is what a blind player hears. `og.ui.a11y()` answers `{ nodes, focusOrder, focused }`, each
189
+ `A11yNode` with `role`, `name`, `description`, `value`, `states` (disabled, focused,
190
+ checked, selected, expanded, busy…) and its rectangle. A screen reader's
191
+ own requests (focus, activate, increment, decrement) arrive as
192
+ `og.A11yAction` events and are applied to the control at the next frame
193
+ like a press. Prove it with a playtest that walks the tree:
194
+
195
+ ```ts
196
+ const tree = og.ui.a11y();
197
+ const unnamed = tree.nodes.filter((n) => n.states.includes("focusable") && n.name === "");
198
+ // a fact the playtest asserts is 0 (templates/ui-kit/tests/a11y.og.ts)
199
+ ```
200
+
201
+ The mirror exists on the web only; natively the tree is still answered
202
+ for tests.
203
+
204
+ Arrows and the d-pad move focus to the nearest control that way (wrapping at
205
+ the edges), Tab and Shift+Tab walk reading order, Enter, Space and A press,
206
+ Escape and B go back: the topmost dialog or menu closes, otherwise
207
+ `og.ui.back()` answers true once (open pause on it). While a dialog or menu
208
+ is open `og.ui.back()` answers false even when asked before the dialog is
209
+ built that tick, so a HUD drawn under pause or the settings never takes the
210
+ Back that closes them (a locked dialog, which Back does not close, lets it
211
+ through). A gameplay HUD whose keys
212
+ move the player calls `og.ui.setNavigation(false)` (the HUD templates do);
213
+ dialogs turn navigation back on while open. What opens takes the focus:
214
+ a dialog (every modal template: pause, settings, shop, results, confirm)
215
+ its first control, a dialogue box or cutscene the box, so Enter, Space and
216
+ A answer it at once without a Tab first. The UI reads pad 0 from
217
+ `og.Gamepad` itself (d-pad, A, B, the left stick); for a custom binding,
218
+ queue intents with `og.ui.navigate(world, intent)`. A focus move plays the
219
+ `hover` sound and Enter, Space or A the `press` sound, as the pointer does
220
+ (og-audio §3). Script a pad in a
221
+ playtest with `gamepad:a` then `gamepad:-` (release), or `gamepad:-:0,0.9`
222
+ for the stick.
223
+
224
+ Local multiplayer on one screen (a lobby, a versus character select): call
225
+ `og.ui.setPlayers(4)` every tick the screen shows. Pads 1 to 3 then each
226
+ move a ring of their own (in the player's colour) among the same controls
227
+ and press with A; `response.pad` says whose press it was (null for the
228
+ pointer and keyboard, 0 for pad 0), `og.ui.focus(pad)` where each ring is.
229
+ Script them with `gamepad:right@2` then `gamepad:-@2`.
230
+
231
+ The touch controls (joystick, d-pad, action buttons) answer several fingers
232
+ at once: a thumb holding the stick and another on a button both count. Test
233
+ it with `touch_start` on the stick and `touch:1:down:<button id>` /
234
+ `touch:1:up:<button id>` for the second finger.
235
+
236
+ The UI is drawn over the finished frame (after bloom, grading and TAA), so
237
+ its colours are exactly the tokens and it stays sharp whatever camera the
238
+ game uses; a scroll list cuts its rows at the edge per pixel.
239
+
240
+ ## 5. Phones, tablets and the safe area
241
+
242
+ Anchored groups sit inside the safe area (notch, home indicator) at
243
+ `topLeft` … `bottomRight` or `center`, inset by the density's gutter.
244
+ They also keep out of the host's own control over the canvas: on a
245
+ published page the web shell's round Back to the Portal sits top left
246
+ (about 12, 12 to 52, 52 CSS px) and says where in
247
+ `og.ui.frameViewport().covered`, so a group it would overlap moves along
248
+ its edge past it (the racing HUD's place and lap to its right), or, in a
249
+ corner where that would land it on another group (a shooter's vitals on a
250
+ phone), down past it. Every HUD template and any HUD built from
251
+ `og.ui.anchor` groups gets this; a corner drawn another way (screen
252
+ sprites, a row placed by hand) reads `covered` itself, so put a game's own
253
+ HUD in anchored groups. Preview it
254
+ with `og.ui.previewViewport(world, { ..., covered: { x: 12, y: 12, width: 40, height: 40 } })`.
255
+ `og.ui.frameViewport().breakpoint` is `phonePortrait`, `phoneLandscape`,
256
+ `tablet` or `desktop`; templates rearrange themselves by it (tabs become a
257
+ select, side panels move under, touch controls appear when `touch` is set).
258
+ Preview a phone headless with a game command that calls
259
+ `og.ui.previewViewport(world, { width: 390, height: 844, dpr: 3, safeArea: { top: 47, bottom: 34, left: 0, right: 0 }, coarsePointer: true })`
260
+ and `og run --size 1170x2532`.
261
+
262
+ Gestures the UI owns: one finger presses and drags (a slider, a scroll's
263
+ flick, a drag and drop), two fingers on a scroller scroll it (and fling it
264
+ as they lift), two on a
265
+ `zoomView` pinch about their centre and pan it, and an `actionButton` is
266
+ pressed while held and clicked on a tap. There is no swipe, long-press or
267
+ double-tap event: a game that wants one reads `og.Pointers` (`down(id)`,
268
+ `start(id)`, `position(id)`; pointer 0 and up to four more) over ticks,
269
+ deciding by distance and ticks held, which replays exactly. A map or an
270
+ artwork to explore:
271
+
272
+ ```ts
273
+ const z = og.ui.zoomView("Map", { texture: map, width: { kind: "grow" }, aspect: 16 / 9, maxZoom: 4 }); // ZoomResult: zoom, centerX, centerY (UI state)
274
+ ```
275
+
276
+ Tab (or a pad) reaches a zoom view: `+` and `-` zoom it, `0` fits it
277
+ again, the arrows pan it (they stay its own while it is focused). Script two
278
+ fingers with `touch_start` for the first and `touch:1:down:<x,y>` /
279
+ `touch:1:move:<x,y>` / `touch:1:up` for the second
280
+ (`templates/ui-kit/tests/gestures.og.ts`).
281
+
282
+ ## 6. Theming: skins, motion, transitions, sounds
283
+
284
+ The default look is the platform's tokens; a game should have its own. A
285
+ **skin** (`og.load.skin`, an `.ogt` of `type skin`) replaces the palette,
286
+ radii, type ramp, durations, fonts and icons, and dresses every element
287
+ class in nine-slice frames from its atlas (panel, card, button with hover,
288
+ pressed and disabled, field, slot, tooltip, HUD capsule, progress track and
289
+ fill), with ornaments (panel corners, the divider, a heading flourish), a
290
+ panel texture, shadows and glows, press squash and hover lift, entrances and
291
+ sounds. Choosing one is og-taste §1. Six packs to start from are in
292
+ the engine repository under `templates/ui-kit/assets/skins/` (`pixel`,
293
+ `cartoon`, `fantasy`, `scifi`, `paper`, `neon`; copy the directory into the
294
+ game's `assets/skins/` and rename it at will: a skin's ids are read
295
+ relative to its file first, then from `assets/`; og-taste §1 has the one
296
+ line to check, the CJK `fallback`):
297
+
298
+ ```ts
299
+ const skin = og.load.skin(world, "skins/fantasy/skin.ogt"); // atlas, fonts, icons load with it
300
+ og.ui.configure(world, {
301
+ skin: skin.ok ? skin.value : null,
302
+ contrast: "high", // still applies over a skin
303
+ reducedMotion: settings.reducedMotion, // every motion becomes a fade
304
+ sounds: { hover, press, open, close, success, error }, // og.load.sound handles, played on the ui bus
305
+ });
306
+ og.ui.configure(world, { reducedMotion: true }); // later, from the settings: the skin, fonts and sounds stay
307
+ ```
308
+
309
+ `configure` takes a partial UiDesc: a field left out keeps the value the
310
+ last call gave it (or the skin a look applied, `GameDesc.look` and
311
+ `og.render.applyLook`), and `null` clears one (`skin: null` is the
312
+ platform tokens again). A UI left with no font at all warns that it draws
313
+ no text.
314
+
315
+ A skin's `slice` line says how a frame's edges and centre fill: `stretch`
316
+ (the default), `repeat` (tiled at the art's size from the corner, the last
317
+ tile cut) or `round` (a whole number of tiles resized to fit, as CSS
318
+ `round`): tile patterned borders (a rope, a rivet row, a pixel bevel),
319
+ stretch plain ones. `glass <part> <blur> <fill> [tint]` makes a part glass:
320
+ the game behind it blurred (`blur` CSS px), tinted by the part's fill, under
321
+ its frame drawn at `fill` opacity (the scifi pack's dialogs:
322
+ `glass panel 16 0.85 0.15`). Glass blurs the scene and the UI drawn under
323
+ it (a HUD under a glass dialog); it costs a frame copy and a blur pyramid
324
+ when any is on screen (about 0.8 ms at 1280×720 on an M4 Pro), and one more
325
+ pyramid for each glass layer with UI under it, nothing without glass.
326
+ A player whose system asks for less transparency (`prefers-reduced-
327
+ transparency`, or `reducedTransparency: true` in `og.ui.configure` from a
328
+ settings toggle) gets glass parts as opaque surfaces and dialog scrims and
329
+ translucent fills (secondary buttons, chips, tracks) flat. Your own
330
+ see-through overlays should follow: read `og.ui.frameTheme().reducedTransparency`
331
+ (and `reducedMotion`) and draw them opaque.
332
+
333
+ A skin of the game's own art: draw each part as a nine-patch PNG
334
+ (`panel.9.png`, `button.9.png`, `buttonHover.9.png`, ..., `soft.9.png`: the
335
+ art inside a one-pixel border whose top row and left column mark in opaque
336
+ black what stretches) and any ornament as a plain PNG (`corner.png`,
337
+ `divider.png`, `flourish.png`, `tile.png`) in the skin's directory, then
338
+ `og skin pack assets/skins/<name>`: it packs them into `atlas.png` and
339
+ writes the skin file's `atlas`, `slice` and `soft` lines and the ornaments'
340
+ regions, keeping every other line (palette, fonts, a slice's `tint` or
341
+ `repeat`). Re-run it after redrawing a part.
342
+
343
+ Never pass hex colours for chrome: use `{ token: "accent" }`, `"fgMuted"`,
344
+ `"surface"` and the type steps (`displayLg`, `xl`, `meta`); the skin's
345
+ palette answers them. A container that should look like a HUD capsule or a
346
+ card takes `frame: "capsule"` (or `"card"`, `"slot"`…) in its desc; titles
347
+ take `heading: true`. Without a skin both are plain.
348
+
349
+ Motion and juice, all by the tick and in the UI state:
350
+
351
+ | Want | Call |
352
+ |---|---|
353
+ | A score, coins or a reward counting up | `og.ui.counter(id, value, { from: 0, font: "display", step: "displayLg" })` |
354
+ | Sparks when a star or reward lands | `og.ui.burst({ count: 10 })` right after the element |
355
+ | A panel or item popping in | `og.ui.transition(id, show, { kind: "pop" })` (also `fade`, `slideUp`…, `scale`) |
356
+ | A change of game screen | `const s = og.ui.screenTransition("Screens", state.screen, { kind: "iris" }).shown;` then build `s` (`fade`, `wipe`, `iris`, `dissolve`, `slide`; a colour or a texture) |
357
+ | A wrong answer, a locked door, a refused purchase | `og.ui.shake({ sound: true })` right after the element, once, on the tick it happens: it swings three times and comes to rest by itself (the skin's `motion.shake`, else 6 px), with the `error` sound |
358
+
359
+ Skins add the rest themselves: dialog and panel entrances with staggered
360
+ controls, hover lift and press squash on springs (the `hoverSpring 1 350` and `pressSpring 0.55 120` lines of a
361
+ skin's `motion` block: damping ratio and response in ms; below 1 bounces), eased progress bars, toasts flying in,
362
+ a shake and the error sound on a press of a disabled button. Reduced motion
363
+ keeps every shake still and turns the rest into fades. The screen the game
364
+ opens on is built settled (`instant: true` on its `mainMenu`, `anchor` or
365
+ `transition` while it is that screen): the web draws it behind the play
366
+ gate, where an entrance would stand part-way until the tap (og-platform-sdk
367
+ §3). How long and how
368
+ much is og-taste §5 and §6; camera shake is never for UI (og-feel §4).
369
+
370
+ ## 7. Text: labels, fields, areas, input methods
371
+
372
+ Labels lay out by themselves: words break at spaces, CJK between
373
+ characters under kinsoku (og-i18n §3), right-to-left runs reorder, complex
374
+ scripts are shaped (Arabic joins, Devanagari conjuncts) and every font's
375
+ kerning pairs apply. What a game sets:
376
+
377
+ ```ts
378
+ og.ui.label("Quest", quest.text, { width: { kind: "grow" }, maxLines: 2, ellipsis: true, color: { token: "fgMuted" } }); // cut with … on the last line
379
+ og.ui.label("Gold", gold, { font: "rounded", step: "xl", nowrap: true }); // body, display, mono or rounded
380
+ ```
381
+
382
+ `ellipsis` cuts a `nowrap` label (which may then shrink in its row) or the
383
+ last of `maxLines`; the probe still reads the whole text, so a playtest
384
+ can find it. Keep long text in a `scroll` instead of cutting what the
385
+ player must read.
386
+
387
+ Text entry is a field (one line) or an area (several), each one call that
388
+ takes the value and answers it, like every widget (§1):
389
+
390
+ ```ts
391
+ // Save = defineResource("Save", { name: str(64), note: str(1024) }): maxChars × 4 bytes always fits.
392
+ const name = og.ui.textField("Name", save.name, { placeholder: "Your name", maxChars: 16 });
393
+ if (name.response.changed) save.name = name.value;
394
+ if (name.response.submitted) state.screen = "play"; // Enter, once
395
+ const note = og.ui.textArea("Note", save.note, { placeholder: "A note for later", rows: 3, maxChars: 256, width: { kind: "grow" } });
396
+ save.note = note.value;
397
+ ```
398
+
399
+ Both take the press point as the caret, select by drag, Shift with
400
+ arrows, and Cmd (on a Mac) or Ctrl with A; copy, cut and paste use the page's
401
+ clipboard; typed text replaces the selection; the text scrolls to keep the
402
+ caret in view and the caret blinks by the tick (steady under reduced
403
+ motion). An area's Enter is a new line, Up and Down move between lines
404
+ and stay out of focus navigation while it is edited, and `maxChars`
405
+ counts its line breaks. On the web the shell keeps a hidden text box over
406
+ the focused field, so a phone's keyboard opens and an input method
407
+ (Chinese, Japanese, Korean) shows its candidates there; the composition
408
+ is drawn underlined at the caret until it commits. Script them:
409
+ `type:<text>` (the text to the `;` exactly, a trailing space included),
410
+ `compose:<text>` (`compose:` alone ends it) and `commit:<text>`, then read
411
+ the field's value from `dump_ui` or the game's facts
412
+ (`templates/ui-kit/tests/text.og.ts`). Fields take the typed-text stream
413
+ (`og.TextInput`: the page's typing, `type:` and `commit:`); until a world
414
+ has had any, a printable key pressed (`key_down:KeyM`) types its US-layout
415
+ character, and from the first on keys only edit (Backspace, arrows), so
416
+ script text with `type:`, not `key_down`.
417
+
418
+ Fonts (the one rule for every label, field and button):
419
+
420
+ 1. The role's font (body, display, mono, rounded; the skin's or
421
+ `og.ui.configure`'s) when it has every character: the whole label in it.
422
+ 2. Else the first of the locale's fonts (its string table's `fonts` line)
423
+ that has every character: the whole label in that face, its Latin and
424
+ digits too. A CJK subset that carries Latin-1, as the archetypes cook
425
+ theirs, therefore sets `"Lantern Vale · 灯笼谷"` entirely in Noto.
426
+ 3. Else run by run: each run of the label from the first font that has it,
427
+ the role's font first, then the locale's fonts, `UiDesc.fallbackFonts`
428
+ (a Hebrew or Devanagari face, a CJK face cooked with `runtime true`) and
429
+ the skin's `fallback`; a letter and its accents stay in one face. A
430
+ CJK-only subset (lantern-vale's `tools/subset-font.py`) gives this: the
431
+ Latin in the skin's face, the Chinese in Noto, on one line.
432
+
433
+ So choose by the subset: keep Latin in the CJK subset for one face per
434
+ label, leave it out to keep the game's Latin face beside CJK. Tracking
435
+ applies to Latin text only.
436
+
437
+ ## 8. Proving it
438
+
439
+ `templates/ui-kit` in the engine repository runs every template; its
440
+ playtests show how to drive UI from a script: `move_to_text:=shop` then
441
+ `touch_start`/`touch_end`, `key_down:Tab`/`key_up:Tab`, a game command that
442
+ calls `og.ui.navigate`, `dump_ui` for bounds and texts, and frame comparison
443
+ against references. `og polish .` scores the screens' contrast, text size,
444
+ transitions and sounds (og-build §5).