@bitmagic/cli 0.1.65-dev.1 → 0.1.65-dev.10
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/README.md +65 -2
- package/assets/skills/trailer-craft.md +180 -0
- package/dist/cli.d.ts +8 -0
- package/dist/commands/forge.d.ts +15 -0
- package/dist/commands/forge.js +136 -37
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +9 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/trailer.js +1 -1
- package/dist/commands/trailer.js.map +1 -1
- package/dist/forge/edit-target.d.ts +30 -0
- package/dist/forge/edit-target.js +88 -0
- package/dist/forge/edit-target.js.map +1 -0
- package/dist/forge/remote-store.d.ts +36 -0
- package/dist/forge/remote-store.js +113 -0
- package/dist/forge/remote-store.js.map +1 -0
- package/dist/forge/run-pipeline.js +11 -2
- package/dist/forge/run-pipeline.js.map +1 -1
- package/dist/forge/stream.d.ts +5 -1
- package/dist/forge/stream.js +4 -0
- package/dist/forge/stream.js.map +1 -1
- package/dist/scaffold/blender-level-skill-files.js +1 -1
- package/dist/scaffold/project.js +2 -1
- package/dist/scaffold/project.js.map +1 -1
- package/dist/scaffold/skill-files.d.ts +32 -5
- package/dist/scaffold/skill-files.js +97 -59
- package/dist/scaffold/skill-files.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ browser reloads itself on every change and the assets panel shows each generatio
|
|
|
202
202
|
| `bitmagic publish [--visibility public\|private] [--name <name>] [--description <desc>] [--force] [--single-file] [--version-url] [--no-qr]` | Verify-gate, build if needed, and upload straight to GCS. `--single-file` publishes the offline bundle described under `bitmagic build`; switching the flag between runs always rebuilds, since it changes nothing on disk for the reuse check to notice. **Keeps the game's current visibility**; a new game is private until published with `--visibility public`. Prints the link worth sharing: `bitmagic.ai/play/<ID>/` — the game's page, with its name, cover and comments — for a public game, and `bitmagic.ai/games/<ID>/` for a private one. Both always serve the newest publish, so a link handed out today keeps working after tomorrow's publish; `--version-url` also prints the URL of this one build (`.../indexN.html`), for pinning a bug report to it. Shows the shared URL as a QR code so you can scan it onto a phone — drawn in the terminal, or written to `.bitmagic/publish-qr.png` when it cannot be. `--no-qr` skips both. |
|
|
203
203
|
| `bitmagic self-update [--tag latest\|dev]` | Update the **CLI itself** to the newest build of its line, installing into the place this CLI actually lives rather than wherever the `npm` on your `PATH` would put it — which under nvm is routinely somewhere else. Reads the project's line from `bitmagic.json` when there is one, so it also fixes a wrong-line install; `--tag` overrides. Verifies the version on disk moved, and refuses (naming the path) for an `npx` run, a source checkout, or an install shape it cannot recognise. Not to be confused with `upgrade`, which is about the project's engine. |
|
|
204
204
|
| `bitmagic upgrade [--engine <version>] [--force]` | Refresh vendored platform files and the CLI's own skills to the currently published engine. Refuses a dirty git tree unless `--force`. Also records the project's `environment` in `bitmagic.json` if it has none yet — the engine it just vendored came from that api-server — and says so when it does. |
|
|
205
|
-
| `bitmagic generate <skybox\|background\|block-type\|sound\|music\|image\|video\|character\|animation\|model\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `model` mints a **new** 3D object from a prompt, `prop` upgrades an existing placeholder **in place** (both produce voxels directly; `--mesh` takes the older mesh path; a low-poly project keeps the mesh as a `glb` asset — see **Art style**), `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt, `block-type` registers a custom voxel block, `video` makes a short clip for a cutscene (played with `engine.playVideo("<id>")`) — see **Cutscene clips** below. `skybox` and `background` write the **same** field: pick one. |
|
|
205
|
+
| `bitmagic generate <skybox\|background\|block-type\|sound\|music\|image\|video\|character\|animation\|model\|prop\|vehicle> ...` | Generate one asset directly into `src/work/world.json`. Costs sparks. `model` mints a **new** 3D object from a prompt, `prop` upgrades an existing placeholder **in place** (both produce voxels directly; `--mesh` takes the older mesh path; a low-poly project keeps the mesh as a `glb` asset — see **Art style**), `vehicle` adds a new drivable one, `animation` takes a parameter spec rather than a prompt, `block-type` registers a custom voxel block, `video` makes a short clip for a cutscene (played with `engine.playVideo("<id>")`) — see **Cutscene clips** below. `skybox` and `background` write the **same** field: pick one. `character --library-only` takes a ready-made body off the shelf for free instead of generating — see **Taking a character off the shelf**. |
|
|
206
206
|
| `bitmagic assets add <file> [--name <n>] [--asset-id <id>] [--keep-glb\|--voxelize] [--voxel-size <m>] [--max-voxel-size <m>] [--height <m>] [--hollow\|--solid] [--force]` | Upload **your own** file — `.glb`, `.vxl`, `.png/.jpg/.webp`, `.opus/.ogg/.m4a/.mp3`, `.json` — into the game's assets and write its entry into `src/work/world.json`. A `.glb` is voxelized in a headless browser (Chrome + a free port in 3000–3199, like `prop`) unless `--keep-glb` — or unless the project is low-poly (see **Art style**). Free — no sparks. See **Adding your own assets** below. |
|
|
207
207
|
| `bitmagic assets list` | The assets in `world.json`: id, name, type, size, placed instances, and total references. Local, no network. |
|
|
208
208
|
| `bitmagic assets materials <assetId> [--set <file.json>] [--auto] [--clear]` | Show what an asset's voxels look like, or say what they are **made of** — so a sword's steel blade shines and its leather grip does not. With no flag it prints the asset's colour groups; `--set` applies your verdicts; `--auto` lets a model decide (what `generate prop` and `generate model` already do on their own); `--clear` removes them. Free — no sparks, nothing generated. See **Voxel materials** below. |
|
|
@@ -215,6 +215,7 @@ browser reloads itself on every change and the assets panel shows each generatio
|
|
|
215
215
|
| `bitmagic trailer analyze \| beats \| cut \| render \| merge` | The steps of `make` as separate commands: score a recording into highlight windows with contact sheets; detect a track's beat grid; turn `trailer/shots.json` into a frame-exact `edl.json`; render an EDL (`--ui` redraws the HUD, `--ui-keyframe-rate` caps how often it may change, `--logo`, `--music …`); join recordings from separate sittings. |
|
|
216
216
|
| `bitmagic trailer publish <file.mp4> [--no-attach]` | Upload a trailer (`.mp4`/`.webm`, ≤ 200 MiB, header-checked so a renamed image is refused before upload) to the CDN under `game-media/<gameId>/` and put it at the front of the game page gallery. Re-running replaces the previous trailer (remembered in `.bitmagic/trailer/published.json`); the page is capped at 12 media items. Works before the first publish — the page goes live on the first `bitmagic publish --visibility public`. `--no-attach` uploads only. |
|
|
217
217
|
| `bitmagic forge --prompt "..." [--city\|--dungeon\|--platformer\|--freeform] [--name <name>] [--resume <jobId>]` | Design and bake a whole playable level into `world.json`. Costs sparks, and takes many minutes — the bake alone can take twenty. `--resume <jobId>` picks a failed run back up from the last step that finished, rather than redoing the whole thing. |
|
|
218
|
+
| `bitmagic forge --edit "..." [--level <id or name>]` | Change a detail of a level this project forged — a door style, the light fixtures, a prop — without re-forging it. The stored design is patched and only what the change touches is re-made; the level is replaced in place. See [Editing a forged level](#editing-a-forged-level). |
|
|
218
219
|
| `bitmagic tools <list\|path\|install> [bundle]` | The script bundles the CLI ships for skills that drive Blender or Python (`blender-level`, `unity-import`): what they are, where this install keeps them, and `install` to copy one into the project's `tools/`. Local, no network, no sparks. |
|
|
219
220
|
| `bitmagic levels <list\|add\|rename\|set-start\|remove> ...` | Manage the levels `forge` creates: which one boots, their names, registering a bake you already have, deleting one. Local, no network, no sparks. See **Managing levels** below. |
|
|
220
221
|
|
|
@@ -1207,6 +1208,42 @@ exact `--glb-url … --voxel-size …` command to retry the browser step without
|
|
|
1207
1208
|
If the finished asset reports that it will not drive, the project's vendored engine predates
|
|
1208
1209
|
vehicle-fitment derivation: run `bitmagic upgrade`, then regenerate.
|
|
1209
1210
|
|
|
1211
|
+
## Editing a forged level
|
|
1212
|
+
|
|
1213
|
+
A forged level's design is kept on its record, so a detail can be changed without designing and
|
|
1214
|
+
baking the world again:
|
|
1215
|
+
|
|
1216
|
+
```bash
|
|
1217
|
+
bitmagic forge --edit "make the doors sliding bulkheads"
|
|
1218
|
+
bitmagic forge --edit "warm amber lanterns instead of the lamps" --level "Freighter Mk IV"
|
|
1219
|
+
bitmagic forge --edit "recolour the hull plating a rusty red" --level level_1788942022362_yujy5i
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
The change goes to the designer as one small call that returns the handful of edits to the stored
|
|
1223
|
+
design — a fraction of a design's cost — and the level is then built from the edited design under
|
|
1224
|
+
a new job. What that build re-makes is decided by comparing it with the original, object by object:
|
|
1225
|
+
|
|
1226
|
+
| The change touches | What is re-made |
|
|
1227
|
+
|---|---|
|
|
1228
|
+
| the door style | the door leaves and the key pickups |
|
|
1229
|
+
| the light fixture (kind, colour, flicker) | the fixtures |
|
|
1230
|
+
| a prop or wall-decor kit piece | that piece |
|
|
1231
|
+
| a material colour, a wall/floor/ceiling kit piece, the rooms, the viewports, the terrain | the level bake and most objects |
|
|
1232
|
+
|
|
1233
|
+
Everything else — the baked level, every untouched object — is reused, so a door-style edit takes
|
|
1234
|
+
about as long as baking three small objects. The run prints what it reused. The level is replaced
|
|
1235
|
+
in place: same level id, doors, keys and placed objects rewritten, your other levels untouched. An
|
|
1236
|
+
object you have since regenerated in high quality is kept as it is.
|
|
1237
|
+
|
|
1238
|
+
One thing it needs: the level must have been forged by a CLI that records the job on the level
|
|
1239
|
+
and mirrors its bakes to the server — anything forged before that is re-forged once and editable
|
|
1240
|
+
from then on, from any machine. (A forge keeps its resumable state in the project's
|
|
1241
|
+
`.bitmagic/forge` and mirrors the browser-baked records to api-server as it goes, so `--resume`
|
|
1242
|
+
and `--edit` both work from a clone that never ran the original forge; a mirror that fails is
|
|
1243
|
+
said once and the run continues from the local copy.) `--level` takes the level's id or name (`bitmagic levels list`), and
|
|
1244
|
+
can be left out when the project has one forged level. `--edit` takes no other forge flag; a 2D
|
|
1245
|
+
side-view level cannot be edited yet.
|
|
1246
|
+
|
|
1210
1247
|
## Managing levels
|
|
1211
1248
|
|
|
1212
1249
|
`bitmagic forge` bakes levels but could not manage them: only a game's *first* forge became the
|
|
@@ -1274,11 +1311,37 @@ read by everything that used to assume cubes:
|
|
|
1274
1311
|
| `bitmagic cover`, `bitmagic reference make` | MagicaVoxel-style prompt lead and tail | flat-shaded low-poly lead and tail (same image model) |
|
|
1275
1312
|
| `bitmagic assets add x.glb` | voxelized in a headless browser | kept as a `glb` asset; `--voxelize` bakes it |
|
|
1276
1313
|
| `bitmagic generate model` / `prop` | voxel master, baked in the browser | mesh generated, re-uploaded into the project, registered as `glb` (`--height` / the placeholder's fitBox); `--voxelize` bakes it |
|
|
1277
|
-
| `bitmagic generate character` | voxelized rig, library search first | the textured low-poly rig,
|
|
1314
|
+
| `bitmagic generate character` | voxelized rig, library search first | the textured low-poly rig; its own library shelf, searched only where the server has enabled it; `--voxelize` overrides |
|
|
1278
1315
|
| `bitmagic generate vehicle` | voxel | still voxel — the builder is voxel-native; the command says so |
|
|
1279
1316
|
| `bitmagic judge` with no `## Art direction` | grades the style it sees | judges within low-poly |
|
|
1280
1317
|
| the engine on load | sunset hemisphere light and a faint bloom | neither; a `MeshLevel` brings its own lighting |
|
|
1281
1318
|
|
|
1319
|
+
### Taking a character off the shelf
|
|
1320
|
+
|
|
1321
|
+
`bitmagic generate character --library-only` skips generation entirely and hands
|
|
1322
|
+
back the closest ready-made body in the library. It is free and takes about a
|
|
1323
|
+
second, against $0.15 for a voxel forge or $0.60 for a low-poly one, and several
|
|
1324
|
+
minutes either way.
|
|
1325
|
+
|
|
1326
|
+
It differs from the ordinary library search in three ways worth knowing:
|
|
1327
|
+
|
|
1328
|
+
- **It ignores the quality bar.** Normal generation only substitutes a stored
|
|
1329
|
+
body when it is a close match; this takes the best there is. The output tells
|
|
1330
|
+
you how close it came, and says so plainly when the match is weak.
|
|
1331
|
+
- **It never forges.** If the library has nothing usable the command fails and
|
|
1332
|
+
charges nothing, rather than quietly falling back to a paid generation — you
|
|
1333
|
+
ran it to avoid spending, so it does not spend.
|
|
1334
|
+
- **It works regardless of whether automatic substitution is enabled.** Asking
|
|
1335
|
+
for a stored body is not the same as the server deciding to give you one.
|
|
1336
|
+
|
|
1337
|
+
`--head-scale` cannot be honoured (proportions are baked into a stored body; it
|
|
1338
|
+
is reported and ignored), and `--mesh-model` is rejected outright, since it
|
|
1339
|
+
selects how a character would be forged and nothing is forged.
|
|
1340
|
+
|
|
1341
|
+
The shelf you get depends on the project's art style, like every other character
|
|
1342
|
+
request: a voxel game searches voxel bodies, a low-poly game searches low-poly
|
|
1343
|
+
ones.
|
|
1344
|
+
|
|
1282
1345
|
In a low-poly game the textured rig IS the product, so `bitmagic generate character` forges it with
|
|
1283
1346
|
`runware-meshy-6`: an even remesh with symmetric limbs and an enforced A-pose, rather than a TRELLIS
|
|
1284
1347
|
reconstruction that is only as low-poly as the concept image happened to be. **That costs about
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Trailer craft — structure, pacing, and on-screen text
|
|
2
|
+
|
|
3
|
+
Read this before composing a cut or putting any text on screen. Compressed
|
|
4
|
+
from Derek Lieu's trailer-editing essays (trailer editor for Half-Life: Alyx,
|
|
5
|
+
Dead Cells, Spelunky 2) plus general trailer-typography guidance; sources at
|
|
6
|
+
the bottom.
|
|
7
|
+
|
|
8
|
+
## Ordering: Genre → Hook → Anchors → Content
|
|
9
|
+
|
|
10
|
+
- **Genre** in 1–2 shots: the viewer must know what kind of game this is
|
|
11
|
+
within the first seconds. For our games one gameplay shot usually does it.
|
|
12
|
+
- **Hook** — the thing only this game has — as soon as genre is established:
|
|
13
|
+
ideally inside the first 10–15 s, never later than the first third.
|
|
14
|
+
Showing generic-but-good content before the hook makes the game read as
|
|
15
|
+
"a lesser version of an old game".
|
|
16
|
+
- **Anchors** — the features people expect from the genre (for a racer:
|
|
17
|
+
overtakes, drifts, crashes, varied tracks) fill the middle.
|
|
18
|
+
- **Content** — breadth (levels, modes, vehicles) comes last, once the viewer
|
|
19
|
+
already cares.
|
|
20
|
+
|
|
21
|
+
## Timeline layout: constraints first, footage last
|
|
22
|
+
|
|
23
|
+
Lay the timeline out as a skeleton and only then hunt for footage — not the
|
|
24
|
+
other way round:
|
|
25
|
+
|
|
26
|
+
1. Write the section skeleton: **Cold open → Introduction → Escalation →
|
|
27
|
+
Climax → Breath → Button/End card**. Sections have different lengths and
|
|
28
|
+
energy; they are not equal slices.
|
|
29
|
+
2. Reserve the **end card** first: 2–3 s, ONE card (logo + tagline + CTA),
|
|
30
|
+
never a sequence of closing screens.
|
|
31
|
+
3. Place any **body title cards** next, as placeholders with their copy
|
|
32
|
+
written — each one names what the following footage must prove.
|
|
33
|
+
4. Only now fill footage, chosen to demonstrate whatever the neighbouring
|
|
34
|
+
card or section claims.
|
|
35
|
+
|
|
36
|
+
This turns "pick 15 great clips" (squishy, drifts toward sameness) into
|
|
37
|
+
"find the one clip that proves this beat" (constrained, self-diversifying).
|
|
38
|
+
|
|
39
|
+
## Pacing
|
|
40
|
+
|
|
41
|
+
- Sections need **ebb and flow**: build to a peak, then give a deliberate
|
|
42
|
+
breathing shot (~5 s, calmer, wider) before the next build or the finale.
|
|
43
|
+
The stops emphasize the fast; the fast emphasizes the slow.
|
|
44
|
+
- **Rule of three**: never more than three similar things in a row — three
|
|
45
|
+
crashes, three drift shots, three of anything. Viewers extrapolate after
|
|
46
|
+
two; the fourth is dead time.
|
|
47
|
+
- **Do not cut on every beat.** Snap cuts to the grid, but vary clip lengths
|
|
48
|
+
(2, 3, 4, 6 beats) and sometimes let a musical moment land on an in-clip
|
|
49
|
+
action (a landing, an impact) instead of a cut. Constant per-beat cutting
|
|
50
|
+
reads as a sizzle reel, and gets nauseating.
|
|
51
|
+
- Average shot length is a genre statement: ~1.3 s reads as AAA shooter,
|
|
52
|
+
~4 s reads as contemplative indie. An arcade racer sits around 1.5–3 s
|
|
53
|
+
with a few longer holds where one continuous action is the payoff.
|
|
54
|
+
- **More editing is not better editing.** A cut must clarify or escalate;
|
|
55
|
+
cutting fast to look "like a real trailer" hides the game. If a single
|
|
56
|
+
take shows the whole stunt, keep the take.
|
|
57
|
+
- Total length: under 90 s unless there's a reason; 30–45 s is a sweet spot
|
|
58
|
+
for social/store use.
|
|
59
|
+
|
|
60
|
+
## Variety — the five axes
|
|
61
|
+
|
|
62
|
+
Audit the cut on each axis; the labels list in the shots file is the tool
|
|
63
|
+
(read the labels top to bottom — repetition is visible in text before you
|
|
64
|
+
ever re-watch):
|
|
65
|
+
|
|
66
|
+
1. **Content** — different actions, not the same action in new places.
|
|
67
|
+
Escalate variants (jump → bigger jump) rather than repeating.
|
|
68
|
+
2. **Composition** — vary camera distance and framing; chase cam all the way
|
|
69
|
+
through is monotone. Watch eye-trace: the viewer's focus point should not
|
|
70
|
+
teleport across the frame on a cut.
|
|
71
|
+
3. **Energy** — intense vs. quiet moments; contrast matters more than speed.
|
|
72
|
+
4. **Pacing** — vary section tempo, not just shot tempo.
|
|
73
|
+
5. **Shot length** — vary beats-per-clip even inside a fast section.
|
|
74
|
+
|
|
75
|
+
## Where the shots come from
|
|
76
|
+
|
|
77
|
+
Played footage is the default and the bulk of any trailer: it is the only
|
|
78
|
+
thing that shows what the game DOES, and a trailer of pure scenery reads as a
|
|
79
|
+
tech demo however pretty it is. But two of the beats above are ones a chase
|
|
80
|
+
camera almost never hands you:
|
|
81
|
+
|
|
82
|
+
- the **cold open**, which wants the level to look composed and deliberate;
|
|
83
|
+
- the **breath**, which wants one calm wide shot with nothing happening.
|
|
84
|
+
|
|
85
|
+
Both are what an authored **camera fly-through** is for — a path through the
|
|
86
|
+
level with the game running underneath, so traffic still drives and animations
|
|
87
|
+
still play while the camera moves on rails. Recording one is cheap and nobody
|
|
88
|
+
has to play it, so a fly-through is worth having even when there is plenty of
|
|
89
|
+
gameplay: cut one or two of its shots into a gameplay trailer rather than
|
|
90
|
+
building a trailer out of it.
|
|
91
|
+
|
|
92
|
+
A fly-through alone is the right whole trailer in a narrow set of cases — a
|
|
93
|
+
world reveal, an architectural or exploration piece, a level that is the
|
|
94
|
+
feature — and whenever there is no one available to play. Otherwise it is a
|
|
95
|
+
source of shots, not a substitute for showing the game.
|
|
96
|
+
|
|
97
|
+
## On-screen text
|
|
98
|
+
|
|
99
|
+
When to use text at all: only when footage cannot say it. A card that
|
|
100
|
+
narrates what the next shot visibly shows is noise. Default for our
|
|
101
|
+
gameplay trailers: **end card only**; add body cards only when they carry
|
|
102
|
+
information the footage can't (game name early, a hook that isn't visual,
|
|
103
|
+
release date).
|
|
104
|
+
|
|
105
|
+
Writing the cards:
|
|
106
|
+
- Copy must sound like this game and no other. Exercise: describe the game
|
|
107
|
+
in three words; build cards from what's left after cutting everything
|
|
108
|
+
generic.
|
|
109
|
+
- ≤ 6 words per card. Evocative verbs beat feature lists ("Forge your own
|
|
110
|
+
path", not "Over 40 unique tracks").
|
|
111
|
+
- Banned: content quantities ("5 unique levels"), genre-descriptor stacks
|
|
112
|
+
("a 2D rogue-lite action adventure"), player-verb lists ("Race! Drift!
|
|
113
|
+
Win!"), and stock phrases ("In a world…", "Get ready…", "Experience…",
|
|
114
|
+
"…like never before").
|
|
115
|
+
|
|
116
|
+
Showing the cards:
|
|
117
|
+
- **Intercut, don't overlay**: a card gets its own beat between gameplay
|
|
118
|
+
shots (dimmed still or plain background). Never ask the viewer to read
|
|
119
|
+
while action plays — they do neither. Small persistent text (logo bug,
|
|
120
|
+
date) is the exception.
|
|
121
|
+
- Reading time: ~0.5–0.7 s per word, minimum 2.5 s on screen; end card
|
|
122
|
+
2–3 s (ours may run longer over a music outro).
|
|
123
|
+
- Cards ride the same beat grid as the cuts — a card is a clip.
|
|
124
|
+
- Typography must match the game's art quality and universe: one typeface
|
|
125
|
+
family for the whole trailer, generous letter-spacing for all-caps,
|
|
126
|
+
strong contrast against footage (our title raster already adds shadow),
|
|
127
|
+
subtle animation only (fade/scale-in) — a static default-font card reads
|
|
128
|
+
as a last-minute add-on.
|
|
129
|
+
|
|
130
|
+
## Structure template (30–45 s, gameplay-only, end card close)
|
|
131
|
+
|
|
132
|
+
| Section | ~share | What goes here |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| Cold open | 5–15% | The single most striking moment; genre obvious at a glance |
|
|
135
|
+
| Introduction | 15–20% | Clean, readable driving; establish look + one anchor |
|
|
136
|
+
| Escalation | 35–45% | Varied action, rising energy, hook prominent early in it |
|
|
137
|
+
| Climax | 15–20% | Densest, loudest stretch; biggest moments back to back |
|
|
138
|
+
| Breath | ~5% | One calmer, wider shot; lets the climax land |
|
|
139
|
+
| End card | 2–3 s+ | Logo, tagline, CTA — over the music's outro |
|
|
140
|
+
|
|
141
|
+
## Selection rules already paid for
|
|
142
|
+
|
|
143
|
+
These came out of review rounds on real cuts. They cost time to learn and
|
|
144
|
+
nothing to apply, so apply them before the first render, not after.
|
|
145
|
+
|
|
146
|
+
- **Never use slow motion.** On gameplay footage it reads as the game running
|
|
147
|
+
at a low framerate, not as drama. `speed` stays 1.0.
|
|
148
|
+
- **Numbers are not interest.** Event `severity` / `impactSpeed` ranks
|
|
149
|
+
car-stops-dead-against-a-wall highest — the worst possible clip. A crash
|
|
150
|
+
earns its slot only if things visibly break (`destruction` events, debris)
|
|
151
|
+
and motion continues through the hit.
|
|
152
|
+
- **Low speed can BE the drama.** Wheelspin recoveries throwing dirt or snow,
|
|
153
|
+
plowing back onto the track, being overtaken while struggling
|
|
154
|
+
(`position-change` with `gained: false`), `off-track` +
|
|
155
|
+
`hard-acceleration` clusters — these are among the best clips and score
|
|
156
|
+
near zero on any speed metric.
|
|
157
|
+
- **"Some shots of X" means a few.** The failure mode is 0-or-100: one
|
|
158
|
+
standing start is an opener, five is a broken trailer.
|
|
159
|
+
- **Quota check before rendering.** Read the shots list labels top to bottom.
|
|
160
|
+
Max one standing start; no more than three of any shot type in the whole
|
|
161
|
+
cut and never three similar in a row; vary track or biome, camera distance
|
|
162
|
+
and action type (the five axes above).
|
|
163
|
+
- **Menu and selection clips need `"ui": false`** — the HUD replay only knows
|
|
164
|
+
the gameplay HUD and will paint it over menus.
|
|
165
|
+
- **Verify on pixels.** Contact sheets and stills for every candidate, and
|
|
166
|
+
stills from the RENDERED file for anything you changed. The timeline knows
|
|
167
|
+
about events; it cannot see framing, occlusion or beauty.
|
|
168
|
+
- **Iterate cheaply, keep history.** Editing the shots and re-rendering is
|
|
169
|
+
minutes, and revisions auto-version — so render rather than debate, and
|
|
170
|
+
compare against the previous `-rN` before delivering.
|
|
171
|
+
|
|
172
|
+
## Sources
|
|
173
|
+
|
|
174
|
+
- [How to Hook the Audience and How Quickly to Do it](https://www.derek-lieu.com/blog/2022/10/24/how-to-hook-the-audience-and-how-quickly-to-do-it)
|
|
175
|
+
- [Step Two of Starting a Game Trailer Timeline](https://www.derek-lieu.com/blog/2023/2/19/step-two-of-starting-a-game-trailer-timeline)
|
|
176
|
+
- [How to Think About Pacing in a Trailer](https://www.derek-lieu.com/blog/2020/11/16/how-to-think-about-pacing-in-a-trailer)
|
|
177
|
+
- [Variety is the Spice of Trailers](https://www.derek-lieu.com/blog/2019/3/27/variety-is-the-spice-of-trailers)
|
|
178
|
+
- [More Trailer Editing is not Better Trailer Editing](https://www.derek-lieu.com/blog/2023/4/23/more-trailer-editing-is-not-better-trailer-editing)
|
|
179
|
+
- [How to Write Trailer Title Cards With Strong Messaging](https://www.derek-lieu.com/blog/2020/4/19/how-to-write-trailer-title-cards-with-strong-messaging)
|
|
180
|
+
- [10 Common Indie Game Trailer Mistakes & How to Fix Them](https://www.derek-lieu.com/blog/2020/9/14/10-common-indie-game-trailer-mistakes-and-how-to-fix-them)
|
package/dist/cli.d.ts
CHANGED
|
@@ -479,6 +479,14 @@ declare const rawCommands: {
|
|
|
479
479
|
readonly type: "boolean";
|
|
480
480
|
readonly description: "Boot the game into this level. Without it, only a game's FIRST forge becomes the start level, so a later forge lands in the level list unseen.";
|
|
481
481
|
};
|
|
482
|
+
readonly edit: {
|
|
483
|
+
readonly type: "string";
|
|
484
|
+
readonly description: "Change a detail of a level this project forged (\"make the doors sliding bulkheads\", \"warm amber lanterns instead of the lamps\") without re-forging it: the stored design is patched and only what the change touches is re-made. Replaces the level in place. Takes --level; no other forge flag.";
|
|
485
|
+
};
|
|
486
|
+
readonly level: {
|
|
487
|
+
readonly type: "string";
|
|
488
|
+
readonly description: "With --edit: the level to change, by id or name (`bitmagic levels list`). Omit when the project has one forged level.";
|
|
489
|
+
};
|
|
482
490
|
readonly json: {
|
|
483
491
|
readonly type: "boolean";
|
|
484
492
|
readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
|
package/dist/commands/forge.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ type ModeFlag = (typeof MODE_FLAGS)[number];
|
|
|
11
11
|
* the server receives the answer rather than recomputing it.
|
|
12
12
|
*/
|
|
13
13
|
export declare function deriveLevelName(explicit: string | undefined, prompt: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* `--edit` changes a level as it was designed, so it takes none of a fresh forge's steering: no
|
|
16
|
+
* prompt, no scene type, no reference image, no revision rounds, no start-level switch, and no
|
|
17
|
+
* resume (a resumed edit replays its frozen request, which `--edit` would contradict). Refused
|
|
18
|
+
* before any request, where it is still free.
|
|
19
|
+
*/
|
|
20
|
+
export declare function assertEditFlags(args: Record<string, unknown>): void;
|
|
14
21
|
/** The single selected mode, or undefined for plain terrain. */
|
|
15
22
|
export declare function selectMode(args: Record<string, unknown>): ModeFlag | undefined;
|
|
16
23
|
/**
|
|
@@ -144,6 +151,14 @@ export declare const forgeCommand: import("citty").CommandDef<{
|
|
|
144
151
|
readonly type: "boolean";
|
|
145
152
|
readonly description: "Boot the game into this level. Without it, only a game's FIRST forge becomes the start level, so a later forge lands in the level list unseen.";
|
|
146
153
|
};
|
|
154
|
+
readonly edit: {
|
|
155
|
+
readonly type: "string";
|
|
156
|
+
readonly description: "Change a detail of a level this project forged (\"make the doors sliding bulkheads\", \"warm amber lanterns instead of the lamps\") without re-forging it: the stored design is patched and only what the change touches is re-made. Replaces the level in place. Takes --level; no other forge flag.";
|
|
157
|
+
};
|
|
158
|
+
readonly level: {
|
|
159
|
+
readonly type: "string";
|
|
160
|
+
readonly description: "With --edit: the level to change, by id or name (`bitmagic levels list`). Omit when the project has one forged level.";
|
|
161
|
+
};
|
|
147
162
|
readonly json: {
|
|
148
163
|
readonly type: "boolean";
|
|
149
164
|
readonly description: "Print the result as JSON on stdout; all progress goes to stderr.";
|
package/dist/commands/forge.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* nothing, and is my project still intact?" is the first question.
|
|
20
20
|
*/
|
|
21
21
|
import { defineCommand } from 'citty';
|
|
22
|
-
import { buildAssetCatalog, createForgedWorkStore, SIDEON_FORGE_STEPS, } from '@bitmagic/world-forger/pipeline/index.js';
|
|
22
|
+
import { buildAssetCatalog, createForgedWorkStore, describeEditSeed, EDIT_FORGE_STEPS, seedEditStore, SIDEON_FORGE_STEPS, } from '@bitmagic/world-forger/pipeline/index.js';
|
|
23
|
+
import { bakeEverythingPlan, buildEditJobInput, resolveEditTarget } from '../forge/edit-target.js';
|
|
23
24
|
import { CliError } from '../errors.js';
|
|
24
25
|
import { createOutput } from '../output.js';
|
|
25
26
|
import { creatorPromptField, ORIGINAL_PROMPT_FLAG } from '../telemetry/creator-prompt.js';
|
|
@@ -43,6 +44,7 @@ import { assertNotOpenWater, describeSideonRegistryLoss, readCharacterRunSpeed,
|
|
|
43
44
|
import { reserveLanePort } from '../local-port.js';
|
|
44
45
|
import { createForgeProgressTracker } from '../forge/progress.js';
|
|
45
46
|
import { createLocalForgeStorage } from '../forge/local-store.js';
|
|
47
|
+
import { createMirroredForgeStorage } from '../forge/remote-store.js';
|
|
46
48
|
import { describeForgeFailure, requestForge } from '../forge/stream.js';
|
|
47
49
|
import { buildCliForgeDeps, createConsoleForgeLogger, readFrozenInput, runForgePipeline, seedForgeStore, } from '../forge/run-pipeline.js';
|
|
48
50
|
/** Mirrors api-server's `FORGE_JOB_ID_PATTERN` so a typo is rejected here, before a round trip. */
|
|
@@ -74,6 +76,27 @@ export function deriveLevelName(explicit, prompt) {
|
|
|
74
76
|
const derived = prompt.trim().split(/\s+/).slice(0, 6).join(' ').slice(0, 48).trim();
|
|
75
77
|
return derived.length > 0 ? derived : 'level';
|
|
76
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* `--edit` changes a level as it was designed, so it takes none of a fresh forge's steering: no
|
|
81
|
+
* prompt, no scene type, no reference image, no revision rounds, no start-level switch, and no
|
|
82
|
+
* resume (a resumed edit replays its frozen request, which `--edit` would contradict). Refused
|
|
83
|
+
* before any request, where it is still free.
|
|
84
|
+
*/
|
|
85
|
+
export function assertEditFlags(args) {
|
|
86
|
+
const conflicts = [
|
|
87
|
+
['prompt', typeof args.prompt === 'string' && args.prompt !== ''],
|
|
88
|
+
['resume', typeof args.resume === 'string' && args.resume !== ''],
|
|
89
|
+
['reference-image', typeof args['reference-image'] === 'string' && args['reference-image'] !== ''],
|
|
90
|
+
['reference-image-url', typeof args['reference-image-url'] === 'string' && args['reference-image-url'] !== ''],
|
|
91
|
+
['match-rounds', args['match-rounds'] !== undefined],
|
|
92
|
+
['make-start', args['make-start'] === true],
|
|
93
|
+
...MODE_FLAGS.map((mode) => [mode, args[mode] === true]),
|
|
94
|
+
].filter(([, set]) => set).map(([flag]) => `--${flag}`);
|
|
95
|
+
if (conflicts.length > 0) {
|
|
96
|
+
throw new CliError(`--edit changes a forged level as it was designed and cannot be combined with ${conflicts.join(', ')}. `
|
|
97
|
+
+ 'Describe the change alone: `bitmagic forge --edit "make the doors sliding bulkheads" --level <id or name>`.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
77
100
|
/** The single selected mode, or undefined for plain terrain. */
|
|
78
101
|
export function selectMode(args) {
|
|
79
102
|
const chosen = MODE_FLAGS.filter((mode) => args[mode] === true);
|
|
@@ -291,7 +314,7 @@ async function runBrowserSteps(options) {
|
|
|
291
314
|
});
|
|
292
315
|
await host.launch();
|
|
293
316
|
await host.loadGame();
|
|
294
|
-
const storage =
|
|
317
|
+
const { storage } = options;
|
|
295
318
|
const store = createForgedWorkStore(jobId, storage);
|
|
296
319
|
const forgeDeps = buildCliForgeDeps({
|
|
297
320
|
projectRoot: context.root,
|
|
@@ -357,11 +380,14 @@ export async function runForge(args, deps) {
|
|
|
357
380
|
const runDeps = deps ?? defaultDeps(args.json === true);
|
|
358
381
|
const prepared = await prepareForgeRequest(args, runDeps);
|
|
359
382
|
const { context, environment, storage, input, resumeJobId, token, reference, lane } = prepared;
|
|
383
|
+
// An edit of a forged level (fresh, or a resumed edit): the frozen request names its parent.
|
|
384
|
+
const editing = input.parentJobId !== undefined;
|
|
360
385
|
// One advisory line about the reference image, fresh runs only (a resume replays the frozen
|
|
361
|
-
// request, picture and all
|
|
362
|
-
//
|
|
363
|
-
// prompt; the forge proceeds
|
|
364
|
-
|
|
386
|
+
// request, picture and all; an edit never sees the picture — it changes a stored design):
|
|
387
|
+
// either the project default took effect, or — when there is no reference at all — a pointer
|
|
388
|
+
// to the command that would give the designer one. Neither is a prompt; the forge proceeds
|
|
389
|
+
// regardless.
|
|
390
|
+
if (resumeJobId === null && !editing) {
|
|
365
391
|
const referenceLine = describeAppliedReference(reference);
|
|
366
392
|
if (referenceLine)
|
|
367
393
|
runDeps.log(referenceLine);
|
|
@@ -396,11 +422,14 @@ export async function runForge(args, deps) {
|
|
|
396
422
|
onLayout: report.layout,
|
|
397
423
|
// A side-on run is four steps (no level bake) — with the default list the
|
|
398
424
|
// banners would read [n/5] and "Baking level" would sit pending forever.
|
|
399
|
-
|
|
425
|
+
// An edit keeps the five, with the design step named for what it does.
|
|
426
|
+
...(editing ? { steps: EDIT_FORGE_STEPS } : lane === 'sideon' ? { steps: SIDEON_FORGE_STEPS } : {}),
|
|
400
427
|
});
|
|
401
428
|
log(resumeJobId !== null
|
|
402
|
-
? `Resuming forge job ${resumeJobId} for game ${context.metadata.gameId}.`
|
|
403
|
-
:
|
|
429
|
+
? `Resuming ${editing ? 'edit' : 'forge'} job ${resumeJobId} for game ${context.metadata.gameId}.`
|
|
430
|
+
: editing
|
|
431
|
+
? `Editing level "${input.name}" of game ${context.metadata.gameId}: "${input.editPrompt ?? input.prompt}"`
|
|
432
|
+
: `Forging a ${lane === 'sideon' ? 'SIDE-ON 2D level' : 'level'} for game ${context.metadata.gameId}: "${input.prompt}"`);
|
|
404
433
|
// Said BEFORE the billed request, never only in the post-forge notes: a
|
|
405
434
|
// side-on forge discards a multi-level registry and the objects in it.
|
|
406
435
|
if (lane === 'sideon' && resumeJobId === null) {
|
|
@@ -418,16 +447,24 @@ export async function runForge(args, deps) {
|
|
|
418
447
|
else
|
|
419
448
|
log(` ${message}`);
|
|
420
449
|
},
|
|
421
|
-
},
|
|
450
|
+
}, editing
|
|
422
451
|
? {
|
|
423
|
-
// The
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
+ 'Nothing was sent or billed — update later, or build the level from the web creator once it ships there.',
|
|
452
|
+
// The edit's own route, for the same loud-404 reason as the side-on lane: an older
|
|
453
|
+
// api-server would otherwise design a whole new level from an edit request and bill it.
|
|
454
|
+
path: '/api/cli/v1/forge-edit/stream',
|
|
455
|
+
notFoundMessage: 'This api-server cannot edit forged levels yet (the route is missing). '
|
|
456
|
+
+ 'Nothing was sent or billed — update later, or re-forge the level with the change in its prompt.',
|
|
429
457
|
}
|
|
430
|
-
:
|
|
458
|
+
: lane === 'sideon'
|
|
459
|
+
? {
|
|
460
|
+
// The sibling route, so an api-server deployed before the side-on lane
|
|
461
|
+
// 404s LOUDLY here — before any model call and before any charge —
|
|
462
|
+
// instead of silently running a wrong 3D forge.
|
|
463
|
+
path: '/api/cli/v1/forge-sideon/stream',
|
|
464
|
+
notFoundMessage: 'This api-server does not support 2D side-view forging yet (the route is missing). '
|
|
465
|
+
+ 'Nothing was sent or billed — update later, or build the level from the web creator once it ships there.',
|
|
466
|
+
}
|
|
467
|
+
: undefined);
|
|
431
468
|
if (!outcome.success) {
|
|
432
469
|
throw new CliError(describeForgeFailure(outcome));
|
|
433
470
|
}
|
|
@@ -437,14 +474,36 @@ export async function runForge(args, deps) {
|
|
|
437
474
|
log(`Design and geometry are saved server-side as job ${jobId}.`);
|
|
438
475
|
// From here on the design and the geometry are PAID FOR and saved server-side. Every failure
|
|
439
476
|
// below must therefore point at the resume rather than at re-running from scratch.
|
|
440
|
-
|
|
477
|
+
if (editing) {
|
|
478
|
+
// The parent's bakes live in THIS project's store, if it forged the level here. The
|
|
479
|
+
// server's plan is an upper bound; the seed says what this machine could actually reuse.
|
|
480
|
+
const parentJobId = outcome.parentJobId ?? input.parentJobId;
|
|
481
|
+
const world = readWorldJson(projectWorldJsonPath(context.root));
|
|
482
|
+
const seed = await seedEditStore({
|
|
483
|
+
store: createForgedWorkStore(jobId, storage),
|
|
484
|
+
parentStore: createForgedWorkStore(parentJobId, storage),
|
|
485
|
+
artifact,
|
|
486
|
+
input,
|
|
487
|
+
reuse: outcome.reuse ?? bakeEverythingPlan(artifact, 'the server returned no reuse plan'),
|
|
488
|
+
worldAssets: Array.isArray(world.assets) ? world.assets.filter(isJsonObject) : [],
|
|
489
|
+
});
|
|
490
|
+
log(describeEditSeed(seed));
|
|
491
|
+
if (platformerMovement)
|
|
492
|
+
await createForgedWorkStore(jobId, storage).put('platformer-movement', platformerMovement);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
await seedForgeStore(createForgedWorkStore(jobId, storage), artifact, input, platformerMovement);
|
|
496
|
+
}
|
|
441
497
|
try {
|
|
442
|
-
await runBrowserSteps({ context, environment, jobId, token, deps: { ...runDeps, log }, tracker, lane });
|
|
498
|
+
await runBrowserSteps({ context, environment, jobId, token, deps: { ...runDeps, log }, tracker, lane, storage });
|
|
443
499
|
}
|
|
444
500
|
catch (error) {
|
|
445
501
|
throw decorateBrowserStepError(error, jobId);
|
|
446
502
|
}
|
|
447
|
-
output.result({
|
|
503
|
+
output.result({
|
|
504
|
+
ok: true, jobId, gameId: context.metadata.gameId, name: input.name,
|
|
505
|
+
...(editing ? { edited: true, levelAssetId: input.assetId, parentJobId: outcome.parentJobId ?? input.parentJobId } : {}),
|
|
506
|
+
});
|
|
448
507
|
});
|
|
449
508
|
}
|
|
450
509
|
async function prepareForgeRequest(args, deps) {
|
|
@@ -455,7 +514,33 @@ async function prepareForgeRequest(args, deps) {
|
|
|
455
514
|
throw new CliError('--resume takes a job id of 1-64 characters (letters, digits, "_" or "-") — the one printed '
|
|
456
515
|
+ 'by the run you are resuming.');
|
|
457
516
|
}
|
|
458
|
-
const
|
|
517
|
+
const environment = resolveEnvironmentForCommand({ cwd: context.root, baseDir: deps.baseDir });
|
|
518
|
+
// Resolved on first use and shared by the store and the request: the store
|
|
519
|
+
// behind every read below is MIRRORED to api-server (remote-store.ts), so a
|
|
520
|
+
// job started on another machine can be resumed or edited here — its frozen
|
|
521
|
+
// request and bakes are fetched when the local copy lacks them — and only
|
|
522
|
+
// such a fetch, or the request itself, needs the credential.
|
|
523
|
+
let tokenPromise;
|
|
524
|
+
const accessToken = () => {
|
|
525
|
+
tokenPromise ??= getAccessToken(environment, resolveAuth0ClientId(environment), { fetch: deps.fetch, sleep: deps.sleep, now: deps.now }, deps.baseDir);
|
|
526
|
+
return tokenPromise;
|
|
527
|
+
};
|
|
528
|
+
const storage = createMirroredForgeStorage(createLocalForgeStorage(context.root), {
|
|
529
|
+
environment, accessToken, fetch: deps.fetch, warn: (message) => deps.log(`warning: ${message}`),
|
|
530
|
+
});
|
|
531
|
+
// A resume replays the frozen request — including its reference image — so a file passed
|
|
532
|
+
// alongside `--resume` would be uploaded and then silently ignored. Refused instead: the
|
|
533
|
+
// caller clearly wanted the picture to matter, and it cannot here.
|
|
534
|
+
if (resumeJobId !== null && typeof args['reference-image'] === 'string' && args['reference-image'] !== '') {
|
|
535
|
+
throw new CliError('A resumed forge replays the original request, reference image and all — --reference-image '
|
|
536
|
+
+ 'cannot change it. Drop --resume to start a fresh forge with this picture.');
|
|
537
|
+
}
|
|
538
|
+
// Same rule for the rounds: they are frozen with the job, and a resume that skips the design
|
|
539
|
+
// step never runs them anyway.
|
|
540
|
+
if (resumeJobId !== null && args['match-rounds'] !== undefined) {
|
|
541
|
+
throw new CliError('A resumed forge replays the original request — --match-rounds cannot change it. '
|
|
542
|
+
+ 'Drop --resume to start a fresh forge.');
|
|
543
|
+
}
|
|
459
544
|
const frozen = resumeJobId !== null ? await readFrozenInput(storage, resumeJobId) : null;
|
|
460
545
|
// Route by the project's dimension HERE, before any token/server work:
|
|
461
546
|
// steps 1-2 bill. 3D projects keep today's pipeline; side-view 2D projects
|
|
@@ -468,24 +553,32 @@ async function prepareForgeRequest(args, deps) {
|
|
|
468
553
|
// Refused here, where it is still free.
|
|
469
554
|
if (lane === 'sideon')
|
|
470
555
|
assertNotOpenWater(context.root);
|
|
556
|
+
// `--edit`: change a forged level instead of designing one. The request is
|
|
557
|
+
// frozen from the level's own record (its name, its asset, the job that built
|
|
558
|
+
// it) and the change; everything a fresh forge steers by is refused.
|
|
559
|
+
const editPrompt = typeof args.edit === 'string' && args.edit.trim().length > 0 ? args.edit.trim() : null;
|
|
560
|
+
if (editPrompt !== null) {
|
|
561
|
+
assertEditFlags(args);
|
|
562
|
+
if (lane === 'sideon') {
|
|
563
|
+
throw new CliError('--edit changes a forged 3D level; a 2D side-view level cannot be edited yet — re-forge it with the change in its prompt.');
|
|
564
|
+
}
|
|
565
|
+
const target = resolveEditTarget(readWorldJson(projectWorldJsonPath(context.root)), typeof args.level === 'string' ? args.level : undefined);
|
|
566
|
+
const input = buildEditJobInput({
|
|
567
|
+
target, editPrompt, gameId: context.metadata.gameId, groundPlane: readGroundPlane(context.root),
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
context, environment, storage, input, resumeJobId: null, token: await accessToken(), lane,
|
|
571
|
+
reference: resolveReferenceImageUrl(args, context.root),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
if (typeof args.level === 'string' && args.level !== '') {
|
|
575
|
+
throw new CliError('--level says which level --edit changes; pass --edit "<the change>" with it.');
|
|
576
|
+
}
|
|
471
577
|
const mode = selectMode(args);
|
|
472
578
|
if (lane === 'sideon' && mode !== undefined) {
|
|
473
579
|
throw new CliError(`--${mode} does not apply to a 2D side-view project — the side-on forge derives the layout `
|
|
474
580
|
+ 'from the prompt (and GAME-DESIGN.md). Drop the flag and describe the level instead.');
|
|
475
581
|
}
|
|
476
|
-
// A resume replays the frozen request — including its reference image — so a file passed
|
|
477
|
-
// alongside `--resume` would be uploaded and then silently ignored. Refused instead: the
|
|
478
|
-
// caller clearly wanted the picture to matter, and it cannot here.
|
|
479
|
-
if (resumeJobId !== null && typeof args['reference-image'] === 'string' && args['reference-image'] !== '') {
|
|
480
|
-
throw new CliError('A resumed forge replays the original request, reference image and all — --reference-image '
|
|
481
|
-
+ 'cannot change it. Drop --resume to start a fresh forge with this picture.');
|
|
482
|
-
}
|
|
483
|
-
// Same rule for the rounds: they are frozen with the job, and a resume that skips the design
|
|
484
|
-
// step never runs them anyway.
|
|
485
|
-
if (resumeJobId !== null && args['match-rounds'] !== undefined) {
|
|
486
|
-
throw new CliError('A resumed forge replays the original request — --match-rounds cannot change it. '
|
|
487
|
-
+ 'Drop --resume to start a fresh forge.');
|
|
488
|
-
}
|
|
489
582
|
const referenceMatchRounds = parseNumberFlag('match-rounds', typeof args['match-rounds'] === 'string' ? args['match-rounds'] : undefined, { min: 0, max: 3, integer: true });
|
|
490
583
|
const reference = resumeJobId !== null
|
|
491
584
|
? resolveReferenceImageUrl(args, context.root)
|
|
@@ -523,9 +616,7 @@ async function prepareForgeRequest(args, deps) {
|
|
|
523
616
|
...(blockSize !== undefined ? { voxelBlockSize: blockSize } : {}),
|
|
524
617
|
lane,
|
|
525
618
|
};
|
|
526
|
-
const
|
|
527
|
-
const clientId = resolveAuth0ClientId(environment);
|
|
528
|
-
const token = await getAccessToken(environment, clientId, { fetch: deps.fetch, sleep: deps.sleep, now: deps.now }, deps.baseDir);
|
|
619
|
+
const token = await accessToken();
|
|
529
620
|
return { context, environment, storage, input, resumeJobId, token, reference, lane };
|
|
530
621
|
}
|
|
531
622
|
catch (error) {
|
|
@@ -608,6 +699,14 @@ export const forgeCommand = defineCommand({
|
|
|
608
699
|
type: 'boolean',
|
|
609
700
|
description: 'Boot the game into this level. Without it, only a game\'s FIRST forge becomes the start level, so a later forge lands in the level list unseen.',
|
|
610
701
|
},
|
|
702
|
+
edit: {
|
|
703
|
+
type: 'string',
|
|
704
|
+
description: 'Change a detail of a level this project forged ("make the doors sliding bulkheads", "warm amber lanterns instead of the lamps") without re-forging it: the stored design is patched and only what the change touches is re-made. Replaces the level in place. Takes --level; no other forge flag.',
|
|
705
|
+
},
|
|
706
|
+
level: {
|
|
707
|
+
type: 'string',
|
|
708
|
+
description: 'With --edit: the level to change, by id or name (`bitmagic levels list`). Omit when the project has one forged level.',
|
|
709
|
+
},
|
|
611
710
|
json: {
|
|
612
711
|
type: 'boolean',
|
|
613
712
|
description: 'Print the result as JSON on stdout; all progress goes to stderr.',
|