@origingame/engine 0.1.1 → 0.2.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 (59) hide show
  1. package/dist/web/origin-engine.js +4 -4
  2. package/dist/web/origin-engine_bg.wasm +0 -0
  3. package/dist/web-publish/origin-engine-lanes.js +6 -6
  4. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  5. package/dist/web-publish/origin-engine.js +4 -4
  6. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  7. package/dist/web-publish-2d/origin-engine-lanes.js +6 -6
  8. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  9. package/dist/web-publish-2d/origin-engine.js +4 -4
  10. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  11. package/knowledge/package.json +4 -4
  12. package/knowledge/scaffold/origin-engine.json +2 -2
  13. package/knowledge/scaffold/template/package.json +1 -1
  14. package/knowledge/scaffold/template/project.ogt +1 -1
  15. package/knowledge/scripts/validate.ts +2 -1
  16. package/knowledge/skills/og-build/SKILL.md +17 -1
  17. package/knowledge/skills/og-feel/SKILL.md +8 -1
  18. package/knowledge/skills/og-game/SKILL.md +117 -13
  19. package/knowledge/skills/og-i18n/SKILL.md +12 -3
  20. package/knowledge/skills/og-platform-sdk/SKILL.md +3 -1
  21. package/knowledge/skills/og-ui/SKILL.md +19 -6
  22. package/knowledge/wiki/ai.md +20 -20
  23. package/knowledge/wiki/anim.md +41 -41
  24. package/knowledge/wiki/atmosphere.md +13 -13
  25. package/knowledge/wiki/audio.md +18 -18
  26. package/knowledge/wiki/camera.md +13 -13
  27. package/knowledge/wiki/commands.md +11 -11
  28. package/knowledge/wiki/game.md +8 -8
  29. package/knowledge/wiki/i18n.md +7 -7
  30. package/knowledge/wiki/index.md +4 -1
  31. package/knowledge/wiki/input.md +47 -47
  32. package/knowledge/wiki/load.md +45 -45
  33. package/knowledge/wiki/math.md +7 -7
  34. package/knowledge/wiki/nav.md +34 -34
  35. package/knowledge/wiki/physics.md +124 -106
  36. package/knowledge/wiki/platform.md +31 -31
  37. package/knowledge/wiki/probe.md +66 -66
  38. package/knowledge/wiki/reflect.md +15 -15
  39. package/knowledge/wiki/render.md +99 -89
  40. package/knowledge/wiki/rng.md +14 -14
  41. package/knowledge/wiki/stream.md +15 -15
  42. package/knowledge/wiki/system.md +8 -8
  43. package/knowledge/wiki/systems.md +2 -2
  44. package/knowledge/wiki/terrain.md +19 -19
  45. package/knowledge/wiki/tilemap.md +19 -15
  46. package/knowledge/wiki/time.md +4 -4
  47. package/knowledge/wiki/transform.md +8 -8
  48. package/knowledge/wiki/tween.md +17 -17
  49. package/knowledge/wiki/ui.md +289 -245
  50. package/knowledge/wiki/world.md +67 -37
  51. package/package.json +6 -6
  52. package/schema/api.schema.json +134 -9
  53. package/src/index.ts +1 -0
  54. package/src/node/polish.ts +2 -1
  55. package/src/node/run.ts +5 -1
  56. package/src/runtime.ts +2 -0
  57. package/src/ui-kit/huds.ts +18 -14
  58. package/types/bindings.json +61 -26
  59. package/types/index.d.ts +144 -13
@@ -16,6 +16,7 @@
16
16
  "ratio": "a fraction between 0 and 1",
17
17
  "bytes": "bytes",
18
18
  "kg": "kilograms",
19
+ "newton": "newtons: the force that gives a kilogram one metre per second squared",
19
20
  "mps": "metres per second",
20
21
  "mps2": "metres per second squared",
21
22
  "radps": "radians per second",
@@ -133,6 +134,7 @@
133
134
  "Entity",
134
135
  "SpawnDesc",
135
136
  "RestoreDesc",
137
+ "WorldRestored",
136
138
  "QueryDesc"
137
139
  ],
138
140
  "consts": [],
@@ -479,7 +481,7 @@
479
481
  },
