@origingame/engine 0.4.0 → 0.5.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 (78) hide show
  1. package/dist/web/origin-engine.d.ts +8 -5
  2. package/dist/web/origin-engine.js +39 -19
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +6 -5
  5. package/dist/web-publish/origin-engine-lanes.js +49 -29
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +8 -5
  8. package/dist/web-publish/origin-engine.js +39 -19
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +6 -5
  11. package/dist/web-publish-2d/origin-engine-lanes.js +49 -29
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +8 -5
  14. package/dist/web-publish-2d/origin-engine.js +39 -19
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +6 -5
  17. package/knowledge/looks/catalogue.json +123 -2
  18. package/knowledge/package.json +1 -1
  19. package/knowledge/scaffold/origin-engine.json +2 -2
  20. package/knowledge/scaffold/template/package.json +1 -1
  21. package/knowledge/scaffold/template/project.ogt +1 -1
  22. package/knowledge/scripts/validate.ts +69 -6
  23. package/knowledge/skills/og-audio/SKILL.md +31 -5
  24. package/knowledge/skills/og-build/SKILL.md +44 -4
  25. package/knowledge/skills/og-core/SKILL.md +4 -3
  26. package/knowledge/skills/og-feel/SKILL.md +9 -1
  27. package/knowledge/skills/og-game/SKILL.md +199 -16
  28. package/knowledge/skills/og-platform-sdk/SKILL.md +6 -0
  29. package/knowledge/skills/og-taste/SKILL.md +7 -0
  30. package/knowledge/skills/og-ui/SKILL.md +24 -0
  31. package/knowledge/wiki/ai.md +20 -20
  32. package/knowledge/wiki/anim.md +526 -60
  33. package/knowledge/wiki/atmosphere.md +13 -13
  34. package/knowledge/wiki/audio.md +160 -20
  35. package/knowledge/wiki/camera.md +13 -13
  36. package/knowledge/wiki/commands.md +11 -11
  37. package/knowledge/wiki/debug.md +13 -13
  38. package/knowledge/wiki/game.md +8 -8
  39. package/knowledge/wiki/i18n.md +7 -7
  40. package/knowledge/wiki/index.md +25 -1
  41. package/knowledge/wiki/input.md +47 -47
  42. package/knowledge/wiki/load.md +76 -50
  43. package/knowledge/wiki/math.md +102 -102
  44. package/knowledge/wiki/nav.md +34 -34
  45. package/knowledge/wiki/net.md +10 -10
  46. package/knowledge/wiki/path.md +14 -14
  47. package/knowledge/wiki/physics.md +111 -100
  48. package/knowledge/wiki/platform.md +44 -44
  49. package/knowledge/wiki/probe.md +86 -86
  50. package/knowledge/wiki/reflect.md +17 -17
  51. package/knowledge/wiki/render.md +245 -176
  52. package/knowledge/wiki/rng.md +14 -14
  53. package/knowledge/wiki/shapes.md +26 -26
  54. package/knowledge/wiki/stream.md +15 -15
  55. package/knowledge/wiki/system.md +8 -8
  56. package/knowledge/wiki/systems.md +2 -2
  57. package/knowledge/wiki/terrain.md +19 -19
  58. package/knowledge/wiki/tilemap.md +13 -13
  59. package/knowledge/wiki/time.md +4 -4
  60. package/knowledge/wiki/timeline.md +14 -14
  61. package/knowledge/wiki/transform.md +8 -8
  62. package/knowledge/wiki/tween.md +17 -17
  63. package/knowledge/wiki/ui.md +243 -243
  64. package/knowledge/wiki/video.md +5 -5
  65. package/knowledge/wiki/world.md +36 -36
  66. package/package.json +4 -4
  67. package/schema/api.schema.json +639 -11
  68. package/shell/page.ts +4 -0
  69. package/src/generated/abi.ts +3 -0
  70. package/src/index.ts +11 -0
  71. package/src/node/device.ts +5 -2
  72. package/src/node/polish.ts +34 -15
  73. package/src/node/run.ts +23 -6
  74. package/src/runtime.ts +1 -0
  75. package/src/web/gl-state.ts +907 -0
  76. package/src/web/worker.ts +6 -0
  77. package/types/bindings.json +823 -349
  78. package/types/index.d.ts +650 -15
@@ -53,8 +53,9 @@ a number and a sound (taste 98, feel 79).
53
53
  `get:<Resource>[.<field>]` or `get:<entity>[.<Component>[.<field>]]`
54
54
  (logs the value as game code sees it, og.Time whole and a light og.DayNight
55
55
  drives as it is drawn, and writes `get-<tick>.json`, then
56
- `get-<tick>-2.json`, ... for more on one tick; a world dump lists
57
- resources by name only, so `get:` is how to read one),
56
+ `get-<tick>-2.json`, ... for more on one tick; `get:Game:game.json`
57
+ names the file; a world dump lists resources by name only, so `get:`
58
+ is how to read one),
58
59
  `set:<entity>.<Component>.<field>=<value>` or
