@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.
- package/bin/og.mjs +25 -0
- package/dist/web/origin-engine.d.ts +1233 -0
- package/dist/web/origin-engine.js +8628 -0
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
- package/dist/web-publish/origin-engine-lanes.js +8509 -0
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +1176 -0
- package/dist/web-publish/origin-engine.js +8313 -0
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
- package/dist/web-publish-2d/origin-engine.js +8313 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
- package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
- package/knowledge/README.md +31 -0
- package/knowledge/knowledge.json +13 -0
- package/knowledge/looks/catalogue.json +1871 -0
- package/knowledge/package.json +33 -0
- package/knowledge/rules/platform.json +329 -0
- package/knowledge/scaffold/README.md +27 -0
- package/knowledge/scaffold/distribution.json +48 -0
- package/knowledge/scaffold/origin-engine.json +19 -0
- package/knowledge/scaffold/template/.origin/setup +14 -0
- package/knowledge/scaffold/template/LICENSES.md +23 -0
- package/knowledge/scaffold/template/README.md +37 -0
- package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
- package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
- package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
- package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
- package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
- package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
- package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
- package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
- package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
- package/knowledge/scaffold/template/gitattributes +22 -0
- package/knowledge/scaffold/template/package.json +18 -0
- package/knowledge/scaffold/template/project.ogt +25 -0
- package/knowledge/scaffold/template/src/main.ts +246 -0
- package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
- package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
- package/knowledge/scaffold/template/tsconfig.json +17 -0
- package/knowledge/scripts/lib.ts +203 -0
- package/knowledge/scripts/mount.ts +210 -0
- package/knowledge/scripts/pack.ts +24 -0
- package/knowledge/scripts/scaffold.ts +72 -0
- package/knowledge/scripts/validate.ts +749 -0
- package/knowledge/skills/og-ai/SKILL.md +386 -0
- package/knowledge/skills/og-audio/SKILL.md +191 -0
- package/knowledge/skills/og-build/SKILL.md +330 -0
- package/knowledge/skills/og-core/SKILL.md +285 -0
- package/knowledge/skills/og-feel/SKILL.md +385 -0
- package/knowledge/skills/og-game/SKILL.md +1479 -0
- package/knowledge/skills/og-i18n/SKILL.md +220 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
- package/knowledge/skills/og-taste/SKILL.md +537 -0
- package/knowledge/skills/og-ui/SKILL.md +444 -0
- package/knowledge/wiki/ai.md +627 -0
- package/knowledge/wiki/anim.md +1187 -0
- package/knowledge/wiki/atmosphere.md +636 -0
- package/knowledge/wiki/audio.md +732 -0
- package/knowledge/wiki/camera.md +711 -0
- package/knowledge/wiki/commands.md +357 -0
- package/knowledge/wiki/game.md +220 -0
- package/knowledge/wiki/i18n.md +213 -0
- package/knowledge/wiki/index.md +1027 -0
- package/knowledge/wiki/input.md +1714 -0
- package/knowledge/wiki/load.md +1280 -0
- package/knowledge/wiki/math.md +166 -0
- package/knowledge/wiki/nav.md +1154 -0
- package/knowledge/wiki/physics.md +3142 -0
- package/knowledge/wiki/platform.md +857 -0
- package/knowledge/wiki/probe.md +1692 -0
- package/knowledge/wiki/reflect.md +285 -0
- package/knowledge/wiki/render.md +3511 -0
- package/knowledge/wiki/rng.md +393 -0
- package/knowledge/wiki/stream.md +376 -0
- package/knowledge/wiki/system.md +197 -0
- package/knowledge/wiki/systems.md +43 -0
- package/knowledge/wiki/terrain.md +727 -0
- package/knowledge/wiki/tilemap.md +422 -0
- package/knowledge/wiki/time.md +150 -0
- package/knowledge/wiki/transform.md +230 -0
- package/knowledge/wiki/tween.md +580 -0
- package/knowledge/wiki/ui.md +8000 -0
- package/knowledge/wiki/world.md +909 -0
- package/package.json +68 -4
- package/schema/api.schema.json +29675 -0
- package/shell/README.md +22 -0
- package/shell/brand/README.md +23 -0
- package/shell/brand/icon-192.png +0 -0
- package/shell/brand/icon-512.png +0 -0
- package/shell/brand/mark-on-paper.svg +6 -0
- package/shell/brand/origin-mark.svg +5 -0
- package/shell/brand/origin-motion.css +251 -0
- package/shell/brand/tokens.css +20 -0
- package/shell/brand.ts +132 -0
- package/shell/page.ts +715 -0
- package/shell/platform.ts +203 -0
- package/shell/player.ts +238 -0
- package/shell/shell.css +185 -0
- package/shell/sw.ts +177 -0
- package/shell/transport.ts +515 -0
- package/shell/wire.ts +369 -0
- package/src/e6.ts +69 -0
- package/src/env.d.ts +100 -0
- package/src/generated/abi.ts +980 -0
- package/src/index.ts +193 -0
- package/src/node/audio.ts +86 -0
- package/src/node/build.ts +109 -0
- package/src/node/device.ts +981 -0
- package/src/node/doctor.ts +60 -0
- package/src/node/index.ts +102 -0
- package/src/node/platform.ts +24 -0
- package/src/node/playtest-run.ts +249 -0
- package/src/node/png.ts +130 -0
- package/src/node/polish.ts +976 -0
- package/src/node/run.ts +373 -0
- package/src/node/web-build.ts +808 -0
- package/src/p2.ts +35 -0
- package/src/platform-local.ts +211 -0
- package/src/playtest.ts +131 -0
- package/src/runtime.ts +1509 -0
- package/src/systems/cameras.ts +247 -0
- package/src/ui-kit/bind.ts +141 -0
- package/src/ui-kit/huds.ts +303 -0
- package/src/ui-kit/index.ts +36 -0
- package/src/ui-kit/menus.ts +187 -0
- package/src/ui-kit/settings.ts +131 -0
- package/src/ui-kit/store.ts +222 -0
- package/src/ui-kit/story.ts +89 -0
- package/src/ui-kit/util.ts +106 -0
- package/src/web/a11y.ts +429 -0
- package/src/web/audio-worklet.ts +193 -0
- package/src/web/audio.ts +307 -0
- package/src/web/basis.ts +239 -0
- package/src/web/gamepad.ts +123 -0
- package/src/web/host.ts +166 -0
- package/src/web/input.ts +237 -0
- package/src/web/lanes.ts +21 -0
- package/src/web/play.ts +20 -0
- package/src/web/rpc-ring.ts +110 -0
- package/src/web/settle.ts +122 -0
- package/src/web/shell.ts +643 -0
- package/src/web/text-input.ts +248 -0
- package/src/web/worker.ts +1002 -0
- package/types/bindings.json +13654 -0
- package/types/index.d.ts +20090 -0
- package/vendor/basis/LICENSE +201 -0
- package/vendor/basis/NOTICE +15 -0
- package/vendor/basis/README.md +18 -0
- package/vendor/basis/basis_transcoder.js +19 -0
- package/vendor/basis/basis_transcoder.wasm +0 -0
- 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).
|