480
482
  {
481
483
  "name": "restore",
482
- "doc": "Replace the world with a save that world.snapshot wrote: entities, components, names, resources, events and the modules' state, so the restored world hashes as the saved one did, on either host. The session stays: input (og.Input, og.Pointers, og.Gamepad), the player's bus volumes (og.Audio), the UI's focus and hover, and what lives outside the world (the platform's calls and clock, the language, the loaded assets, the command channel's scheduled steps, which keep their remaining delays though og.Time takes the saved tick). Called in a system or a command it applies when the tick ends, after the last system; elsewhere (GameDesc.start, between ticks) at once. A save a system took mid-tick restores to that moment of its tick (og.Time reads its tick): the next step completes it (the systems after the saver, then the engine's), so the ticks after it are the saved run's. The meshes and height fields its colliders name must be loaded or registered first (a save naming one that is not is refused). A save holds only the world: progress kept in module variables is not in it, so keep it in components and resources.",
484
+ "doc": "Replace the world with a save that world.snapshot wrote: entities, components, names, resources, events and the modules' state, so the restored world hashes as the saved one did, on either host. The session stays: input (og.Input, og.Pointers, og.Gamepad), the player's bus volumes (og.Audio), the UI's focus and hover, and what lives outside the world (the platform's calls and clock, the language, the loaded assets, the command channel's scheduled steps, which keep their remaining delays though og.Time takes the saved tick, so an `og run --commands` script goes on to its last step or `quit` whether the save is of a later tick or an earlier one). Called in a system or a command it applies when the tick ends, after the last system; elsewhere (GameDesc.start, between ticks) at once. Either way the tick after it delivers og.WorldRestored, for the systems that reset what lives in the session. A save a system took mid-tick restores to that moment of its tick (og.Time reads its tick): the next step completes it (the systems after the saver, then the engine's), so the ticks after it are the saved run's. The meshes and height fields its colliders name must be loaded or registered first (a save naming one that is not is refused). A save holds only the world: progress kept in module variables is not in it, so keep it in components and resources.",
483
485
  "params": [
484
486
  {
485
487
  "name": "save",
@@ -1234,6 +1236,12 @@
1234
1236
  "Shader"
1235
1237
  ],
1236
1238
  "consts": [
1239
+ {
1240
+ "name": "PARTICLES_ARMED",
1241
+ "type": "u32",
1242
+ "value": 4294967295,
1243
+ "doc": "ParticleSystem.startTick's default: never written. A looping emitter emits as if started at tick 0; a one-shot (`looping false`) emits nothing until its startTick is written, so a burst spawned in `start` waits for its first use. Write it back to re-arm a one-shot without firing it."
1244
+ },
1237
1245
  {
1238
1246
  "name": "MAX_LIGHTS",
1239
1247
  "type": "u32",
@@ -8030,6 +8038,33 @@
8030
8038
  },
8031
8039
  "additionalProperties": false
8032
8040
  },
8041
+ "WorldRestored": {
8042
+ "x-og-kind": "event",
8043
+ "x-og-module": "world",
8044
+ "description": "A world.restore applied: the tick after it (the first whole tick after a save taken mid-tick, whose next step completes the saved tick) delivers this, read through world.events like any event. Everything in the world came back as saved, request numbers and resources the session filled included; the session did not (og-game §17), so a system that reads this resets what belongs to the session: it forgets requests answered long ago, asks the store again for the settings and progress, and closes a menu the player had open. Sent as that tick starts, not when the restore applies, so the world between the two hashes as the save did.",
8045
+ "type": "object",
8046
+ "properties": {
8047
+ "from": {
8048
+ "type": "integer",
8049
+ "minimum": 0,
8050
+ "maximum": 4294967295,
8051
+ "description": "The tick the world was at when the restore applied.",
8052
+ "x-og-unit": "ticks"
8053
+ },
8054
+ "tick": {
8055
+ "type": "integer",
8056
+ "minimum": 0,
8057
+ "maximum": 4294967295,
8058
+ "description": "The saved tick: og.Time's tick right after the restore.",
8059
+ "x-og-unit": "ticks"
8060
+ }
8061
+ },
8062
+ "required": [
8063
+ "from",
8064
+ "tick"
8065
+ ],
8066
+ "additionalProperties": false
8067
+ },
8033
8068
  "QueryDesc": {
8034
8069
  "x-og-kind": "desc",
8035
8070
  "x-og-module": "world",
@@ -12645,8 +12680,8 @@
12645
12680
  "type": "integer",
12646
12681
  "minimum": 0,
12647
12682
  "maximum": 4294967295,
12648
- "description": "The tick emission started: write og.world tick when spawning a one-shot burst.",
12649
- "default": 0
12683
+ "description": "The tick emission started: write the tick (`time.tick`, `world.tick`) to fire a one-shot burst, or to restart one. Its default, PARTICLES_ARMED, is never written: a looping emitter emits as if started at tick 0, while a one-shot (`looping false`) stays armed and emits nothing until a tick is written, so effects spawned ahead of their first use wait.",
12684
+ "default": 4294967295
12650
12685
  },
12651
12686
  "stopTick": {
12652
12687
  "type": "integer",
@@ -14880,7 +14915,7 @@
14880
14915
  "CharacterController": {
14881
14916
  "x-og-kind": "component",
14882
14917
  "x-og-module": "physics",
14883
- "description": "A kinematic character (3D, or top-down 2D): the entity's capsule (or any og.Collider) moved by `move` each tick, sliding along walls, climbing slopes up to `maxSlope` and steps up to `stepHeight`, snapped to the ground, carried by the moving platform it stands on and falling by gravity. It keeps `skin` off every surface, standing that far over the ground; a step is climbed when the ground beyond it is at most `stepHeight` over the ground it stands on. What blocks it is every solid collider its og.Collider's `layers` and `mask` interact with (each one's layers meeting the other's mask), as for bodies. The entity needs an og.Transform and an og.Collider; the step makes it a kinematic body and pushes dynamic bodies it walks into.",
14918
+ "description": "A kinematic character (3D, or top-down 2D): the entity's capsule (or any og.Collider) moved by `move` each tick, sliding along walls, climbing slopes up to `maxSlope` and steps up to `stepHeight`, snapped to the ground, carried by the moving platform it stands on and falling by gravity. It keeps `skin` off every surface, standing that far over the ground; a step is climbed when the ground beyond it is at most `stepHeight` over the ground it stands on. What blocks it is every solid collider its og.Collider's `layers` and `mask` interact with (each one's layers meeting the other's mask), as for bodies. The entity needs an og.Transform and an og.Collider; the step makes it a kinematic body and pushes the dynamic bodies it walks into (`pushBodies`) with at most `pushStrength`, so a light one is shoved and a heavy one holds.",
14884
14919
  "type": "object",
14885
14920
  "properties": {
14886
14921
  "move": {
@@ -14964,9 +14999,17 @@
14964
14999
  },
14965
15000
  "pushBodies": {
14966
15001
  "type": "boolean",
14967
- "description": "Push dynamic bodies it walks into.",
15002
+ "description": "Push dynamic bodies it walks into, with at most `pushStrength`.",
14968
15003
  "default": true
14969
15004
  },
15005
+ "pushStrength": {
15006
+ "type": "number",
15007
+ "description": "The most force it pushes with: each tick the bodies it walks into share an impulse of at most this times the tick's length (mass times the speed it asks of them, capped), so a body of a few kilograms is shoved at its speed while one whose friction holds more (a tonne on the ground) does not move. 0 leaves every body where it is, as `pushBodies` false does.",
15008
+ "default": 1000,
15009
+ "minimum": 0,
15010
+ "x-og-unit": "newton",
15011
+ "x-og-tunable": true
15012
+ },
14970
15013
  "grounded": {
14971
15014
  "type": "boolean",
14972
15015
  "description": "Written: standing on walkable ground after the step.",
@@ -15024,7 +15067,7 @@
15024
15067
  "PlatformerController": {
15025
15068
  "x-og-kind": "component",
15026
15069
  "x-og-module": "physics",
15027
- "description": "A 2D platformer character (the 2D profile): run, jump and fall with designer units (jump height and time to apex, not gravity), coyote time, jump buffering, variable jump height (releasing jump while rising cuts the jump), apex hang, air jumps, wall slide and wall jump, one-way platforms (colliders on `oneWayLayers` are solid from above only; hold `down` to drop through), moving platforms (it rides what it stands on) and corner correction (a jump that clips a ceiling corner by less than `cornerCorrection` slides past it). It keeps `skin` off every surface; what blocks it is every solid collider its og.Collider's `layers` and `mask` interact with (each one's layers meeting the other's mask), as for bodies. The entity needs an og.Transform and an og.Collider (a box or capsule); it becomes a kinematic body.",
15070
+ "description": "A 2D platformer character (the 2D profile): run, jump and fall with designer units (jump height and time to apex, not gravity), coyote time, jump buffering, variable jump height (releasing jump while rising cuts the jump), apex hang, air jumps, wall slide and wall jump, one-way platforms (colliders on `oneWayLayers` are solid from above only; hold `down` to drop through), moving platforms (it rides what it stands on) and corner correction (a jump that clips a ceiling corner by less than `cornerCorrection` slides past it). It keeps `skin` off every surface; what blocks it is every solid collider its og.Collider's `layers` and `mask` interact with (each one's layers meeting the other's mask), as for bodies. The entity needs an og.Transform and an og.Collider (a box or capsule); it becomes a kinematic body and pushes the dynamic bodies it runs into (`pushBodies`) with at most `pushStrength`, so a crate's mass tells: a light one is shoved at the run speed, a heavy one holds (set its og.RigidBody `mass`).",
15028
15071
  "type": "object",
15029
15072
  "properties": {
15030
15073
  "moveX": {
@@ -15242,6 +15285,19 @@
15242
15285
  "minimum": 0.001,
15243
15286
  "x-og-unit": "m"
15244
15287
  },
15288
+ "pushBodies": {
15289
+ "type": "boolean",
15290
+ "description": "Push dynamic bodies it runs into, with at most `pushStrength`.",
15291
+ "default": true
15292
+ },
15293
+ "pushStrength": {
15294
+ "type": "number",
15295
+ "description": "The most force it pushes with: each tick the bodies it runs into share an impulse of at most this times the tick's length (mass times the speed it asks of them, capped), so a body of a few kilograms is shoved at its speed while one whose friction holds more (a tonne on the ground) does not move. 0 leaves every body where it is, as `pushBodies` false does.",
15296
+ "default": 1000,
15297
+ "minimum": 0,
15298
+ "x-og-unit": "newton",
15299
+ "x-og-tunable": true
15300
+ },
15245
15301
  "grounded": {
15246
15302
  "type": "boolean",
15247
15303
  "description": "Written: standing on ground after the step.",
@@ -15335,7 +15391,7 @@
15335
15391
  "CharacterEvent": {
15336
15392
  "x-og-kind": "event",
15337
15393
  "x-og-module": "physics",
15338
- "description": "Something a CharacterController or PlatformerController did this step: the hooks for dust, squash, sounds and camera kicks. Read the next tick.",
15394
+ "description": "Something a CharacterController or PlatformerController did this step: the hooks for dust, squash, sounds and camera kicks. Read the next tick: the controllers move in the physics step, after the game's systems, so a jump pressed in tick T moves the character in T and its CHARACTER_JUMPED is read in T + 1 (og-feel §12 has the same-tick alternative).",
15339
15395
  "type": "object",
15340
15396
  "properties": {
15341
15397
  "entity": {
@@ -18900,7 +18956,7 @@
18900
18956
  "type": "integer",
18901
18957
  "minimum": 0,
18902
18958
  "maximum": 4294967295,
18903
- "description": "The collision layers of those colliders, as bits (Collider.layers).",
18959
+ "description": "The collision layers of those colliders, as bits (Collider.layers), for every collision layer of the map that names none of its own (a map layer's `colliderLayers`, with its `colliderMask`, puts a layer of grates on a one-way layer while the walls stay here).",
18904
18960
  "default": 1
18905
18961
  },
18906
18962
  "friction": {
@@ -18943,6 +18999,18 @@
18943
18999
  "type": "boolean",
18944
19000
  "description": "Its solid tiles collide."
18945
19001
  },
19002
+ "colliderLayers": {
19003
+ "type": "integer",
19004
+ "minimum": 0,
19005
+ "maximum": 4294967295,
19006
+ "description": "The collision layers (og.Collider.layers bits) of its colliders, from its `colliderLayers` line or Tiled int property; 0 (unset) takes Tilemap.colliderLayers."
19007
+ },
19008
+ "colliderMask": {
19009
+ "type": "integer",
19010
+ "minimum": 0,
19011
+ "maximum": 4294967295,
19012
+ "description": "The layers its colliders collide with (og.Collider.mask bits), from its `colliderMask` line or Tiled int property; 0 (unset) is every layer."
19013
+ },
18946
19014
  "terrain": {
18947
19015
  "type": "string",
18948
19016
  "description": "The auto-tiling terrain of a terrain layer (its cells are 1 filled or 0 empty); empty for a tile layer."
@@ -18962,6 +19030,8 @@
18962
19030
  "z",
18963
19031
  "parallax",
18964
19032
  "collision",
19033
+ "colliderLayers",
19034
+ "colliderMask",
18965
19035
  "terrain",
18966
19036
  "visible",
18967
19037
  "lit"
@@ -21193,7 +21263,7 @@
21193
21263
  },
21194
21264
  "time": {
21195
21265
  "type": "number",
21196
- "description": "The host's clock when it answered, UTC milliseconds since 1970 (PlatformInfo.clock says whose); 0 when the host gave none.",
21266
+ "description": "The host's clock when it answered, UTC milliseconds since 1970 (PlatformInfo.clock says whose); 0 when the host gave none. A slot call (saveSlot, loadSlot) answers the time of the store answer that ended it.",
21197
21267
  "x-og-unit": "ms"
21198
21268
  }
21199
21269
  },
@@ -25805,6 +25875,16 @@
25805
25875
  "type": "boolean",
25806
25876
  "description": "Show touch controls (a d-pad and Jump) on phones and tablets.",
25807
25877
  "default": false
25878
+ },
25879
+ "jumpLabel": {
25880
+ "type": "string",
25881
+ "description": "The touch Jump button. Every word of the HUD is a field with its English default: a localised game passes its string table's (og.i18n.t).",
25882
+ "default": "Jump"
25883
+ },
25884
+ "pauseLabel": {
25885
+ "type": "string",
25886
+ "description": "The pause button's name (what a screen reader says; the button shows its icon).",
25887
+ "default": "Pause"
25808
25888
  }
25809
25889
  },
25810
25890
  "additionalProperties": false
@@ -25943,6 +26023,21 @@
25943
26023
  "type": "boolean",
25944
26024
  "description": "Show touch controls (a stick and Fire) on phones and tablets.",
25945
26025
  "default": false
26026
+ },
26027
+ "killsLabel": {
26028
+ "type": "string",
26029
+ "description": "The word after the kill count. Every word of the HUD is a field with its English default: a localised game passes its string table's (og.i18n.t).",
26030
+ "default": "kills"
26031
+ },
26032
+ "fireLabel": {
26033
+ "type": "string",
26034
+ "description": "The touch Fire button.",
26035
+ "default": "Fire"
26036
+ },
26037
+ "pauseLabel": {
26038
+ "type": "string",
26039
+ "description": "The pause button's name (what a screen reader says; the button shows its icon).",
26040
+ "default": "Pause"
25946
26041
  }
25947
26042
  },
25948
26043
  "additionalProperties": false
@@ -26007,6 +26102,21 @@
26007
26102
  ],
26008
26103
  "description": "Seconds left; null hides it.",
26009
26104
  "default": null
26105
+ },
26106
+ "movesLabel": {
26107
+ "type": "string",
26108
+ "description": "The caption under the moves left. Every word of the HUD is a field with its English default: a localised game passes its string table's (og.i18n.t).",
26109
+ "default": "moves left"
26110
+ },
26111
+ "targetLabel": {
26112
+ "type": "string",
26113
+ "description": "The line beside the stars; `{target}` is replaced by the target, formatted (1,000). A localised game passes its string table's, with `{target}` in it.",
26114
+ "default": "Target {target}"
26115
+ },
26116
+ "pauseLabel": {
26117
+ "type": "string",
26118
+ "description": "The pause button's name (what a screen reader says; the button shows its icon).",
26119
+ "default": "Pause"
26010
26120
  }
26011
26121
  },
26012
26122
  "additionalProperties": false
@@ -26118,6 +26228,21 @@
26118
26228
  "type": "string",
26119
26229
  "description": "The tap target's label; empty hides it.",
26120
26230
  "default": "Tap"
26231
+ },
26232
+ "levelLabel": {
26233
+ "type": "string",
26234
+ "description": "A generator's level badge; `{level}` is replaced by its level. Every word of the HUD is a field with its English default: a localised game passes its string table's (og.i18n.t), with `{level}` in it.",
26235
+ "default": "Lv {level}"
26236
+ },
26237
+ "rateLabel": {
26238
+ "type": "string",
26239
+ "description": "A currency's rate; `{rate}` is replaced by its per-second amount, formatted (+12/s is `+{rate}/s`). A localised game passes its string table's, with `{rate}` in it.",
26240
+ "default": "+{rate}/s"
26241
+ },
26242
+ "pauseLabel": {
26243
+ "type": "string",
26244
+ "description": "The pause button's name (what a screen reader says; the button shows its icon).",
26245
+ "default": "Pause"
26121
26246
  }
26122
26247
  },
26123
26248
  "additionalProperties": false
package/src/index.ts CHANGED
@@ -100,6 +100,7 @@ export const MAX_POINTERS: typeof Api.MAX_POINTERS = 5;
100
100
  export const MAX_GAMEPADS: typeof Api.MAX_GAMEPADS = 4;
101
101
  export const MAX_LIGHTS: typeof Api.MAX_LIGHTS = 256;
102
102
  export const MAX_CLUSTER_LIGHTS: typeof Api.MAX_CLUSTER_LIGHTS = 64;
103
+ export const PARTICLES_ARMED: typeof Api.PARTICLES_ARMED = 4294967295;
103
104
  export const UI_LAYER: typeof Api.UI_LAYER = 1000;
104
105
  export const BODY_DYNAMIC: typeof Api.BODY_DYNAMIC = 0;
105
106
  export const BODY_STATIC: typeof Api.BODY_STATIC = 1;
@@ -223,7 +223,8 @@ async function runOne(file: string, entry: string, root: string, out: string): P
223
223
  quit = true;
224
224
  break;
225
225
  }