59
60
  `set:<Resource>.<field>=<value>` (`set:og.Camera.trauma=0.5`,
60
61
  `set:Game.coins=10`, `set:og.Stylise.ink=false`: a number, `true` or
@@ -98,7 +99,46 @@ each its argument form and one sentence in `GameDesc.commandHelp`
98
99
  (`{ spawn_wave: { usage: "<count>", doc: "Spawns a wave now." } }`)
99
100
  so `help` explains it (it lists `spawn_wave:<count>`: the name, then the
100
101
  usage). An entry naming no action is logged when the game
101
- starts.
102
+ starts. A step's arguments are its colon-delimited fields after the
103
+ action, never shell words: `0.5:records:12:34.5` hands the handler
104
+ `args[0]` `"12"` and `args[1]` `"34.5"`, and its usage is written the same
105
+ way, `usage: "<berries>:<seconds>"` (a space stays inside its field:
106
+ `records:12 34.5` is one argument). A game may not take a core action's
107
+ name; og-game §3 lists them, with `open_pause` beside the engine's `pause`.
108
+
109
+ **File names.** Every output of one script has its own file. An unnamed
110
+ output takes the action and the tick (`get-000030.json`), and a second on
111
+ the same tick is suffixed quietly (`get-000030-2.json`). A named one that
112
+ an earlier step of the script already wrote, at any time (a copy-pasted
113
+ `0.5:get:Game:met.json`), is written as `met-2.json`, then `met-3.json`,
114
+ never over the first, and logs a WARN naming both steps ("'met.json' was
115
+ already written by the 0.5s get step, so this one is 'met-2.json'"). The
116
+ WARN is in `run.warnings`, so a playtest that asserts a clean run
117
+ (`t.equal(run.warnings.length, 0, ...)`) fails on the clash, and
118
+ `run.json("met.json")` reads the first step's: give each named step its
119
+ own file.
120
+
121
+ **Values in the JSON.** A dump and a `get` answer write a vector as an
122
+ array in lane order (`Vector3` `[x, y, z]`, `Quaternion` `[x, y, z, w]`,
123
+ `Color` `[r, g, b]`), where the API's values are objects
124
+ (`{ x, y, z }`); `set:` takes the lanes comma-separated, without spaces
125
+ (`set:box.og.Transform.position=4,5,6`). Read a dump by index:
126
+
127
+ ```ts
128
+ // 0.3:get:box.og.Transform:t.json writes {"kind":"get","target":"box.og.Transform","tick":18,"value":{"position":[1,2.5,-3],...}}
129
+ const [x, y, z] = run.json<{ value: { position: [number, number, number] } }>("t.json").value.position;
130
+ t.near(y, 2.5, 1e-4, "the box stands on the ledge");
131
+ ```
132
+
133
+ An `f32` field is IEEE-754 single precision. Game code reads it as the
134
+ f32's exact value promoted to a JavaScript number, so `2.7` written to an
135
+ f32 reads back as `2.700000047683716`, and what game code sends carries
136
+ that number as JSON (the facts, an og.platform payload, so
137
+ `dump_platform`); `get` and the dumps print the f32's shortest form,
138
+ `2.7`. Compare numbers with a tolerance in a test (`t.near(a, b, 1e-4,
139
+ ...)`), round only to display one (`toFixed(1)` on the label), and keep a
140
+ value that must round-trip exactly (a payload a test compares with `===`)
141
+ in an `f64` field.
102
142
 
103
143
  ## 2. Live sessions: the probe and hot reload
104
144
 
@@ -205,7 +245,7 @@ every screen that matters.
205
245
  | taste | UI sounds | a press that changed the UI and started a voice on the ui bus within 8 ticks | every press | og-taste §6, og-audio §3 |
206
246
  | taste | Locale keys | `og cook`'s warnings | none | og-i18n §1 |
207
247
  | feel | Input latency | ticks from the tick an input lands to the first thing seen or heard to change (a named entity accelerating or changing clip, the UI, a sound, a tween, a floating text, shake, hit stop, a particle burst); a press answered on its release counts from the release | 0–1 ticks | og-feel §1 |
208
- | feel | Hit feedback | each damage moment (a fact named `hp`, `health`, `lives`… falling, or `hits`, `damage`, `kills`… rising): hit stop, shake, floating text and a sound within 8 ticks | three of the four | og-feel §3, §4, §7, og-audio §1 |
248
+ | feel | Hit feedback | each damage moment (a fact named `hp`, `health`, `lives`… falling, or `hits`, `damage`, `kills`… rising): hit stop, shake, floating text and a sound within 8 ticks | three of the four, per moment (a moment with three owes no fourth: a bump that holds, shakes and sounds is not asked for a number) | og-feel §3, §4, §7, og-audio §1 |
209
249
  | feel | Idle frames | ticks after the first ten where nothing moves or animates, no UI moves, no sound starts or plays, no tween, text or particle runs | at most 10 % | og-feel §9, og-taste §5 |
210
250
  | feel | Sound on input | inputs answered by a sound within 6 ticks | every action | og-audio §1 |
211
251
  | feel | Frame time | step + render, p95, on the tier it ran on (`metal` on a Mac, `vulkan` on a GPU); the probe's own screenshots (render, read back, PNG) are not the game's step and are left out. Without a renderer the row says `n/a (no renderer)`, as do contrast, text size and HUD occupancy, which read frames | 8.3 ms (half the frame) | og-game §14 |
@@ -240,9 +240,10 @@ export const Move = defineSystem("Move", { reads: [Velocity], writes: [og.Transf
240
240
  entity each.
241
241
  - A handle is a 64-bit number (an `og.load` answer, a texture, a sound,
242
242
  a geometry, a material, an entity): keep it in a field of its own kind,
243
- `Asset()`, `Texture()`, `Sound()`, `Geometry()`, `Material()` or
244
- `Entity()` (each handle type of the API has its factory), in a
245
- component or a resource. A `u32` field refuses one (`4294967298 is not
243
+ `Asset()`, `Texture()`, `Sound()`, `Font()`, `Style()`, `Look()`,
244
+ `Geometry()`, `Material()` or `Entity()` (each handle type of the API
245
+ has its factory, named as the type: a font is a `Font()`, never an
246
+ `Asset()` cast), in a component or a resource. A `u32` field refuses one (`4294967298 is not
246
247
  a u32`).
247
248
  - Per-entity calls (`world.get`, `world.set`) are for commands, setup and
248
249
  the few named entities a system touches (a camera, a listener, the
@@ -169,7 +169,10 @@ it is a cutscene or a reward.
169
169
  ## 7. Floating text
170
170
 
171
171
  `og.render.floatingText(world, { text, position, ... })` pops a text that
172
- rises, drifts and fades by itself (world state, deterministic).
172
+ rises, drifts and fades by itself (world state, deterministic). A label
173
+ that stays with a character (a name tag, a player's number) is not
174
+ floating text: it is screen text where the og.Camera resource's
175
+ `worldToScreen` puts the point, og-ui §3 (Text over a world point).
173
176
 
174
177
  - Damage and score numbers above the hit point, not on the HUD: the eye is
175
178
  already there. Pickups: what was gained (`+1`, `key`), short.
@@ -180,6 +183,11 @@ rises, drifts and fades by itself (world state, deterministic).
180
183
  biggest number on screen should be the most important.
181
184
  - Lifetime 0.6–1.0 s, rise 0.8–1.5 units, spread 0.2–0.4 so stacked hits do
182
185
  not overlap. At most 6–8 on screen: sum rapid hits into one number.
186
+ - Not every impact wants a number. An impact is held by three of four
187
+ signals (hit stop §3, shake §4, floating text, a sound): `og polish`
188
+ counts them per damage moment and asks for the missing ones only of a
189
+ moment with fewer than three, so a party game's bump that holds, shakes
190
+ and sounds owes no floating text.
183
191
 
184
192
  ## 8. Camera: look-ahead, dead zones, zones, rails
185
193
 
@@ -213,6 +213,36 @@ export default og.game({
213
213
  channel and playtests can call (`0.5:spawn_wave:3`), with their usage in
214
214
  `GameDesc.commandHelp` for `help` (og-build §1). Add one for each state a
215
215
  test needs to reach quickly, instead of playing there.
216
+ - **Arguments are colon-delimited**, the fields after the name, each a
217
+ string (`Number(args[0])` to read a number); a space is part of its
218
+ field, not a separator, and `\:` writes a colon inside one. Two
219
+ arguments, and the usage that says so:
220
+
221
+ ```ts
222
+ commands: { records: (world, args) => setRecords(world, Number(args[0]), Number(args[1])) },
223
+ commandHelp: { records: { usage: "<berries>:<seconds>", doc: "Sets the isle's records." } },
224
+ ```
225
+
226
+ `0.5:records:12:34.5` hands `args` `["12", "34.5"]`; `records:12 34.5`
227
+ would hand one argument, `"12 34.5"`.
228
+ - **Reserved names.** The channel's core actions are not a game's to
229
+ take, and the game refuses to start on one (`'pause' is a core action;
230
+ register under another name`): `screenshot`, `listen`, `touch_move_to`,
231
+ `touch_move`, `touch_start`, `touch_end`, `touch`, `scroll`, `gamepad`,
232
+ `key_down`, `key_up`, `type`, `compose`, `commit`, `move_to_text`,
233
+ `teleport`, `frame_entity`, `dump_world`, `dump_entity`, `dump_ui`,
234
+ `get`, `set`, `seed`, `pause`, `resume`, `advance`, `describe`,
235
+ `expect`, `save_scene`, `quit` and `help`. The actions the engine's
236
+ modules register are refused the same way (`'view' is a registered
237
+ engine action; register under another name`), in every build:
238
+ `bake_probes`, `cover`, `debug`, `dump_ai`, `dump_desync`,
239
+ `dump_lanes`, `dump_nav`, `dump_platform`, `dump_pulse`,
240
+ `dump_render`, `dump_room`, `dump_stream`, `lanes`, `locale`,
241
+ `terrain_stream`, `timeline`, `turntable` and `view` (`help` marks
242
+ them `(registered)`). The engine's `pause`
243
+ freezes the tick from outside the game; the game's own pause (its menu
244
+ open, `Game.paused` set) is a game action under its own name,
245
+ `open_pause`.
216
246
  - **Facts** (`GameDesc.facts`) return a small JSON object for the probe's
217
247
  `state()`: score, phase, lives. Tests assert on facts and dumps.
218
248
 
@@ -255,7 +285,19 @@ registered schema and refuses a model path that does not exist.
255
285
  (ETC1S for colour, UASTC for normal and linear maps), WAV into QOA, fonts
256
286
  into MSDF atlases, scenes into component tables. Import settings live next
257
287
  to the source as `<file>.ogt` (`texture { kind normal }`,
258
- `font { charset latin chars "开始游戏" }`).
288
+ `font { charset latin chars "开始游戏" }`). The prose here writes a
289
+ block on one line for short; in a file, as in every `.ogt`, a `{` ends
290
+ its key's line and a `}` stands alone on its own (the parser refuses
291
+ `'{' must end its line`):
292
+
293
+ ```text
294
+ type import
295
+ version 1
296
+ ---
297
+ texture {
298
+ kind normal
299
+ }
300
+ ```
259
301
  - At load a cooked texture reaches the GPU block-compressed in the device's
260
302
  format with its mips (BC7, and BC5 for normal maps, on desktop GPUs; ASTC
261
303
  on Apple and most Android; ETC2 and EAC elsewhere), a quarter of RGBA8's
@@ -411,6 +453,22 @@ else ignores it. The stamp is world state, so a replay reproduces it.
411
453
  ```ts
412
454
  world.insert(hero, og.Sprite, { texture: tex, width: 1, height: 1.5, anchor: { x: 0.5, y: 0 }, dropShadow: 0.5, shadow: 0.3 });
413
455
  ```
456
+ - **Billboards** (three.js `Sprite`): `og.Sprite.facing` (or
457
+ `SpriteDesc.facing`) turns a world sprite to the camera that draws it.
458
+ `"cameraY"` stands it upright and turns it about y (trees, characters,
459
+ props a looking-down camera must not tip back); `"camera"` lays it in
460
+ the view plane (smoke, glints, signs); `"none"`, the default, keeps the
461
+ xy plane. Every camera turns it to itself (og.CameraView, og.Mirror, reflection
462
+ probes), the turn is render-only (never read by systems or snapshots),
463
+ faced sprites in one layer draw back to front from the camera and are
464
+ culled off screen, and a faced sprite's `dropShadow` is its whole shape
465
+ from any angle. It is still a sprite: flash, glow, blend, 2D lighting,
466
+ arrays, flipbooks and `toneMapped` all apply. A billboard behind a
467
+ transparent mesh still draws over it (sprites draw after the 3D scene).
468
+
469
+ ```ts
470
+ world.insert(tree, og.Sprite, { texture: treeTex, width: 2, height: 2.6, anchor: { x: 0.5, y: 0 }, facing: "cameraY", dropShadow: 0.6 });
471
+ ```
414
472
  - **World UI that must not change with the light** (speech bubbles, name
415
473
  tags, markers) takes `og.Sprite.toneMapped: false` (or
416
474
  `SpriteDesc.toneMapped`), three.js's `Material.toneMapped`: still placed
@@ -717,6 +775,65 @@ strike, a bite or a fall at weight 1, back to 0 when `finished`.
717
775
  Transform by the root joint's xz travel and yaw, `full` by all of it,
718
776
  `off` (the default) leaves the hips travelling inside the model; switch it
719
777
  to `planar` for a lunge's clip and back to `off` after.
778
+ **Look-at and joint limits.** `og.LookAt { skeleton, joint, target }`
779
+ on its own entity turns a joint (a head, an eye, a turret's barrel) so its
780
+ `axis` (default +Z, as three.js `lookAt`) points at `target` after the
781
+ clips and IK, its `upAxis` (+Y) rolling towards `up` (world +Y; zero
782
+ keeps the animated roll). `maxYaw` and `maxPitch` (radians from the
783
+ animated forward) hold a head from turning round, `clamped` reads back
784
+ when they did; `bones: 3` spreads the turn over neck, chest and spine;
785
+ `weight` fades it in and out. `og.JointLimit { skeleton, joint, axis,
786
+ swing, twistMin, twistMax }` (radians from the rest pose) limits what
787
+ `og.IkChain` and `og.LookAt` may turn a joint to: a knee is a hinge,
788
+ `axis` +X, `swing: 0.15`, `twistMin: 0`, `twistMax: 2.6`, so a
789
+ foot IK target never bends it backwards; a shoulder is a cone (`swing`).
790
+ The defaults limit nothing, and the clip's own pose is never clamped.
791
+
792
+ ```ts
793
+ const look = must(world.spawn({ name: "hero-look" }));
794
+ must(world.insert(look, og.LookAt, { skeleton: body, joint: HEAD, bones: 2, maxYaw: 1.2, maxPitch: 0.5 }));
795
+ const knee = must(world.spawn({ name: "hero-knee-l" }));
796
+ must(world.insert(knee, og.JointLimit, { skeleton: body, joint: SHIN_L, axis: { x: 1, y: 0, z: 0 }, swing: 0.15, twistMin: 0, twistMax: 2.6 }));
797
+ // each tick, from a system that writes og.LookAt: world.set(look, og.LookAt, { target: playerPosition })
798
+ ```
799
+
800
+ **Foot locking.** `og.FootLock { skeleton, foot }` on its own entity, one
801
+ per foot, keeps a foot the clips plant (within 5 mm of its lowest in the
802
+ clip, the same test retargeting uses) where it touched the ground while
803
+ root motion carries the body on and clips cross-fade, bending the leg
804
+ (two-bone IK, `og.JointLimit`s respected); when the clips lift it the
805
+ foot rejoins its animation over `release` (0.15 s). `planted` and
806
+ `locked` read back each tick; `maxDistance` (0.5 m) lets go before a leg
807
+ would stretch. It needs clips whose feet actually rest: a foot that only
808
+ grazes its lowest for a frame is held for that frame only.
809
+
810
+ **Spring bones.** `og.SpringBone { skeleton, joint, bones }` on its own
811
+ entity, one per chain (a tail, ears, a ponytail, a single jiggling node),
812
+ swings `joint` and its `bones` − 1 parents after their parents' motion:
813
+ each joint's tail is a damped spring (`frequency` 3 Hz, `damping` 0.3;
814
+ 1 settles without overshoot) towards where the animation puts it, under
815
+ `gravity` (`{ x: 0, y: -9.8, z: 0 }` droops it), held at the bone's
816
+ length. `space` world (default) lags behind the entity's motion; model
817
+ ignores it (a teleport does not whip the tail). Applied last, after IK
818
+ and look-at; `lag` reads back how far the chain trails. Deterministic and
819
+ saved with the world. `tipFrequency` and `tipDamping` (1) scale the last
820
+ joint's frequency and damping, the joints between in proportion: 0.25 is
821
+ a strand soft at its tip on a stiff root.
822
+
823
+ Keep hair out of the head with `og.SpringCollider { skeleton, joint,
824
+ shape, offset, tail, radius }` on its own entity, a sphere about `offset`
825
+ or a capsule from `offset` to `tail` in the joint's frame: every tail of
826
+ that skeleton's springs is pushed out of it to its `radius` plus the
827
+ spring's `radius`, keeping its bone's length. A collider's `layers` (1)
828
+ must meet the spring's `colliders` (all) to count.
829
+
830
+ ```ts
831
+ const tail = must(world.spawn({ name: "hero-tail" }));
832
+ must(world.insert(tail, og.SpringBone, { skeleton: body, joint: TAIL_TIP, bones: 3, gravity: { x: 0, y: -4, z: 0 }, tipFrequency: 0.5, radius: 0.02 }));
833
+ const hips = must(world.spawn({ name: "hero-hips-collider" }));
834
+ must(world.insert(hips, og.SpringCollider, { skeleton: body, joint: HIPS, shape: "capsule", offset: { x: -0.15, y: 0, z: 0 }, tail: { x: 0.15, y: 0, z: 0 }, radius: 0.12 }));
835
+ ```
836
+
720
837
  `templates/archetypes/third-person` (`src/anim.ts`: a 1D blend by speed,
721
838
  a hero retargeted at cook time and sentinels at run time) is the pattern,
722
839
  and for a 3D action game as a whole: a CharacterController over a level,
@@ -770,6 +887,7 @@ animation {
770
887
  library "anims/humanoid-clips.glb" # its clips land in this model's #anim, retargeted (repeatable)
771
888
  rig "rigs/hero.ogt" # how this model's bones are named (`type rig`); default: detected from the node names
772
889
  footIk true # retargeting solves the legs so planted feet stay planted (default true)
890
+ mirror "turnLeft" "turnRight" # also write a clip mirrored to the other side, under that name (default "<clip>.mirrored"; repeatable)
773
891
  compress false # quantised samples and keyframe reduction (default false: f32)
774
892
  tolerance 0.0001 # the most compression may move a sampled value (default 0.0001, at most 0.1)
775
893
  }
@@ -789,6 +907,34 @@ stride), and its legs are solved so planted feet stay on the ground
789
907
  (`footIk`, on by default). The fx-showcase `retarget` station is the
790
908
  pattern.
791
909
 
910
+ Hands retarget too: the fingers are humanoid bones (VRM 1.0's
911
+ `leftThumbMetacarpal` … `leftLittleDistal`, read from Mixamo's
912
+ `LeftHandIndex1`, VRoid's `J_Bip_L_Index1`, Rigify's `DEF-f_index.01.L`,
913
+ Unreal's `index_01_l`, plain `index.01.L`; Unity's legacy `Left Thumb
914
+ Proximal` is the metacarpal), each turning from its hand as
915
+ the source's does, so a fist or a grip holds on an A-posed arm whatever
916
+ the arm does. The target's twist bones (unmapped nodes named `twist`, such
917
+ as Unreal's `lowerarm_twist_01_l`, or Rigify's `DEF-forearm.L.001`) take a
918
+ share of their segment's roll by where they sit along it: none at the
919
+ segment's head, the next bone's at its end, so a forearm does not
920
+ candy-wrap. A clip made for the rig itself that leaves its twist bones
921
+ still gets the same shares each tick (after IK); a clip that keys them
922
+ is played as keyed. A character whose rest pose does not stand on the ground (an
923
+ armature lifted off it, a crouched rest) says how high its ankles stand
924
+ in its `type rig` file, `footHeight 0.08` (or `footHeight <left>
925
+ <right>`); planted feet then land on that ground, not at the rest pose's
926
+ foot height.
927
+
928
+ **Mirroring.** `og.anim.mirror(world, { clip, skeleton })` answers a new
929
+ clip that plays `clip` on the other side (a left-handed swing, a limp on
930
+ the other leg, a turn the other way): left and right nodes are paired by
931
+ the side tokens every naming scheme writes (`LeftArm`/`RightArm`,
932
+ `thigh.L`/`thigh.R`, `J_Bip_L_`/`J_Bip_R_`, `calf_l`/`calf_r`), each
933
+ node takes its counterpart's motion reflected across the skeleton's
934
+ x = 0 plane, root motion's sideways travel and turns flip. It assumes a
935
+ symmetric rest pose; mirror a retargeted clip on the skeleton it was
936
+ retargeted for.
937
+
792
938
  **Compression** shrinks a model's clips in the bundle (not in memory: they
793
939
  are rebuilt at load). In the same `animation` block, `compress true`
794
940
  quantises rotations to 48 bits and the rest to 16 and drops the keyframes
@@ -818,6 +964,9 @@ clip walk "walk" {
818
964
  speed 1 # duration multiplier, above 0 (default 1)
819
965
  loop true # false: holds its last frame (default true)
820
966
  }
967
+ clip walkOther "walk" {
968
+ mirror true # plays the clip on the other side of the skeleton (default false): no og.anim.mirror needed
969
+ }
821
970
  clip run "run"
822
971
  clip waving "wave"
823
972
  blend1d move speed { # blend1d <motion> <param>: the two children around the value blend linearly, clamped at the ends
@@ -833,12 +982,14 @@ layer base { # the first layer is the base: root motion and og.An
833
982
  state still idle # state <name> <motion>
834
983
  state move move {
835
984
  speed 1 # playback rate, 0 or more (default 1)
985
+ sync gait # a sync group: states of one group play in step across layers (default none)
836
986
  }
837
987
  entry still # the state it starts in (default: the first)
838
988
  transition still move { # transition <from | any> <to>; in file order, the first that holds fires
839
989
  when speed gt 0.2 # when <param> gt | ge | lt | le | eq | ne <number>; every `when` must hold
840
990
  exit 0.9 # and the source has played this many cycles (a `when` or an `exit` is required)
841
991
  fade 0.25 # cross-fade seconds (default 0.2)
992
+ interrupt any # what may cut this fade: any (default) | none (it completes first) | source (only the state being left) | next (only the new state's own)
842
993
  }
843
994
  transition any still { # any: from every state but the target
844
995
  when speed le 0.2
@@ -847,7 +998,11 @@ layer base { # the first layer is the base: root motion and og.An
847
998
  layer upper {
848
999
  mode additive # override (default): replaces the pose below | additive: adds each clip's difference from its first frame
849
1000
  weight wave # a number 0 to 1, or a parameter clamped to it (default 1)
850
- state wave waving
1001
+ mask "spine" 0.5 # mask "<node>" [weight 0 to 1, default 1]: that node and every node under it (default: no mask, all 1)
1002
+ mask "arm.L" # a deeper line wins for its branch; nodes under no line take 0
1003
+ state wave waving {
1004
+ sync gait
1005
+ }
851
1006
  }
852
1007
  ```
853
1008
 
@@ -855,7 +1010,16 @@ Each state advances one phase by `dt × AnimationGraph.speed × state speed
855
1010
  / duration`, the duration weighted across a blend's clips, so blended
856
1011
  clips stay in step. Per tick and layer the transitions are checked first,
857
1012
  then the phases advance, then the poses blend; a transition during a fade
858
- replaces it. `og.anim.graph` refuses a parse error naming the line, a
1013
+ replaces it unless its `interrupt` says otherwise (`none` for a
1014
+ committed attack or landing, `source` to let an idle re-target a start
1015
+ that has just begun). A layer's `mask` lines confine it to branches of the
1016
+ skeleton (by node name, the nearest named ancestor's weight; an upper-body
1017
+ layer is `mask "spine"`), so a wave or an aim plays over a walk without
1018
+ freezing the legs. In a sync group the first layer (file order) whose
1019
+ current state is in it leads: every later layer's state of that group
1020
+ plays at the leader's phase, whatever its clip's length, and a transition
1021
+ between two states of one group keeps the phase (a walk-to-run blend
1022
+ keeps the feet in step). `og.anim.graph` refuses a parse error naming the line, a
859
1023
  clip name `clips` lacks, a parameter not declared and a motion that
860
1024
  contains itself. Write the parameters from a system every tick
861
1025
  (`world.set(body, og.AnimationGraph, { p0: speed })`); `state` and
@@ -1671,16 +1835,24 @@ there. `world.set(e, og.Velocity, ...)` refuses on
1671
1835
  an entity without one (a body in `start`, before its first step):
1672
1836
  `world.insert` it there, which adds and sets it.
1673
1837
 
1674
- `og.CollisionStarted` is read on the tick after the physics step. Its
1675
- `normal` points from `a` to `b`; the bodies' `og.Velocity` already contains
1676
- the solver's outgoing velocity, not their incoming impact velocity. Do
1677
- not derive impact weight from those velocities: bodies may already be
1678
- separating. There is no contact-time velocity or impulse on this event.
1679
- For driven bodies, retain the velocity handed to physics in a component
1680
- and read that cache before the next drive system overwrites it (as in
1681
- `examples/rumpus-isle/src/critters.ts`). Project `velocityA - velocityB`
1682
- onto the event normal for closing speed. This estimates linear approach
1683
- speed, not contact-point speed from rotation or forces during the step.
1838
+ `og.CollisionStarted` is read on the tick after the physics step, when
1839
+ the bodies' `og.Velocity` already holds the solver's answer (they may
1840
+ already be parting), so never weigh a hit from og.Velocity. The event
1841
+ carries the hit itself, taken in the step that made the contact. `a` is
1842
+ the lower id, and `normal` points from `a` to `b`. `point` is the deepest
1843
+ contact point.
1844
+ `relativeVelocity` (m/s) is `a`'s velocity relative to `b` at `point` as
1845
+ that step began: each body's linear velocity plus its spin crossed with
1846
+ the point's offset from its centre of mass. A static collider counts as
1847
+ still, and a kinematic body moves at its velocity. Its part along
1848
+ `normal` is the closing speed:
1849
+ `Math.max(0, rv.x * n.x + rv.y * n.y + rv.z * n.z)`, which is positive
1850
+ while they approach. `impulse` (N·s, at least 0) is the normal impulse the
1851
+ solver gave the pair in that step, summed over substeps and contact points:
1852
+ `b` took `impulse` along `normal` and `a` the same against it, so heavier
1853
+ bodies hitting give more. Scale feedback on the closing speed (how fast)
1854
+ or the impulse (how hard, mass included), as
1855
+ `examples/rumpus-isle/src/critters.ts` does for its bumps.
1684
1856
 
1685
1857
  ```ts
1686
1858
  const hit = og.physics.raycast(world, { origin: eye, direction: { x: 0, y: 0, z: -1 }, maxDistance: 30, filter: { mask: 0b10, exclude: player } });
@@ -2821,11 +2993,22 @@ door. Mark the scene's unmoving models `static true` and put an
2821
2993
  entity "hall" {
2822
2994
  model "models/hall.glb"
2823
2995
  static true
2824
- Transform { position 0 0 0 quaternion 0 0 0 1 scale 1 1 1 }
2996
+ Transform {
2997
+ position 0 0 0
2998
+ quaternion 0 0 0 1
2999
+ scale 1 1 1
3000
+ }
2825
3001
  }
2826
3002
  entity "hall-probes" {
2827
- Transform { position 0 2 0 quaternion 0 0 0 1 scale 1 1 1 }
2828
- LightVolume { size 12 4 12 spacing 1 }
3003
+ Transform {
3004
+ position 0 2 0
3005
+ quaternion 0 0 0 1
3006
+ scale 1 1 1
3007
+ }
3008
+ LightVolume {
3009
+ size 12 4 12
3010
+ spacing 1
3011
+ }
2829
3012
  }
2830
3013
  ```
2831
3014
 
@@ -88,6 +88,12 @@ if (answer.ok && typeof answer.data.value === "number") run.best[i] = Math.max(r
88
88
  | AI-generated content shown | `aiMark(world, { kind, label })` | disclosure, not permission |
89
89
  | Play with others in one room | `roomCreate(world, { name })`, `roomJoin(world, room)`, `roomLeave`, `roomSend(world, data, to?)`, `roomPeers`, `roomInfo`; the answers as `og.RoomJoined`, `og.RoomPeerJoined`, `og.RoomPeerLeft`, `og.RoomMessage` (read one with `roomMessage`) | natively `og relay` with `og run --room ws://127.0.0.1:P/<room>`; on the web the Player's rooms, when its hello lists `room.join` (`roomInfo(world).host` is `none` otherwise and joins are refused). A message is this peer's host input only: for a game both peers simulate, use og.net's lockstep (`og.Lockstep`, `og.PlayerInput`, og-game §20) and send nothing yourself |
90
90
 
91
+ A payload keeps the number it was given: an `f32` field's value is single
92
+ precision promoted to a JSON number (`f32(2.7)` sends `2.700000047683716`),
93
+ and the store neither rounds nor widens it. Compare with a tolerance in
94
+ a playtest, round only for display, or keep a value that must compare
95
+ exactly in an `f64` (og-build §1, Values in the JSON).
96
+
91
97
  `og.platform.info(world)` says what answers here (`player`, `local` or
92
98
  `none`) and the methods it answers (`capabilities`): check it rather than
93
99
  trying.
@@ -342,6 +342,13 @@ the look lives in authored art (painted backdrops, frame-by-frame
342
342
  animation, hand-placed pixels); the engine frames it with the look, but
343
343
  the assets carry it, so budget for them.
344
344
 
345
+ **Read `look.profiles` before matching.** It names the project profiles a
346
+ style draws for: `["2d"]` for flipbooks, hand-placed pixels and paper
347
+ cut-outs, `["3d"]` for lit meshes, toon materials and voxels, both for a
348
+ post treatment that frames either. `create --premise` matches only among the
349
+ styles of the game's profile (`--profile`, else the premise's, else 3D);
350
+ `--look` names any style and wins.
351
+
345
352
  **Write the look.** Start from the style's preset and change only what the
346
353
  game needs; your file wins field by field. A style whose `look.presetReady`
347
354
  is false has no preset in this engine yet: leave out `extends` and set the
@@ -105,6 +105,30 @@ layers (`anchor` for HUD corners, `modal` for dialogs,
105
105
  playtest can `touch_move_to` it (`Shop/Panel/Detail/Buy` resolves as
106
106
  `Detail/Buy`).
107
107
 
108
+ **Text over a world point** (a name tag over a critter, a marker over a
109
+ door): project the point each tick with the og.Camera resource's
110
+ `worldToScreen` (null behind the camera: draw nothing) and draw screen
111
+ text there, not world text. World text (`og.render.text` without `screen`) faces +z, rides
112
+ the tone curve and is blurred by depth of field at the frame's edge;
113
+ screen text is never tone mapped, blurred or depth tested, so the tag
114
+ stays crisp and in its colour, and at a `layer` up to `UI_LAYER` (1000) it
115
+ draws under every UI element (the HUD and the dialogs cover it):
116
+
117
+ ```ts
118
+ const Tags = defineSystem("Tags", { reads: [Game, og.Camera] }, (_q, _t, world) => {
119
+ const cam = must(world.resource(og.Camera));
120
+ for (const c of critters(world)) {
121
+ const at = cam.worldToScreen({ x: c.x, y: c.y + 1.25, z: c.z }); // CSS pixels, y down
122
+ if (at === null) continue;
123
+ og.render.text(world, { font, text: c.name, position: { x: at.x, y: at.y, z: 0 }, size: 18, align: "center", outline: 0.7, screen: true, layer: 10 });
124
+ }
125
+ });
126
+ ```
127
+
128
+ Keep the font in a `Font()` field. Two tags within a tag's width of each
129
+ other overlap into one label: lift the later one by its height, or hide
130
+ the farther one's.
131
+
108
132
  Mixed styles in one paragraph are `richText(id, markup)`: `[b]`,
109
133
  `[color=danger]` (a token, or `#rrggbb`), `[size=lg]`, `[font=mono]`,
110
134
  `[icon=coin]`, `[link=shop]…[/link]` and the per-glyph `[wave]`, `[shake]`,
@@ -18,7 +18,7 @@ module ai {
18
18
  # declarations follow, each quoted below
19
19
  ```
20
20
 
21
- <sub>`api.ogt` line 9496</sub>
21
+ <sub>`api.ogt` line 9869</sub>
22
22
 
23
23
  ## Contents
24
24
 
@@ -74,7 +74,7 @@ fn load {
74
74
  }
75
75
  ```
76
76
 
77
- <sub>`api.ogt` line 9668</sub>
77
+ <sub>`api.ogt` line 10041</sub>
78
78
 
79
79
  <a id="fn-parse"></a>
80
80
 
@@ -104,7 +104,7 @@ fn parse {
104
104
  }
105
105
  ```
106
106
 
107
- <sub>`api.ogt` line 9675</sub>
107
+ <sub>`api.ogt` line 10048</sub>
108
108
 
109
109
  <a id="fn-get"></a>
110
110
 
@@ -136,7 +136,7 @@ fn get {
136
136
  }
137
137
  ```
138
138
 
139
- <sub>`api.ogt` line 9682</sub>
139
+ <sub>`api.ogt` line 10055</sub>
140
140
 
141
141
  <a id="fn-set"></a>
142
142
 
@@ -170,7 +170,7 @@ fn set {
170
170
  }
171
171
  ```
172
172
 
173
- <sub>`api.ogt` line 9690</sub>
173
+ <sub>`api.ogt` line 10063</sub>
174
174
 
175
175
  <a id="fn-restart"></a>
176
176
 
@@ -200,7 +200,7 @@ fn restart {
200
200
  }
201
201
  ```
202
202
 
203
- <sub>`api.ogt` line 9699</sub>
203
+ <sub>`api.ogt` line 10072</sub>
204
204
 
205
205
  <a id="fn-noise"></a>
206
206
 
@@ -230,7 +230,7 @@ fn noise {
230
230
  }
231
231
  ```
232
232
 
233
- <sub>`api.ogt` line 9706</sub>
233
+ <sub>`api.ogt` line 10079</sub>
234
234
 
235
235
  <a id="fn-describe"></a>
236
236
 
@@ -260,7 +260,7 @@ fn describe {
260
260
  }
261
261
  ```
262
262
 
263
- <sub>`api.ogt` line 9713</sub>
263
+ <sub>`api.ogt` line 10086</sub>
264
264
 
265
265
  ## Components, resources and events
266
266
 
@@ -310,7 +310,7 @@ component AiAgent {
310
310
  }
311
311
  ```
312
312
 
313
- <sub>`api.ogt` line 9515</sub>
313
+ <sub>`api.ogt` line 9888</sub>
314
314
 
315
315
  <a id="record-Perception"></a>
316
316
 
@@ -411,7 +411,7 @@ component Perception {
411
411
  }
412
412
  ```
413
413
 
414
- <sub>`api.ogt` line 9541</sub>
414
+ <sub>`api.ogt` line 9914</sub>
415
415
 
416
416
  <a id="record-Perceivable"></a>
417
417
 
@@ -440,7 +440,7 @@ component Perceivable {
440
440
  }
441
441
  ```
442
442
 
443
- <sub>`api.ogt` line 9611</sub>
443
+ <sub>`api.ogt` line 9984</sub>
444
444
 
445
445
  <a id="record-AiChanged"></a>
446
446
 
@@ -467,7 +467,7 @@ event AiChanged {
467
467
  }
468
468
  ```
469
469
 
470
- <sub>`api.ogt` line 9623</sub>
470
+ <sub>`api.ogt` line 9996</sub>
471
471
 
472
472
  ## Types
473
473
 
@@ -483,7 +483,7 @@ A generational handle: a number the engine hands out. A dead handle is an error
483
483
  handle Behaviour "A loaded behaviour tree or state machine."
484
484
  ```
485
485
 
486
- <sub>`api.ogt` line 9502</sub>
486
+ <sub>`api.ogt` line 9875</sub>
487
487
 
488
488
  <a id="type-BehaviourInfo"></a>
489
489
 
@@ -512,7 +512,7 @@ struct BehaviourInfo {
512
512
  }
513
513
  ```
514
514
 
515
- <sub>`api.ogt` line 9631</sub>
515
+ <sub>`api.ogt` line 10004</sub>
516
516
 
517
517
  <a id="type-NoiseDesc"></a>
518
518
 
@@ -544,7 +544,7 @@ struct NoiseDesc {
544
544
  }
545
545
  ```
546
546
 
547
- <sub>`api.ogt` line 9641</sub>
547
+ <sub>`api.ogt` line 10014</sub>
548
548
 
549
549
  <a id="type-AiDescription"></a>
550
550
 
@@ -579,7 +579,7 @@ struct AiDescription {
579
579
  }
580
580
  ```
581
581
 
582
- <sub>`api.ogt` line 9655</sub>
582
+ <sub>`api.ogt` line 10028</sub>
583
583
 
584
584
  ## Column kinds
585
585
 
@@ -598,7 +598,7 @@ column Behaviour {
598
598
  }
599
599
  ```
600
600
 
601
- <sub>`api.ogt` line 9504</sub>
601
+ <sub>`api.ogt` line 9877</sub>
602
602
 
603
603
  ## Constants
604
604
 
@@ -612,16 +612,16 @@ column Behaviour {
612
612
  const AI_RUNNING u32 1 "AiAgent.status: the tree or state is still running."
613
613
  ```
614
614
 
615
- <sub>`api.ogt` line 9511</sub>
615
+ <sub>`api.ogt` line 9884</sub>
616
616
 
617
617
  ```ogt
618
618
  const AI_SUCCESS u32 2 "AiAgent.status: the tree succeeded on the last think."
619
619
  ```
620
620
 
621
- <sub>`api.ogt` line 9512</sub>
621
+ <sub>`api.ogt` line 9885</sub>
622
622
 
623
623
  ```ogt
624
624
  const AI_FAILURE u32 3 "AiAgent.status: the tree failed on the last think."
625
625
  ```
626
626
 
627
- <sub>`api.ogt` line 9513</sub>
627
+ <sub>`api.ogt` line 9886</sub>