@volter/play 0.5.211 → 0.5.213

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/NOTICE CHANGED
@@ -4,9 +4,9 @@ This package is developed in the public volter-ai/editor repository and is
4
4
  Volter AI, Inc.'s own work. It was first published under AGPL-3.0-only and is
5
5
  licensed under Apache-2.0 from this notice on. The owner's words are "the
6
6
  runtime should be apache, the editor is agpl"; that this package is runtime
7
- (the play-script API a game's gameplay is written against) is the reading
8
- recorded as ruling D336 on board task t_b315eb39 and in COMMERCIAL.md at the
9
- repository root. LICENSE contains the Apache-2.0 text.
7
+ is the reading recorded as ruling D336 on board task t_b315eb39 and in
8
+ COMMERCIAL.md at the repository root. LICENSE contains the Apache-2.0 text.
10
9
 
11
- A play script is written against `src/play-script.ts`. The package imports
12
- `@volter/sdk` (Apache-2.0), three and React, and nothing of Blender.
10
+ The package imports `@volter/sdk` (Apache-2.0), three and React, and nothing of Blender. A game it plays is a
11
+ volter-ai/std game (Apache-2.0; the Blender scripts std ships are GPL-3.0-or-later), whose packages are the game's own
12
+ dependencies: this package runs the game's own commands and pages and carries none of std's code.
package/README.md CHANGED
@@ -1,185 +1,169 @@
1
1
  # Play
2
2
 
3
- Play scripts on detached model documents. Play (the Game panel's, in Cyclotron) runs
4
- `src/models/<name>.play.ts` beside `src/models/<name>.blend` on a detached copy
5
- of the model; the model, its selection and its history stand as they were, and
6
- Stop returns to them. The script's default export is called once with the play
7
- context (`ModelPlayContext`, `src/play-script.ts`) and answers the game:
8
-
9
- ```ts
10
- import type { ModelPlayContext, ModelPlayGame } from '@volter/play/play-script';
11
-
12
- export default (play: ModelPlayContext): ModelPlayGame => {
13
- const player = play.find('Player')!;
14
- let checkpoint = false;
15
- return {
16
- update(dt) {
17
- player.position.y += dt;
18
- if (!checkpoint && player.position.y > 10) {
19
- checkpoint = true;
20
- play.tint('Checkpoint.Pad', '#2bff6b');
21
- play.log('checkpoint', { stage: 2, at: player.position });
22
- }
23
- },
24
- };
25
- };
26
- ```
3
+ A Cyclotron game is a [volter-ai/std](https://github.com/volter-ai/std) game: three.js, React Three Fiber, Rapier and
4
+ Colyseus, used as their own docs describe them, with std adding only what they lack. This repository pins std as
5
+ `external/std`. Play is the project's, as an engine's Play button is, whatever document is in front: it runs the game
6
+ itself, on its own dev server, each player one of the game's pages, a std device, in the Game view.
7
+
8
+ ## A std game
9
+
10
+ A project with `std.json` that depends on `@volter-ai/std-dev` is a std game (`src/std-game.ts` asks the session,
11
+ `serving/std-dev.ts`). `cyclotron create --template game` makes one with std's own `std new`; `cyclotron add-play`
12
+ runs `std new` into an existing project and moves its `src/models/*.blend` into `levels/`. Either installs the game's
13
+ dependencies with pnpm, as std's README does; the editor hears `std.json` arrive and needs no reload.
14
+
15
+ The project opens on its main level (`levels/MainLevel.tsx`, else the level last worked on), a document that stays
16
+ the level as authored. Play, in the Game panel under every document, saves the edit Blender holds first
17
+ (editor-blender's `modelDocument.flush`) and waits for std's export of it (`src/std-level.ts`), so the game plays the
18
+ current edit (`src/game-controls.ts`). It then opens the Game view (`src/game-view.tsx`) as a tab of the editor,
19
+ in front, starts the game's dev server where none runs (`npm run dev -- --no-open`, owned by the session and ended with
20
+ it), finds it by the record std writes in Vite's cacheDir (`node_modules/.vite/std-dev.json`, whose token stays in the
21
+ session), and stands each player in the Game view as an iframe. Another tab in front never stops the game; Stop, or the
22
+ Game view closed, does, and brings back the tab that was in front. The players' pages draw in the Game view's
23
+ retained layer (`@volter/sdk/document-layer`), mounted in the workbench's container and laid over the Game view's box
24
+ while that box is shown (hidden, and its pages throttled, while it is not), as Code-OSS keeps a webview that retains
25
+ its context: a tab's box is parked out of the page while it is behind another, and a frame moved with it would load
26
+ again.
27
+
28
+ - **The view bar**, at the top of the Game view: Scene, Player N (each "you" or "autopilot"), and Grid (Scene and every
29
+ player tiled, each player's tile its whole view scaled down, as its page lays out at the view's size in every view;
30
+ a tile clicked opens it).
31
+ - **Scene** is the current level (the level document in front last) live: the same level view, drawn again in the Game
32
+ view and put each frame on the game's running scene, read where it runs (std-dev's `GET /__std/scene`, through
33
+ `GET /__editor/play/std/scene`: the world the game's server runs, else the scene the viewed player's page renders, a
34
+ game of one player's among them) by std's node put (`contributions/std-level.document.tsx`'s `useRunningScene`,
35
+ `serving/std-scene-runtime.js`'s `SceneNodes`); an object the copy lacks (a character) stands as an empty of its name.
36
+ The level's own document stays the level as authored, beside it.
37
+ - **Players [-][+]**, in the Game panel beside Play: how many play, the project's setting `volter.play.players` (one
38
+ where it is unset), which it writes to the project's settings; a running game's players join or leave to match
39
+ whenever the setting changes.
40
+ - **The keys** 1 to 4 open Player N and Tab the next view, while the Game view is in front and the editor holds the
41
+ keyboard: in Scene, in Grid, or after a press on the view bar. A viewed player's page holds the keyboard, and its
42
+ keys, 1 to 4 included, are the game's, as Unity's Multiplayer Play Mode and Unreal's Play In Editor treat a focused
43
+ game window.
44
+ - **Each player's page** is told apart by the slot its address names (`?slot=<run>-<n>`), which its trace's first
45
+ line says; a page that loads again is found again by it. A player whose page never joins is taken out, and its
46
+ failure stands.
47
+ - Play, Stop and Restart load, unload and reload the pages.
48
+
49
+ ## std's device controls
50
+
51
+ Every word Play says to a player's page is one of std's device controls (`POST /__std/devices/<id>/control`), sent
52
+ through `src/std-controls.ts` and the session's route, which holds the dev server's token:
53
+
54
+ | Control | What Play uses it for |
55
+ | --- | --- |
56
+ | `clock` (`pause`, `resume`, `step`, `speed`) | The Game panel's pause, step and speed, sent to every joined player's page; the panel shows the clock std answers |
57
+ | `autoplay`, `manual` | Who drives the viewed player: its bot, with its slot and goal, or the person |
58
+ | `inspect` | `editor.inspect` while a player is in view: the page's live three.js objects and labelled screen nodes |
59
+ | `act` | `editor.document.key` with no selector, and `editor.document.click('role=<role>[name="<name>"]')` (the page's labelled node pressed at its middle), while a player is in view: std-driver/3 actions through the page's own input |
60
+ | `capture` | `capture --region play`: the viewed page's next frame, written by std's receiver |
27
61
 
28
- ## The play log
62
+ `editor.document.query` answers the editor's DOM as always and, beside it, the viewed page's labelled nodes
63
+ (`framed`; `@volter/sdk/framed-document-driver`). A refusal is std's own answer, shown with its words.
29
64
 
30
- `play.log(kind, facts?)` writes one entry, stamped with `simT` (seconds of
31
- simulation since Play started, the sum of the `dt`s given to `update`) and
32
- `tick` (the update in progress). Facts are snapshotted as JSON when logged. `simT` and `tick` are the Game panel's
33
- clock: they stand still while paused and run at the speed (see Time below). The
34
- runner adds `play-start`, `play-restart` (`{ speed }`, right after a Restart's
35
- `play-start`), `script-reload` (`{ reason: 'saved' | 'dependency-deleted', path }`),
36
- `script-error` (`{ phase, message }`), `tint-unsupported` (`{ object, material, why }`),
37
- `tint-unknown-object` (`{ object, call }`, once per name: `tint` or `setOpacity` was given a
38
- name the model has none of, and did nothing), `autoplay-on` / `autoplay-off`
39
- (`{ by: 'panel' | 'cli' | 'takeover' | 'script' }`, see Autoplay below),
40
- `autoplay-unavailable` (`{ why }`, when a script's first update has run and it registered no
41
- bot; once per run, and again only if a bot came and went),
42
- `pause`, `resume`, `step` (`{ dt }`, one per stepped update), `speed`
43
- (`{ speed, from }` on a change; `{ speed }` at a start that is not 1×) and
44
- `play-stop` (`{ reason }`). Log transitions rather than every frame. Logging
45
- never throws and never changes timing or state; a script's `log`, `tint` and
46
- `setOpacity` do nothing once it has been replaced or Play has stopped.
47
-
48
- Each model document keeps its own log; a read takes the active Play's unless it
49
- names a document. The newest 5000 entries are kept (older ones are counted as
50
- `dropped`), each entry's facts up to 2048 characters of JSON. Play from a fresh
51
- copy, and Restart, empty the document's log; a script reload does not, and the log stays readable after Stop until the
52
- next Play. `console.log` does not reach the session's console feed (only
53
- warnings and errors do); the play log does.
65
+ The clock holds the pages. A room on the dev server runs on: Colyseus has no pause for a room, so a timer a room runs
66
+ keeps running while the pages are paused.
54
67
 
55
- ```sh
56
- cyclotron play-log # every kept entry, one per line
57
- cyclotron play-log --kind death # one kind
58
- cyclotron play-log --since 12.5 --json # entries at or after simT 12.5, as JSON
59
- cyclotron play-log --document <id> # another model document's log
60
- cyclotron eval "(await editor.modelPlayLog({ kind: 'death' })).entries"
61
- ```
68
+ ## Autoplay
62
69
 
63
- ## Recolouring the copy
64
-
65
- `play.tint(object, color)` draws an object (a name, or what `find` answered)
66
- and every mesh under it in `color`; an emitting surface glows in it, and an
67
- image texture is multiplied by it. `play.setOpacity(object, opacity)` fades it.
68
- `null` returns the authored look. Other objects wearing the same Blender
69
- material are not affected.
70
-
71
- Use these instead of editing materials directly: a presented mesh's `material`
72
- is an array with one `MeshPhysicalMaterial` per Blender material slot, shared by
73
- every object with that material; the presenter re-assigns those slots whenever
74
- it re-applies shading; `clone()` drops its shader hooks; and a node graph that
75
- drives Base Color or Alpha ignores `color` and `opacity`. A tinted or faded
76
- object wears copies of its own slots that draw the material's constant inputs
77
- (so a node graph's other inputs are not drawn while it does), and keeps them
78
- across a script reload. An object with a material the document cannot copy
79
- (one the script made itself) is left as it is, and the log says so once with
80
- `tint-unsupported`.
70
+ The game's bot is its own program, `autopilot/index.ts`, whose goals are named by words (std's `autopilot({ goals })`).
71
+ Play lists them from the session's route `/__editor/play/std/goals`, which loads that program in Node as std does.
72
+ Autoplay, the Game panel's and `cyclotron play autoplay on [<goal>]` alike, is std's own play of a goal
73
+ (`/__std/play`), recorded and checked, on the device std's play takes where one is linked: the viewed player's page
74
+ while the Game view is on screen, which std drives as it is and hands back to the person as the goal ends, and std's
75
+ headless device otherwise (the editor not seen, or the Game view behind another tab; an agent's, or an arm's, also
76
+ while no player is viewed, where the panel's waits for one). The person's
77
+ own key or click in the page takes it over, which ends the goal there, failed, as Autoplay off does; armed while
78
+ stopped, Autoplay plays as the run starts. One plays at a time. `play state`'s `autoplay.state` (with how the goal's
79
+ checks went, as std said it) and the play log's `autoplay-end` say how it ended; `autoplay.report` is its report.
80
+ Every other player is the game's bot from the start, playing the run's goal.
81
81
 
82
- ## Autoplay
82
+ ## The play log
83
83
 
84
- A game offers its bot; the editor decides whether it drives.
84
+ Each player's page traces its run (std's trace, `build/runs/<run>/<page>.jsonl`), and the run reads each trace into
85
+ the play log, each entry tagged with its player's slot (`source: 'script'`). Play adds its own entries
86
+ (`source: 'play'`): `play-start`, `running`, `player-joined`, `player-page-loaded`, `player-left`, `view`,
87
+ `autoplay-on` and `autoplay-off` (with `by`), `pause`, `resume`, `step` and `speed` (each with the clock std answered),
88
+ `capture`, `script-error` (with its `phase`) and `play-stop`. A page's entry carries its trace's step as `tick`
89
+ (std's trace format 3, `s`: sixty a game second from the trace's start, since std's live clock is the page's
90
+ `performance.now`) and that step in seconds as `simT`; Play's own carry the run's elapsed seconds and the newest step
91
+ heard. A page writes a trace line only as something happens on it (an input, a word said, its bot's intent; a clock
92
+ control writes none), so its step is when it last traced, not its clock now. `play state`'s clock says which reading it
93
+ gives (`from`): while the game is held, `std-answer`, std's exact answer to the `pause` or `step`, the same clock its
94
+ play-log entry records; while it runs, `newest-traced-step`, the newest step the viewed player's page traced, which
95
+ holds while that page is idle. After Stop it keeps the run's last reading and its `from`, with `running` false, so
96
+ `play state` still says how far the run got (the panel says only that it stopped).
97
+
98
+ What a room says is not in any page's trace: the room runs in the dev server's process, so its own `console.log` is on
99
+ that server's stdout, with Vite's notices, and the exceptions Colyseus logs on its stderr, with Vite's errors and
100
+ warnings. Of a dev server Play started, each run reads both streams from the run's start (`/__editor/play/std/output`),
101
+ each kept apart: each stdout line is a `server-output` entry; stderr is read in blocks, a line and the indented lines
102
+ under it, and a block that names an `Error` or carries a stack is a `script-error` with the phase `server` and an
103
+ error in the console, once by its first line, any other a `server-output` entry.
104
+
105
+ One log, the latest run's: each Play replaces it, and it outlives Stop. The newest entries are
106
+ kept in a bounded ring (older ones counted as `dropped`). `console.log` does not reach the session's console feed;
107
+ the play log does.
85
108
 
86
- ```ts
87
- play.autoplay(({ dt, simT, tick, keys }) => {
88
- // the game's own decision, from its own state
89
- return car.speed < 20 ? ['ArrowUp'] : [];
90
- });
109
+ ```sh
110
+ cyclotron play-log # every kept entry, one per line
111
+ cyclotron play-log --kind coin # one kind
112
+ cyclotron play-log --since 12.5 --json # entries at or after 12.5 s, as JSON
91
113
  ```
92
114
 
93
- The controller is a plain function. While autoplay is on it is called before each `update`
94
- with that update's `dt`, `simT` and `tick` (the stamps that update's log entries carry) and
95
- `keys`, the keys the person holds; it returns the `KeyboardEvent.code` keys the bot holds for
96
- that update (any iterable, or nothing). The runner merges them into `play.keys`, so the bot
97
- drives through the script's own input code exactly as a person does. One bot per script:
98
- registering again replaces it, `play.autoplay(null)` withdraws it, and it goes with the script
99
- on a reload or Stop. The bot is called only inside the editor's Play runner; a game run
100
- anywhere else never drives itself.
101
-
102
- Whether it drives is the editor's:
103
-
104
- - Autoplay is **off** whenever Play starts or restarts.
105
- - Only the Game panel's **Autoplay** toggle, `cyclotron play autoplay on|off` or
106
- `await editor.command('volter.model-play.autoplay', { on: true })` turn it on (or off).
107
- - **A bot exists only while the game runs**: `play.autoplay` is called by the play script, so
108
- a stopped game (or one still starting) has none, and neither has a running script that never
109
- registers one. The panel says which, in words beside the toggle — "Available once the game is
110
- running" or "No autoplay — this game doesn't provide a bot" — and `on` is refused with the
111
- same reason, which `play state` also gives as `autoplay.why`.
112
- - **Arming.** Pressed while stopped, the panel's Autoplay arms the next start: autoplay turns
113
- on (`by: 'panel'`) as soon as that run's script has run its first update with a bot, and the
114
- arm is dropped if it offers none. A person's key or click in the game before then drops it
115
- too (`autoplay-off` with `{ by: 'takeover', armed: true }`): the person always wins. Only an
116
- explicit arm carries over; Stop, and closing the model, drop it.
117
- - **The person always wins.** A new key press the game would hear, or a pointer pressed in the
118
- game's area (the HUD included), turns autoplay off before that key reaches `play.keys`, and
119
- the panel reads "You're driving" until someone switches the bot on again. Synthetic keys and
120
- clicks (`editor.document.key`, `click`) count as a person's: they are how an agent plays by
121
- hand. A key repeat is not a new press; keys typed in a text field, or while the game's surface
122
- does not hold the keyboard, are not the game's and do not take over.
123
- - A bot that throws turns autoplay off (`by: 'script'`, with a `script-error` of phase
124
- `autoplay`), as does a reload whose script offers no bot.
125
-
126
- Each change is a play-log entry, `autoplay-on` or `autoplay-off`, with `by`.
127
-
128
- ## Time
129
-
130
- `update(dt)` is the only clock a play script is given, and the editor decides it:
131
-
132
- | Control | What the script sees |
133
- | --- | --- |
134
- | Playing at 1× | One `update` per drawn frame, `dt` = the frame's seconds (at most 0.1). |
135
- | Speed 0.25× – 4× | `dt` scaled by the speed. A scaled frame longer than 0.1 s is split into equal updates, so no single `dt` exceeds 0.1. |
136
- | Pause | No `update` at all. The copy, the camera and the HUD hold the last frame. |
137
- | Step (while paused) | One `update` with `dt` = 1/60. |
138
- | Restart | The run begins again on a fresh detached copy, the clock at zero, without the camera's fly-in. |
139
-
140
- A script that reads the page's own clock (`performance.now()`, `Date.now()`) instead of
141
- summing `dt` is outside the reach of pause and speed.
142
-
143
- The run's clock — simulation time (the sum of the `dt`s handed to the script) and tick (the
144
- number of updates) — is kept per document beside whether it plays (`src/model-play.ts`).
145
- Speed is kept for the page's life; pause and the clock reset on every Play.
146
- When the run plays but no game runs — the script failed to start, or threw — the clock's
147
- `failure` says why (the Game panel shows it in place of "Playing"); a save of the script
148
- retries, and the next running game clears it.
149
-
150
115
  ## Controls
151
116
 
152
- The tool registers the `model` document Play extension
153
- (`@volter/sdk/kit/document-play-extension`). Beside Play and Stop it offers:
154
-
155
- - `transport` — `setPaused`, `step`, `setSpeed`, `restart`, the `clock` and its own
156
- subscription, the offered `speeds`, and the restart `generation` the document keys its
157
- detached copy on.
158
- - `transport.autoplay(documentId)` and `transport.setAutoplay(documentId, on, by)` — the bot's
159
- switch: `{ on, available, by, armed }`, announced through `subscribeClock`;
160
- `transport.armAutoplay(documentId, armed)` arms it, while stopped, for the next start.
161
- - `log` — `tail(documentId, last, kind?)` (the newest entries and every kind the run wrote) and
162
- `subscribe`, which the Game panel draws live.
163
- - `scriptPath(sourcePath)` and `hasScript(sourcePath)` — where a model's play script goes and
164
- whether it exists, so a layout can open a model with a script as a game.
165
-
166
- In Cyclotron these are drawn by the Game panel (`@volter/editor-blender`): the Game /
167
- Animation switch at the left of the bottom area's header puts it there in place of the Timeline. The panel's controls
168
- are also commands, `volter.model-play.<verb>`, so an agent drives the same run the person
169
- sees:
170
-
171
- | Command | Arguments |
172
- | --- | --- |
173
- | `volter.model-play.state` | — |
174
- | `volter.model-play.play` / `stop` | — (`play` also switches the document to Game mode) |
175
- | `volter.model-play.pause` / `resume` | — |
176
- | `volter.model-play.step` | `{ count?: 1–600 }`, while paused |
177
- | `volter.model-play.speed` | `{ speed: 0.25 \| 0.5 \| 1 \| 2 \| 4 }` |
178
- | `volter.model-play.restart` | — |
179
- | `volter.model-play.mode` | `{ mode?: 'game' \| 'animation' }` |
180
- | `volter.model-play.autoplay` | `{ on: boolean }` — the game's bot drives, or the person does |
181
-
182
- Each takes an optional `document` (the model document's id) and otherwise acts on the model
183
- document on screen; each answers with the panel's state. From the shell:
184
- `cyclotron play pause`; under `eval`:
185
- `await editor.command('volter.model-play.speed', { speed: 0.5 })`.
117
+ play's page half (`contributions/play.page.tsx`) registers its Game panel through editor-blender's API
118
+ (`registerPlay`), whose bottom area shows it under every document, and below a model in its Game mode (a model's
119
+ Animation mode is Blender's animation editors). The panel's controls are also commands (`volter.play.<verb>`), so an agent drives the same run the person sees: `cyclotron play play|stop|pause|resume|restart`,
120
+ `play step [count]`, `play speed <0.25|0.5|1|2|4>`, `play mode [game|animation]` and `play autoplay on [<goal>]|off`,
121
+ each answering with the panel's state.
122
+
123
+ A std game's Export (the panel's button and menu; `volter.play.export { target, add? }` under `eval`) builds the
124
+ game for the web (`std build`, into `dist/`) or exports it to an engine (`std export --target <engine>`). An engine not
125
+ yet one of the game's targets is offered as "Add Roblox and export", which adds it first (`std target add <engine>`: its
126
+ package installed, its `std.json` settings and its scripts); the verb refuses one unless `add` is true. A game is built
127
+ for an engine only once someone asks for it, as an engine's editor builds for a platform once it is chosen. Each line
128
+ std prints goes to the console, and the panel says how the export stands and where it is.
129
+
130
+ ## A std game's editor
131
+
132
+ A std game carries no editor code. Play supplies its adapter (`src/std-game-adapter.ts`, declared in
133
+ `contributes.volter.projectAdapters` for a project with `std.json`): Blender's layout, keymap and properties inspector,
134
+ with every `.blend` under `levels/` and `prefabs/`, and each level, as a document, the main level the one it opens
135
+ on. A project's own `editor/volter.adapter.ts` wins over it.
136
+
137
+ ## A level's export
138
+
139
+ Each save of a level's `.blend` (`levels/**/*.blend`) runs std's level export, its two halves as std states them. The
140
+ Blender half, std-dev's `blender/blend-to-scene.py`, is the save's own script (editor-blender's
141
+ `modelDocument.setSaveScript`): it runs in the save's turn, on the very scene that save wrote, and the save's answer
142
+ carries the files it wrote (`levels/<name>.glb`, its description, a copied sky image, a baked `<name>.view.cube`).
143
+ Then, one save after another, those files are carried into the project and the Node half,
144
+ `std level-scene <levels/name.glb>`, runs in the game's folder at the session route
145
+ `/__editor/play/std/level`, by the game's own std, and writes `<Name>Level.tsx`, `<name>.canvas.ts`, the environment
146
+ and grading. std's dev server takes the level from there, its rooms reloaded by Colyseus's own dev mode. Both halves'
147
+ output is the console's (`std-level`); a half that fails is an error there.
148
+
149
+ The game's `prefabs/` folder is a Blender asset library named Prefabs whose import method is Link
150
+ (editor-blender's `setAssetLibrary`), so a prefab dragged from it is linked, not appended.
151
+
152
+ ## A level's document
153
+
154
+ A level is the document an engine opens. Each level component, `levels/<Name>Level.tsx`, is a document of its own
155
+ (`std-level`, listed by `contributions/levels.finder.ts`), named as its component is less `Level` (`MainLevel` is Main)
156
+ and drawn as the game draws it, by its own component and the game's own React Three Fiber, react-three-rapier and std,
157
+ in editor-threejs's viewport (`contributions/std-level.document.tsx`; its module, `/@volter-std-scene/<source>`, is
158
+ `serving/std-scene.ts`). Its
159
+ source is its own code, or the `levels/<name>.blend` std's export writes it from (a Blender level's environment, canvas
160
+ and grading drawn with it), which stays the Blender document it is authored in. Overlays draws colliders and sensors
161
+ and signs each std marker with its name. The view edits nothing, and draws the level again as anything it depends on
162
+ changes (`docs/adr/std-scene-view.md`). It is the level as authored, while the game runs too; the Game view's Scene is
163
+ the same view, live.
164
+
165
+ ## Outside the editor
166
+
167
+ `cyclotron export web|roblox|unreal` runs the game's own `std build` or `std export --target <engine>`, which refuses an
168
+ engine that is not one of the game's targets, naming `std target add` (std's guide in
169
+ `node_modules/@volter-ai/std-dev/docs/`, "Targets").