226
- if ((answer.tick ?? 0) > report!.untilTick) break;
226
+ // Its last step has run (a restore may have moved og.Time, and the steps with it).
227
+ if ((answer.pending ?? ((answer.tick ?? 0) > report!.untilTick ? 0 : 1)) === 0) break;
227
228
  }
228
229
  drain();
229
230
  const wav = join(out, "audio.wav");
package/src/node/run.ts CHANGED
@@ -304,7 +304,11 @@ if (!probing) {
304
304
  break;
305
305
  }
306
306
  if (ticks !== undefined && tick >= ticks) break;
307
- if (ticks === undefined && commands !== undefined && listening === undefined && !watchFile && tick > untilTick) break;
307
+ // The script ends once its last step has run: a restore moving og.Time
308
+ // forward or back moves the steps with it (they keep their delays), so
309
+ // the tick its last step was scheduled for is no end (brasswick #13).
310
+ const left = answer.pending ?? (tick > untilTick ? 0 : 1);
311
+ if (ticks === undefined && commands !== undefined && listening === undefined && !watchFile && left === 0) break;
308
312
  if (realtime || listening !== undefined || watchFile) await sleep(Math.max(0, tickMs - (performance.now() - last)));
309
313
  }
310
314
  }
package/src/runtime.ts CHANGED
@@ -1179,6 +1179,8 @@ export class Engine {
1179
1179
  /** Wall milliseconds a tick took (this frame's mean, or the last frame's that ticked) and the last render took. */
1180
1180
  stepMs?: number;
1181
1181
  renderMs?: number;
1182
+ /** The command channel's steps still to run (scheduled, or a screenshot waiting for its frame): a script has ended at 0, however a restore moved og.Time. */
1183
+ pending?: number;
1182
1184
  reason?: string;
1183
1185
  /** The UI's focused text control (web/text-input.ts's TextTarget), or null. */
1184
1186
  text?: { path: string; rect: [number, number, number, number]; multiline: boolean; selection: string; composing: boolean } | null;
@@ -2,7 +2,9 @@
2
2
  // set of anchored groups over the game in the safe area, plus the touch
3
3
  // controls they show on phones and tablets. HUDs turn keyboard and gamepad
4
4
  // navigation off (the keys move the player); Back still answers `paused`,
5
- // except under an open dialog, which keeps it (og.ui.back).
5
+ // except under an open dialog, which keeps it (og.ui.back). Every word a HUD
6
+ // draws or names is a field of its desc with the English default (og-i18n
7
+ // §2), so a game in another language has none of the kit's English left.
6
8
 
7
9
  import type * as Api from "@origingame/engine";
8
10
  import { kit } from "./bind.ts";
@@ -15,7 +17,8 @@ function hud(): Hud {
15
17
  return { paused: kit.back(), action: null, moveX: 0, moveY: 0, held: [] };
16
18
  }
17
19
 
18
- function pauseButton(out: Hud, label = "Pause"): void {
20
+ /** The pause button: its icon, named by the desc's `pauseLabel` (what a screen reader says). */
21
+ function pauseButton(out: Hud, label: string): void {
19
22
  const r = kit.actionButton("Pause", label, { size: 44, icon: "pause" });
20
23
  if (r.clicked) out.paused = true;
21
24
  }
@@ -85,9 +88,9 @@ export function platformerHud(id: string, desc: Api.PlatformerHudDesc = {}): Api
85
88
  });
