@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,386 @@
1
+ ---
2
+ name: og-ai
3
+ description: Enemies and NPCs on Origin Engine — navigation with og.nav (grids from costs or a tilemap's collision, cooked navmeshes, flow fields, agents that plan and steer) and minds with og.ai (behaviour trees and state machines as .ogt data, sight cones and hearing, blackboards, the game's commands), and how to prove them with dump_ai, dump_nav and playtests. Use when a game needs anything that chases, patrols, flees, guards or wanders, or when an enemy's behaviour is wrong.
4
+ ---
5
+
6
+ An enemy on Origin Engine is three records on an entity and one data file:
7
+ `og.NavAgent` (where it goes, moved by `og.nav`), `og.AiAgent` (which
8
+ behaviour it runs, ticked by `og.ai`), `og.Perception` (what it sees and
9
+ hears), and a behaviour in `assets/ai/<name>.ogt`. Game code does not move
10
+ enemies or switch their states by hand; it sets facts (components,
11
+ resources) the behaviour checks, and handles the commands it raises. The
12
+ API is in `reference/nav.md` and `reference/ai.md`; this skill is how to
13
+ use them.
14
+
15
+ ## What works
16
+
17
+ The Kestrel Stair (`examples/kestrel-stair`, a 3D third-person action
18
+ game written from this package alone) baked its navmesh from the level's
19
+ glTF with one `model` line (§1), and five agents ran on it: imps on a
20
+ behaviour tree (patrol, chase, a 0.25 s wind-up and a bite, flight when
21
+ hurt) and wardens on a machine (patrol, alert, chase, slam, search), each
22
+ raising the game's commands, with sight occluded by the level,
23
+ `og.Steering` separation and avoidance, and `og.nav.setObstacle` closing
24
+ the summit gate until it rose. Two processes of a full session of chases
25
+ end on one world hash.
26
+
27
+ ## 1. Pick the navigation
28
+
29
+ | World | Navigation | How |
30
+ |---|---|---|
31
+ | A tilemap (top-down, dungeon) | a grid from its collision | `og.nav.gridFromTilemap(world, { entity: map })` (`inflate` blocks cells around walls for wide agents) |
32
+ | An open 2D or 3D arena | a grid from costs | `og.nav.grid(world, { width, height, cellSize, origin, costs })`; costs 0 blocked, 1 normal, up to 255 to avoid |
33
+ | Rooms, slopes, floors in 3D | a navmesh baked at cook time | `assets/nav/<level>.ogt` of `type navmesh` (every line below) gives the agent, the cell and tile sizes, the bounds and the geometry; `og cook` voxelises it; `og.nav.load(world, "nav/<level>.ogt")` answers `{ mesh }` |
34
+ | Many units to one goal (crowds, tower defence) | a flow field | `og.nav.flowField(world, { grid, targets })`; one field serves every agent |
35
+
36
+ Doors and crates that block for a while are navmesh obstacles
37
+ (`og.nav.setObstacle`): only the tiles under them rebuild. Never bake a
38
+ navmesh at run time; the wasm does not carry the voxeliser.
39
+
40
+ ### The navmesh file
41
+
42
+ The geometry is every `scene`, `model`, `terrain` and `box` entry
43
+ together (at least one must give triangles); every other line tunes the
44
+ bake. Every line, with its default:
45
+
46
+ ```text
47
+ type navmesh
48
+ version 1
49
+ ---
50
+ agent { # the agent the mesh is for
51
+ radius 0.4 # metres kept from walls: the floor is eroded by it
52
+ height 1.8 # headroom: floor under a lower ceiling is left out
53
+ climb 0.4 # the highest step or ledge walked up or down
54
+ slope 45 # degrees: a steeper triangle is not floor
55
+ }
56
+ cellSize 0.2 # metres across a voxel column (half the radius or less keeps narrow doors open)
57
+ cellHeight 0.1 # metres of a voxel's height
58
+ tileSize 32 # cells per tile side, 8 to 256: an obstacle rebuilds only the tiles it touches
59
+ maxError 1.3 # cells a simplified wall outline may stray from the voxels
60
+ scene "scenes/level.ogt" # a scene's static geometry: its models, and its static colliders as boxes
61
+ model "models/level.glb" { # every triangle of a glTF's default scene (repeatable)
62
+ position 0 0 0 # where the level places it (default 0 0 0)
63
+ quaternion 0 0 0 1 # x y z w (default 0 0 0 1)
64
+ scale 1 1 1 # (default 1 1 1)
65
+ }
66
+ terrain "terrain/vale.ogt" { # a terrain's surface (repeatable)
67
+ position 0 0 0 # where its entity stands (default 0 0 0)
68
+ step 1 # every step-th height sample (default 1: all of them)
69
+ }
70
+ box { # a solid box the level lacks: a plinth, a crate (repeatable)
71
+ center 0 0.5 0 # (default 0 0 0)
72
+ size 1 1 1 # whole sizes (default 1 1 1)
73
+ quaternion 0 0 0 1 # (default 0 0 0 1)
74
+ }
75
+ bounds { # the box baked; left out: the triangles' own bounds
76
+ min -10 -10 -10 # (default -10 -10 -10 inside the block)
77
+ max 10 10 10 # (default 10 10 10)
78
+ }
79
+ ```
80
+
81
+ - Floors are triangles facing up (glTF's counter-clockwise front faces);
82
+ give the agent the body's radius and height, and `climb` the stair's
83
+ riser or more.
84
+ - A scene's dynamic bodies are left out; its sphere and capsule colliders
85
+ bake as their bounding boxes, convex and mesh colliders only through
86
+ their model. A terrain bakes only the cells inside `bounds`.
87
+ - `bounds` cuts what the agents may reach: a min y a little over a lake's
88
+ surface leaves the lake a hole they walk round.
89
+ - The cook refuses any other line (naming it), a scene, model or terrain
90
+ missing under `assets/`, no triangles at all, and a level over 16
91
+ million columns (raise `cellSize` or bound it); its report notes the
92
+ triangles, cells, polygons and tiles it baked.
93
+
94
+ The Kestrel Stair's, baked from its glTF level (`tools/level.ts` writes it
95
+ beside the model):
96
+
97
+ ```text
98
+ type navmesh
99
+ version 1
100
+ ---
101
+ agent {
102
+ radius 0.5
103
+ height 2.6
104
+ climb 0.3
105
+ slope 36
106
+ }
107
+ cellSize 0.25
108
+ cellHeight 0.1
109
+ tileSize 32
110
+ model "models/stair.glb" {
111
+ position 0 0 0
112
+ }
113
+ bounds {
114
+ min -23 -1 -67
115
+ max 23 14 45
116
+ }
117
+ ```
118
+
119
+ Before writing a mind, prove the mesh: load it in `start` and check
120
+ `og.nav.nearest(world, mesh, spawn)` at every spawn point and route
121
+ point (null, or a point far from it, is a place the agents cannot stand;
122
+ og-game's order, step 5).
123
+
124
+ ## 2. The agent
125
+
126
+ ```ts
127
+ const post = og.nav.nearest(world, yard.mesh, spawn) ?? spawn; // on the mesh
128
+ world.set(guard, og.Transform, { position: post });
129
+ world.insert(guard, og.NavAgent, { nav: yard.mesh, target: post, seeking: true, speed: 1.5, acceleration: 12, radius: 0.4, faceMovement: true, turnSpeed: 8 });
130
+ world.insert(guard, og.AiAgent, { behaviour: mind.behaviour });
131
+ world.insert(guard, og.Perception, { sight: 9, fov: 120, eyeHeight: 1.6, team: 2 });
132
+ world.insert(hero, og.Perceivable, { team: 1, eyeHeight: 1 });
133
+ ```
134
+
135
+ - Place every agent on the mesh, at its `og.nav.nearest` point (idle, or
136
+ seeking a target there: an agent that has not planned yet stays where
137
+ it was put, and a teleported one walks on from where it lands), and
138
+ prove it in a playtest: a few ticks in, each agent stands where it was
139
+ put (its height too) and its `status` is not `NAV_UNREACHABLE`.
140
+ - `radius` keeps grid paths clear of walls; set it to the body's.
141
+ - `faceMovement` turns the body to where it walks (3D +Z forward, 2D +X).
142
+ - `drive: false` keeps planning and reporting but leaves the motion to the
143
+ game: set it during a knock-back or a stun, back to true after.
144
+ - og.nav re-plans when the target moves past `repathDistance`, every
145
+ `repathTicks`, and when the agent is pushed more than 1.5 m (or four
146
+ radii) off its path. `status` says `NAV_ARRIVED` or `NAV_UNREACHABLE`;
147
+ read it rather than measuring distances.
148
+
149
+ ## 3. Write the mind as data
150
+
151
+ ```text
152
+ type behaviour
153
+ version 1
154
+ ---
155
+ name "slime"
156
+ route beat {
157
+ point 11 6.5 0
158
+ point 16 6.5 0
159
+ }
160
+ tree {
161
+ selector {
162
+ sequence flee {
163
+ check Slime.hp below 30 # a component field of the agent
164
+ flee {
165
+ distance 6
166
+ }
167
+ }
168
+ sequence bite {
169
+ check sees
170
+ check targetDistance below Tuning.slimeReach # compared with a resource field, read every think
171
+ command bite { # the game's command, the agent's handle first
172
+ cooldown 1
173
+ }
174
+ }
175
+ sequence chase {
176
+ check sees
177
+ chase {
178
+ speed Tuning.slimeSpeed # a resource field, read every think
179
+ }
180
+ }
181
+ patrol beat {
182
+ wait 0.6
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ - A **tree** for reactive enemies (re-checked from the top every think, so
189
+ a higher branch that becomes true takes over); a **machine** (below) for
190
+ NPCs whose mode should stick (patrol, alert, search), with enter and
191
+ exit actions and timed transitions.
192
+ - Checks read the blackboard's built-in keys, your own keys (`key alert
193
+ 0`, `set alert 1`), `Component.field` of the agent and `Resource.field`,
194
+ compared with a literal or with another key or field read each time
195
+ (`check Foe.hp below Tuning.fleeHp`). An unknown operand fails its check
196
+ and logs one warning naming the fix: read the run's warnings.
197
+ - Keep damage, death, loot and knock-back in the game: a behaviour raises
198
+ `command bite` and the game's command handler applies it. Tune numbers
199
+ live in components and resources (`speed Tuning.slimeSpeed`, `check
200
+ targetDistance below Tuning.slimeReach`), never as copies in the .ogt.
201
+ - A new run or a respawn: put the agent back (its Transform, its
202
+ NavAgent's target with `seeking: false`) and call
203
+ `og.ai.restart(world, agent)`: a machine starts again in its initial
204
+ state, a tree at its root, timers, cooldowns and the blackboard reset.
205
+ - Telegraph attacks: a `wait 0.3` or a wind-up state before the command
206
+ (og-feel §2). An enemy that hits on the tick it arrives feels unfair.
207
+ - `thinkEvery 2` or `3` for crowds: agents are staggered, so the cost
208
+ spreads across ticks.
209
+
210
+ ### The behaviour file
211
+
212
+ A file holds a name, its keys, its routes and one `tree` or one
213
+ `machine`. Every line of a tree, with its default:
214
+
215
+ ```text
216
+ type behaviour
217
+ version 1
218
+ ---
219
+ name "slime" # what dump_ai shows (default "behaviour")
220
+ key alert 0 # a blackboard key of yours and its start: a number, x y z, or true/false (repeatable)
221
+ # og.ai's own keys, written every think, which checks read and `key` or `set` may not name: self,
222
+ # position, sees, heard, target, targetPosition, targetDistance, lastSeen, sinceSeen, heardAt,
223
+ # sinceHeard, elapsed (seconds in the machine's state)
224
+ route beat { # a patrol route, declared before the tree names it (repeatable)
225
+ point 11 6.5 0 # x y z (at least one)
226
+ }
227
+ tree { # one root node; or a `machine` (below), never both
228
+ selector main { # children in order: the first that does not fail decides; re-checked from the top every think
229
+ sequence hurt { # children in order while they succeed; re-checked from the first every think
230
+ check Slime.hp below 30 # <operand> [is | not | below | above | atMost | atLeast <value>]; no operator: truthy; the value may be a key or field
231
+ flee { # flee [key]: away from the key's position (default target)
232
+ distance 6 # until this far (default 6)
233
+ speed 3 # metres per second, or a key or field read every think (default: the NavAgent's speed)
234
+ }
235
+ }
236
+ steps attack { # a sequence that resumes at its running child
237
+ move_to lastSeen { # move_to <key> | <x y z>: succeeds on arrival, fails when unreachable
238
+ speed Tuning.slimeSpeed
239
+ }
240
+ wait 0.3 # runs this many seconds, then succeeds
241
+ command bite 2 { # the game's command: the agent's handle, then the words after the name
242
+ cooldown 1 # fails for this long after it ran (default 0)
243
+ }
244
+ }
245
+ parallel watch { # every child every think
246
+ policy all # all (default): succeeds when all do, fails when one does | one: the reverse
247
+ chase { # chase [key]: after an entity key's position (default target)
248
+ reach 0.5 # succeeds within this (default 0.5)
249
+ }
250
+ repeat 0 { # its one child, N times while it succeeds (0: forever)
251
+ cooldown 2 { # after its child succeeds, fails for this many seconds
252
+ timeout 5 { # fails when its child runs longer than this
253
+ invert { # success is failure and failure success
254
+ succeed { # success whatever the child answers (running stays running)
255
+ set alert 1 # a key of yours to a value; succeeds
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ patrol beat { # round a route, resuming at the nearest point
263
+ wait 0.6 # seconds at each point (default 0)
264
+ loop true # false: succeeds at the route's end (default true)
265
+ }
266
+ wander { # to random points round where it started
267
+ radius 4 # (default 4)
268
+ wait 1 # seconds at each (default 0)
269
+ }
270
+ stop # stops moving; succeeds
271
+ }
272
+ }
273
+ ```
274
+
275
+ - An action without `speed` moves at its og.NavAgent's `speed`. A value
276
+ in `key`, `set` and `check` is a number, three numbers (a point) or
277
+ `true`/`false`; a `check`'s may also be a key, `Component.field` or
278
+ `Resource.field`, read every time the check runs (`check Foe.hp below
279
+ Tuning.fleeHp`). Command names are snake_case.
280
+ - The cook refuses an unknown entry, node or option, a route that is not
281
+ declared, a decorator without exactly one child and a malformed value,
282
+ naming the line.
283
+
284
+ A machine replaces the tree. Every think it checks the current state's
285
+ transitions in order and takes the first that holds (running the exit
286
+ actions, then the new state's enter actions), then ticks the state's
287
+ nodes as one sequence. The Kestrel Stair's warden:
288
+
289
+ ```text
290
+ type behaviour
291
+ version 1
292
+ ---
293
+ name "warden-0"
294
+ route round {
295
+ point 6 4 -2
296
+ point 6 4 -14
297
+ point -4 4 -12
298
+ }
299
+ machine {
300
+ initial patrol # the state it starts in (default: the first)
301
+ state patrol {
302
+ patrol round { # a state's nodes, ticked while in it
303
+ wait 1.6
304
+ speed Tuning.wardenPatrol
305
+ }
306
+ to alert { # to <state>: when every check holds ...
307
+ check sees
308
+ check Game.phase is 1
309
+ }
310
+ }
311
+ state alert {
312
+ enter { # instant actions on entering: set, stop, command (an `exit { }` block: on leaving)
313
+ command alert
314
+ }
315
+ chase {
316
+ speed Tuning.wardenChase
317
+ reach 1.6
318
+ }
319
+ to slam {
320
+ check targetDistance below Tuning.wardenReach
321
+ }
322
+ to search {
323
+ check sees is false
324
+ }
325
+ to patrol {
326
+ check Game.phase not 1
327
+ }
328
+ }
329
+ state slam {
330
+ enter {
331
+ command slam
332
+ }
333
+ stop
334
+ to alert {
335
+ after 1.8 # ... and the state has lasted this many seconds (default 0)
336
+ }
337
+ }
338
+ state search {
339
+ move_to lastSeen {
340
+ speed Tuning.wardenPatrol
341
+ }
342
+ to alert {
343
+ check sees
344
+ check Game.phase is 1
345
+ }
346
+ to patrol {
347
+ after 4
348
+ }
349
+ }
350
+ }
351
+ ```
352
+
353
+ ## 4. Perception
354
+
355
+ - `fov` 90–140 for guards (360 for things that sense all round), `sight`
356
+ a little past the screen's half width, `occlude: true` so walls hide.
357
+ Before an agent first moves its cone points the way its Transform faces.
358
+ - Hearing: spatial `og.AudioSource`s that start within `hearing × volume`
359
+ are heard, except the agent's own (its entity's and its children's: an
360
+ imp's chitter does not alert the imp); `og.ai.noise(world, { position,
361
+ loudness, source })` for silent events (a thrown rock, a door).
362
+ - `memory` is how long a lost target stays the target; `lastSeen` lets a
363
+ search state walk to where it was.
364
+
365
+ ## 5. Prove it
366
+
367
+ - `dump_ai:<entity>[:file]` writes the behaviour, state, running node, the
368
+ path from the root, status, blackboard and perception;
369
+ `dump_nav:<entity>[:file]` the path, the corner it heads for and status.
370
+ - A playtest per behaviour beat (`templates/archetypes/topdown/tests/ai.og.ts`
371
+ and `flee.og.ts`, `templates/game/tests/guard.og.ts`): place the hero with
372
+ a command, advance, dump, assert `node`, `state`, `sees` and positions.
373
+ `templates/archetypes/roguelike/tests/foes.og.ts` proves a turn-based
374
+ hunt (a tree that acts only on `Game.foesTurn`) and
375
+ `templates/archetypes/tower-defense/tests/maze.og.ts` a flow field
376
+ re-routed round placements and a placement refused because `findPath`
377
+ no longer reaches the goal.
378
+ `templates/archetypes/third-person` is the 3D action pattern: sentinels on
379
+ a cooked navmesh with behaviour machines (patrol, alert, grab) raising
380
+ the game's commands, restarted for a new run (`tests/sentinels.og.ts`,
381
+ `tests/restart.og.ts`).
382
+ Include the negative: a wall that hides the hero, a target outside the
383
+ cone.
384
+ - Determinism: a playtest with `runs: 2` over a chase must end on one hash.
385
+ - `og polish` reports damage moments without hit feedback and inputs nobody
386
+ answered (og-build §5); enemies that bite with no hit stop show up there.
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: og-audio
3
+ description: Sound on Origin Engine — one-shot voices for game events, the procedural sound presets og cook synthesises (coin, jump, hit, click and more), UI sounds, the four buses and their effects (filters, reverb send, compressor or limiter, ducking), music with stems, bar-aligned starts, crossfades and stingers on og.Music's clock, spatial sources with occlusion and distance dulling, voice priority and the cap, and proving it all headless with listen and og run --audio. Use when a game is silent, when adding any sound or music, or when the mix is muddy, clipping or wrong.
4
+ ---
5
+
6
+ Audio on Origin Engine is world state: an entity with `og.AudioSource`
7
+ plays while `playing` is true, the tick sends commands to the mixer, and
8
+ the same script renders the same samples on every host. The API is in
9
+ `reference/audio.md`; this skill is how much of each to use.
10
+
11
+ ## What works
12
+
13
+ Two games written from this package alone (`examples/lantern-vale`,
14
+ `examples/hollowbell`) put this skill to work: Lantern Vale's four-minute
15
+ track, cooked in 4-second chunks, streams from the title screen under the
16
+ effects' duck, Hollowbell's music streams the same way, flipbook frame
17
+ events put Hollowbell's footsteps, strikes and bell on their exact frames,
18
+ and `og polish` heard a sound on every damage moment. In The Kestrel Stair
19
+ (`examples/kestrel-stair`, a 3D third-person game) spatial sources (the
20
+ braziers, the imps' chitter, the crypt's drip, the bell) were heard from a
21
+ listener on the camera with distance, pan, distance dulling and occlusion
22
+ behind walls, each read back exactly through `og.AudioPlayback` with
23
+ `get:`; its 2:40 track streamed in 4-second
24
+ chunks on the music bus. Its crypt is an `og.ReverbZone` (a box with a
25
+ 1.5 m `fade`, the sfx bus sending 0.55 to a room of 0.92 at damping 0.25;
26
+ §4, §6): a playtest read the zone's `weight` 0 from the door and 1 inside,
27
+ while og.Audio and the bus kept the open air's room 0.5 and send 0.12.
28
+
29
+ ## 1. Every event sounds
30
+
31
+ A game with no sound feels broken, whatever it shows. Before anything else,
32
+ give each of these a sound: the player's actions (jump, attack, dash), hits
33
+ given and taken, pickups, a door, a death, a win and a loss, every UI press.
34
+
35
+ - One voice entity per sound, spawned in `start` with `playing: false`, and
36
+ a `Voices` system that plays each requested one: systems set bits of a
37
+ game resource (`Sounds.sfx |= Sound.Jump`), Voices sets `playing` on the
38
+ matching voice (a voice already playing is stopped this tick and
39
+ restarted the next). Every archetype in `templates/archetypes` does
40
+ this; copy a `Voices`.
41
+ - Vary repeated sounds: `pitch` 0.92–1.08 from the world's `og.Rng` for
42
+ footsteps, hits and coins, so a stream of them does not machine-gun.
43
+ - A hit's sound starts on the hit's tick, with its hit stop and flash
44
+ (og-feel §3): the three together sell the contact.
45
+
46
+ ## 2. Presets: sounds without files
47
+
48
+ `og sfx` lists the procedural presets (`coin`, `pickup`, `powerup`, `jump`,
49
+ `land`, `footstep`, `hit`, `hurt`, `explosion`, `laser`, `whoosh`, `door`,
50
+ `death`, `click`, `select`, `back`, `error`, `levelup`). `og sfx jump --out
51
+ assets/audio/jump.ogt` writes an asset `og cook` synthesises into a normal
52
+ sound; `og.load.sound(world, "audio/jump.ogt")` plays it like a WAV. Tune
53
+ `seed` (another take), `pitch`, `length` and `volume` in the file; `og sfx
54
+ jump --out jump.wav` renders it to listen to. `og sfx library <dir>` writes
55
+ all of them. Replace a preset with a recorded or generated sound when the
56
+ game's style needs it; presets are a floor, not a style.
57
+
58
+ ## 3. UI sounds
59
+
60
+ `og.ui.configure(world, { sounds: { hover, press, open, close, success, error } })`
61
+ (or the skin's `sound` lines) plays them on the `ui` bus: `click` for
62
+ press, `select` for success, `back` for close, `error` for a refused press
63
+ (and for `og.ui.shake({ sound: true })`, og-ui §6). Each starts in the
64
+ tick of its input. `hover` also answers keyboard and gamepad focus moving
65
+ to a control, and `press` a press by Enter, Space or the pad's A, so a
66
+ menu walked with a pad is heard step by step; keep `hover` short and quiet
67
+ (volume ≤ 0.3). `configure` changes only the fields it is given: setting
68
+ the sounds after a skin (or a look that brings one) keeps the skin and its
69
+ fonts, and `sounds: null` silences them again.
70
+
71
+ ## 4. Buses and the mix
72
+
73
+ Four buses: `master`, `music`, `sfx`, `ui`. Set levels with the sources'
74
+ `volume` and each bus's `og.AudioBusFx` (one entity per bus):
75
+
76
+ | Need | AudioBusFx |
77
+ |---|---|
78
+ | Music under effects | on music: `duck: true`, `duckBus` sfx, `duckDepth` 8–12 (dB), `duckRelease` 0.3–0.6 (s) |
79
+ | Dialogue or a stinger above everything | duck music and sfx by the ui or a dialogue bus |
80
+ | Muffled (underwater, pause menu) | `lowpass` 600–1200 Hz on sfx and music |
81
+ | Thin radio or phone | `highpass` 400 Hz plus `lowpass` 3 kHz |
82
+ | Space and room | `reverb` 0.1–0.3 on sfx (og.Audio `reverbRoom`, `reverbDamping`); keep music dry |
83
+ | A room that sounds like a room (a crypt, a cave, a hall) | an `og.ReverbZone` on an entity at the room's centre (§6), not code that eases the bus |
84
+ | Even out loud and quiet effects | compressor: `threshold` −18 (dB), `ratio` 3–4, `attack` 0.005 and `release` 0.15 (s), `makeup` 3 (dB) |
85
+ | Never clip | a limiter on master: `threshold` −1, `ratio` 20, `attack` 0.001 |
86
+
87
+ `og polish` reports clipped samples in the run's mix (og-build §5); a peak
88
+ at 1.0 is a mix bug, not loudness.
89
+
90
+ ## 5. Music
91
+
92
+ - Loop a track with `AudioSource.loop` (`loop: true`, with `loopStart` and
93
+ `loopEnd` for an intro that plays once) and `bus: "music"`.
94
+ - Stems: start every layer at the same `startAt` (world seconds) and bring
95
+ layers in and out with `volume` and `fade` (0.5–2 s), never by starting
96
+ them late: they stay sample-aligned.
97
+ - `og.Music` (`bpm`, `beatsPerBar`, `start`) is the clock: `nextBar` and
98
+ `nextBeat` are world times, so `startAt: music.nextBar` starts a stinger
99
+ or the next section on the bar line, sample-accurately, and `og.MusicBeat`
100
+ events drive beat-synced visuals.
101
+ - Bar-length stems started bar by bar: on the last beat of each bar
102
+ `og.MusicBeat` brings, start the next bar's stems at that event's `time`
103
+ plus a beat. og.audio runs on the world's ticks, which keep going at
104
+ time scale 0, so a pause stops the stems and a resume starts them again.
105
+ `templates/archetypes/rhythm` (its `Band` system, and
106
+ `tests/band.og.ts`, which asserts every `startAt`) does this.
107
+ - Part-way starts: `startOffset` (seconds into the clip) with `startAt`
108
+ starts a source that far in, to the sample, so a resume carries on from
109
+ the beat it paused on: each stem sounding then starts at the count-in's
110
+ end with `startOffset` how far into it that beat is (the rhythm's
111
+ `tests/pause.og.ts` asserts both).
112
+ - What the player hears lags the mix by the device's output latency:
113
+ `og.audio.outputLatency(world)` (seconds; the Web Audio context's
114
+ `outputLatency` or `baseLatency` plus the engine's buffer on the web, the
115
+ device's callback-to-playback time natively, 0 headless; the system
116
+ declares `og.Audio` among its reads). A rhythm game judges a press at
117
+ its tick's time plus its event's `at` (og-game, input) less this and a
118
+ calibrated offset for the rest of the chain (the rhythm's `Play` system,
119
+ `tests/timing.og.ts`).
120
+ - A crossfade: the new track `startAt: nextBar` with `fade: 2`, the old one
121
+ `playing: false` with the same `fade`.
122
+ - Long tracks stream in chunks: a sound whose cooked file passes 1 MiB
123
+ (about 27 s of 48 kHz stereo) is cooked in chunks of whole frames, and
124
+ the engine keeps only from half a second behind each playhead to four
125
+ seconds ahead (on the web each chunk is fetched as it nears). Set it in
126
+ the track's import settings:
127
+
128
+ ```text
129
+ # assets/music/theme.wav.ogt
130
+ type import
131
+ version 1
132
+ ---
133
+ sound {
134
+ stream true # true, or false to keep a long track whole
135
+ chunk 4 # seconds per chunk, 1 to 60
136
+ }
137
+ ```
138
+
139
+ Nothing changes in code: loops, `startOffset`, crossfades and stems play
140
+ sample-identical to the whole file, and the world hash is the same.
141
+ `og.load.soundInfo(world, sound)` answers `chunks`, how many (0: whole).
142
+ A chunk that has not arrived plays silence, so start a long track a beat
143
+ before it must be heard rather than on the first frame of a scene.
144
+ What a page downloads: a chunked sound in the first group costs the boot
145
+ its first chunk only (its other chunks are fetched ahead of the
146
+ playheads, none before it plays). In a streaming group (og-game §6) it
147
+ costs the boot nothing: its first chunk comes in the group's pack, it
148
+ plays silence of its own length until the group is resident, and only
149
+ then are its other chunks read ahead of the playheads, each a file of
150
+ its own, never in the pack; when the group unloads it is silent again
151
+ and nothing more is read. Put a level's or a region's music in that
152
+ level's group; keep the title's music in the first group, since a group
153
+ wanted at boot loads right after it anyway.
154
+
155
+ ## 6. Space, occlusion, priority
156
+
157
+ - `spatial: true` sources pan and attenuate by their world transform
158
+ against the `og.AudioListener` (on the camera or the player);
159
+ `refDistance` 1–3, `maxDistance` 20–50. A parented source follows its
160
+ parent: put the footstep or the engine hum on a child entity
161
+ (`SpawnDesc.parent`) and it is heard where the parent carries it; a
162
+ listener on a child of the camera rig hears from the rig.
163
+ - `distanceCutoff` (e.g. 2000 Hz) dulls far sounds; `occlude: true` plays a
164
+ source behind a collider at og.Audio's `occlusionGain` through
165
+ `occlusionCutoff`: enemies behind walls sound behind walls.
166
+ - A room of its own: `og.ReverbZone` (a box `size` along the entity's
167
+ axes, or `radius`) eases the shared reverb towards its `room`,
168
+ `damping` and its `bus`'s `send` while the listener is inside, fading
169
+ over `fade` metres past the walls (2–4 m reads as walking through a
170
+ door). A stone crypt: `room` 0.9, `damping` 0.25, `send` 0.5 on sfx;
171
+ a small wooden room: `room` 0.4, `damping` 0.6. og.Audio and the
172
+ bus's og.AudioBusFx stay the open air's; read `weight` to know where
173
+ the listener is. It follows the listener's world transform, so put the
174
+ og.AudioListener where the ears are (the camera, or the player); a zone
175
+ on a child of a moving room moves with it.
176
+ - `og.Audio.maxVoices` (32) caps voices; past it the lowest `priority`,
177
+ then the quietest, then the oldest is culled (`SOUND_CULLED`). Give the
178
+ player's own sounds and the music a high priority (200+), ambient props a
179
+ low one.
180
+
181
+ ## 7. Prove it
182
+
183
+ - `listen[:file][:seconds]` in a command script writes the last seconds of
184
+ the mix as a WAV; a playtest reads it with `run.wav(file)` (`peak`,
185
+ `rms`): silence where it should be silent, sound after the event.
186
+ - `og run --audio out.wav --json` captures the whole run and prints its
187
+ SHA-256: two runs of the same script give the same hash.
188
+ - `dump_entity:<voice>` shows `og.AudioPlayback` while it plays; a counter
189
+ on the voice (`Sfx.plays`) proves a short sound played at all.
190
+ - `og polish` scores inputs answered by a sound, UI presses with a ui-bus
191
+ sound and hits with a sound (og-build §5).