86
89
  }
87
90
  if (score !== null) capsule("Score", () => kit.label("Points", fmt(score), { font: "mono", nowrap: true }));
88
- pauseButton(out);
91
+ pauseButton(out, desc.pauseLabel ?? "Pause");
89
92
  });
90
- if ((desc.touch ?? false) && touch) touchControls(out, { dpad: true, buttons: [{ id: "jump", label: "Jump", primary: true }] });
93
+ if ((desc.touch ?? false) && touch) touchControls(out, { dpad: true, buttons: [{ id: "jump", label: desc.jumpLabel ?? "Jump", primary: true }] });
91
94
  return out;
92
95
  }
93
96
 
@@ -120,9 +123,9 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
120
123
  kit.anchor(`${id}.top`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
121
124
  capsule("Kills", () => {
122
125
  kit.label("Count", fmt(desc.kills ?? 0), { font: "mono", nowrap: true });
123
- kit.label("Unit", "kills", { step: "meta", color: token("fgMuted"), nowrap: true });
126
+ kit.label("Unit", desc.killsLabel ?? "kills", { step: "meta", color: token("fgMuted"), nowrap: true });
124
127
  });
125
- pauseButton(out);
128
+ pauseButton(out, desc.pauseLabel ?? "Pause");
126
129
  });
127
130
  kit.anchor(`${id}.ammo`, { anchor: showTouch ? "topRight" : "bottomRight", offsetY: showTouch ? 56 : 0, direction: "column", cross: "end", gap: t.xs }, () => {
128
131
  const mag = desc.magazine ?? 0;
@@ -144,7 +147,7 @@ export function shooterHud(id: string, desc: Api.ShooterHudDesc = {}): Api.HudRe
144
147
  });
145
148
  }
146
149
  if (desc.crosshair ?? true) crosshair();
147
- if (showTouch) touchControls(out, { buttons: [{ id: "fire", label: "Fire", primary: true }] });
150
+ if (showTouch) touchControls(out, { buttons: [{ id: "fire", label: desc.fireLabel ?? "Fire", primary: true }] });
148
151
  return out;
149
152
  }
150
153
 
@@ -156,21 +159,21 @@ export function puzzleHud(id: string, desc: Api.PuzzleHudDesc = {}): Api.HudResu
156
159
  const marks = desc.stars && desc.stars.length > 0 ? desc.stars : [Math.round(target / 3), Math.round((2 * target) / 3), target];
157
160
  kit.anchor(`${id}.left`, { anchor: "topLeft", direction: "column", gap: t.xs }, () => {
158
161
  if (desc.level) kit.label("Level", desc.level, { step: "meta", color: token("fgMuted"), nowrap: true });
159
- if (desc.moves !== null && desc.moves !== undefined) stat("Moves", `${desc.moves}`, "moves left", "xl3");
162
+ if (desc.moves !== null && desc.moves !== undefined) stat("Moves", `${desc.moves}`, desc.movesLabel ?? "moves left", "xl3");
160
163
  });
161
164
  kit.anchor(`${id}.score`, { anchor: "top", direction: "column", cross: "center", gap: t.xs, width: px(260) }, () => {
162
165
  kit.label("Score", fmt(score), { step: "displaySm", font: "display", align: "center", width: grow() });
163
166
  kit.meter("Progress", score, target, { kind: "accent", width: grow(), segments: 3 });
164
167
  kit.row("Stars", { gap: t.sm, cross: "center" }, () => {
165
168
  marks.slice(0, 3).forEach((m, i) => iconOr(kit.idi("Star", i), "star", 20, token(score >= m ? "accent" : "fillStrong")));
166
- kit.label("Target", `Target ${fmt(target)}`, { step: "meta", color: token("fgMuted"), nowrap: true });
169
+ kit.label("Target", (desc.targetLabel ?? "Target {target}").replaceAll("{target}", fmt(target)), { step: "meta", color: token("fgMuted"), nowrap: true });
167
170
  });
168
171
  });
169
172
  kit.anchor(`${id}.right`, { anchor: "topRight", gap: t.sm, cross: "center" }, () => {
170
173
  if (desc.timeLeft !== null && desc.timeLeft !== undefined) {
171
174
  capsule("Timer", () => kit.label("Time", clock(desc.timeLeft!), { font: "mono", color: token(desc.timeLeft! <= 10 ? "danger" : "fg"), nowrap: true }));
172
175
  }
173
- pauseButton(out);
176
+ pauseButton(out, desc.pauseLabel ?? "Pause");
174
177
  });
175
178
  return out;
176
179
  }
@@ -185,10 +188,10 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
185
188
  capsule(c.id ?? kit.idi("Currency", i), () => {
186
189
  if (c.icon) iconOr("Icon", c.icon, 20, token("accent"));
187
190
  kit.label("Amount", fmt(c.amount ?? 0), { step: i === 0 ? "lg" : "body", font: "mono", nowrap: true });
188
- if ((c.perSecond ?? 0) !== 0) kit.label("Rate", `+${fmt(c.perSecond ?? 0)}/s`, { step: "meta", color: token("success"), nowrap: true });
191
+ if ((c.perSecond ?? 0) !== 0) kit.label("Rate", (desc.rateLabel ?? "+{rate}/s").replaceAll("{rate}", fmt(c.perSecond ?? 0)), { step: "meta", color: token("success"), nowrap: true });
189
192
  }, { height: i === 0 ? t.controlLg : t.controlMd });
190
193
  });
191
- pauseButton(out);
194
+ pauseButton(out, desc.pauseLabel ?? "Pause");
192
195
  });
193
196
  if (desc.tapLabel !== "") {
194
197
  kit.anchor(`${id}.tap`, { anchor: phone ? "top" : "center", offsetY: phone ? 96 : 0, offsetX: phone ? 0 : -150 }, () => {
@@ -205,7 +208,7 @@ export function idleHud(id: string, desc: Api.IdleHudDesc = {}): Api.HudResult {
205
208
  kit.column("Text", { width: grow(), gap: t.xxs }, () => {
206
209
  kit.row("Head", { gap: t.sm, cross: "center", width: grow() }, () => {
207
210
  kit.label("Name", g.name ?? g.id ?? "", { step: "lg", nowrap: true, width: grow() });
208
- kit.badge("Level", `Lv ${g.level ?? 0}`, "neutral");
211
+ kit.badge("Level", (desc.levelLabel ?? "Lv {level}").replaceAll("{level}", `${g.level ?? 0}`), "neutral");
209
212
  });
210
213
  kit.meter("Cycle", g.progress ?? 0, 1, { kind: "accent", height: 4, width: grow() });
211
214
  });
@@ -254,7 +257,8 @@ export function racingHud(id: string, desc: Api.RacingHudDesc = {}): Api.HudResu
254
257
  const delta = desc.delta ?? null;
255
258
  if (delta !== null) kit.label("Delta", deltaText(delta), { step: "lg", font: "mono", color: deltaInk(delta), nowrap: true });
256
259
  if (desc.best !== null && desc.best !== undefined) {
257
- kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", clock(desc.best, true)), { step: "meta", font: "mono", color: token("fgMuted"), nowrap: true });
260
+ // A finished lap: m:ss.mmm as lapTime cuts it (30.717 s is 0:30.717, where the running clock's floor reads 0:30.716).
261
+ kit.label("Best", (desc.bestLabel ?? "Best {best}").replaceAll("{best}", lapTime(desc.best)), { step: "meta", font: "mono", color: token("fgMuted"), nowrap: true });
258
262
  }
259
263
  const splits = desc.splits ?? [];
260
264
  if (splits.length > 0) {