@bitmagic/cli 0.1.52-dev.9 → 0.1.53-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +45 -9
  2. package/dist/assets/add.js +2 -1
  3. package/dist/assets/add.js.map +1 -1
  4. package/dist/assets/voxelize-glb.d.ts +17 -1
  5. package/dist/assets/voxelize-glb.js +43 -19
  6. package/dist/assets/voxelize-glb.js.map +1 -1
  7. package/dist/cli.d.ts +8 -0
  8. package/dist/cli.js +6 -6
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/generate.d.ts +27 -0
  11. package/dist/commands/generate.js +132 -46
  12. package/dist/commands/generate.js.map +1 -1
  13. package/dist/commands/judge.d.ts +5 -0
  14. package/dist/commands/judge.js +28 -7
  15. package/dist/commands/judge.js.map +1 -1
  16. package/dist/commands/levels.d.ts +10 -17
  17. package/dist/commands/levels.js +48 -48
  18. package/dist/commands/levels.js.map +1 -1
  19. package/dist/commands/publish.d.ts +6 -1
  20. package/dist/commands/publish.js +29 -15
  21. package/dist/commands/publish.js.map +1 -1
  22. package/dist/commands/verify.d.ts +4 -0
  23. package/dist/commands/verify.js +94 -51
  24. package/dist/commands/verify.js.map +1 -1
  25. package/dist/editor/journal.js +13 -7
  26. package/dist/editor/journal.js.map +1 -1
  27. package/dist/generate/model.d.ts +13 -2
  28. package/dist/generate/model.js +70 -21
  29. package/dist/generate/model.js.map +1 -1
  30. package/dist/generate/prop.d.ts +8 -0
  31. package/dist/generate/prop.js +8 -6
  32. package/dist/generate/prop.js.map +1 -1
  33. package/dist/judge/record.d.ts +3 -0
  34. package/dist/judge/record.js +3 -0
  35. package/dist/judge/record.js.map +1 -1
  36. package/dist/levels/registry.d.ts +5 -2
  37. package/dist/levels/registry.js +32 -27
  38. package/dist/levels/registry.js.map +1 -1
  39. package/dist/project/jobs.d.ts +1 -1
  40. package/dist/project/jobs.js +16 -20
  41. package/dist/project/jobs.js.map +1 -1
  42. package/dist/project/platform-declaration.d.ts +51 -0
  43. package/dist/project/platform-declaration.js +81 -0
  44. package/dist/project/platform-declaration.js.map +1 -0
  45. package/dist/publish/platform.d.ts +4 -8
  46. package/dist/publish/platform.js +19 -53
  47. package/dist/publish/platform.js.map +1 -1
  48. package/dist/scaffold/project-files.js +54 -21
  49. package/dist/scaffold/project-files.js.map +1 -1
  50. package/dist/verify/artifacts.d.ts +19 -0
  51. package/dist/verify/artifacts.js +34 -0
  52. package/dist/verify/artifacts.js.map +1 -0
  53. package/dist/verify/browser.d.ts +10 -0
  54. package/dist/verify/browser.js +79 -9
  55. package/dist/verify/browser.js.map +1 -1
  56. package/dist/verify/classify.d.ts +37 -1
  57. package/dist/verify/classify.js +66 -2
  58. package/dist/verify/classify.js.map +1 -1
  59. package/dist/verify/device.d.ts +43 -0
  60. package/dist/verify/device.js +59 -0
  61. package/dist/verify/device.js.map +1 -0
  62. package/dist/verify/iterate.d.ts +21 -2
  63. package/dist/verify/iterate.js +43 -36
  64. package/dist/verify/iterate.js.map +1 -1
  65. package/dist/verify/mobile-parity.d.ts +42 -0
  66. package/dist/verify/mobile-parity.js +84 -0
  67. package/dist/verify/mobile-parity.js.map +1 -0
  68. package/dist/verify/platform.d.ts +47 -0
  69. package/dist/verify/platform.js +89 -0
  70. package/dist/verify/platform.js.map +1 -0
  71. package/dist/verify/result.d.ts +90 -6
  72. package/dist/verify/result.js +89 -10
  73. package/dist/verify/result.js.map +1 -1
  74. package/dist/verify/url.d.ts +9 -1
  75. package/dist/verify/url.js +10 -2
  76. package/dist/verify/url.js.map +1 -1
  77. package/package.json +4 -4
@@ -1,11 +1,13 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import { aliasSourceDir } from '../scaffold/aliases.js';
1
+ import { readPlatformDeclaration } from '../project/platform-declaration.js';
4
2
  /**
5
3
  * The platform indicators a publish syncs from the project's own work copy: which platform the
6
4
  * game is FOR (`primaryPlatform`, top level of game.json) and, for a phone game that only makes
7
5
  * sense one way up, `worldProfileData.mobileOrientation`.
8
6
  *
7
+ * The parse itself lives in `project/platform-declaration.ts`, shared with `bitmagic verify`,
8
+ * which boots the platform the same two fields declare. What stays HERE is the wire semantics,
9
+ * which are publish's alone.
10
+ *
9
11
  * The project files are the source of truth, exactly as they are in the web lane — the columns
10
12
  * are "indicators synced from game.json at publish, like `genre`" (migration 048). So a value
11
13
  * changed anywhere else is reverted by the creator's next publish, and the way to make a game
@@ -22,44 +24,29 @@ import { aliasSourceDir } from '../scaffold/aliases.js';
22
24
  * the key instead would make an orientation impossible to remove from here. Only a work copy
23
25
  * neither of whose files can be read omits the key.
24
26
  *
25
- * world.json wins over game.json for the orientation, matching the web lane's own resolution
26
- * (`game-storage-backend.ts`'s `getListingFieldsSync`). Note this is the OPPOSITE order to
27
- * `readFaviconOverride` in favicon.ts, which reads game.json first — the favicon override's home
28
- * is game.json, the orientation's is world.json, and neither order is a house style.
29
- *
30
- * The legacy `mainPlatform` spelling is NOT honoured: it predates the CLI lane entirely, so no
31
- * project this reads can carry it.
32
- *
33
27
  * Best effort throughout, like `readFaviconOverride`: a missing, unreadable or malformed file
34
28
  * means "the project declares nothing", never an error. A catalog indicator must not be able to
35
29
  * fail a publish whose bundle is already uploaded.
36
30
  */
37
31
  export function readPlatformIndicators(root) {
38
- const workDir = path.join(root, aliasSourceDir('work'));
39
- const game = readJsonObject(path.join(workDir, 'game.json'));
40
- const world = readJsonObject(path.join(workDir, 'world.json'));
32
+ const declaration = readPlatformDeclaration(root);
41
33
  const fields = {};
42
34
  const notes = [];
43
- const declared = game?.primaryPlatform;
44
- if (declared === 'desktop' || declared === 'mobile') {
45
- fields.primaryPlatform = declared;
35
+ if (declaration.primaryPlatform !== undefined) {
36
+ fields.primaryPlatform = declaration.primaryPlatform;
37
+ }
38
+ if (declaration.workCopyReadable) {
39
+ // Explicit null when the work copy is readable but declares nothing — see the doc comment.
40
+ fields.mobileOrientation = declaration.mobileOrientation ?? null;
46
41
  }
47
- else if (declared !== undefined) {
48
- notes.push(`game.json primaryPlatform ${JSON.stringify(declared)} is not "desktop" or "mobile" — ` +
49
- 'leaving the stored platform alone.');
42
+ const { rejected } = declaration;
43
+ if ('primaryPlatform' in rejected) {
44
+ notes.push(`game.json primaryPlatform ${JSON.stringify(rejected.primaryPlatform)} is not "desktop" `
45
+ + 'or "mobile" — leaving the stored platform alone.');
50
46
  }
51
- if (world !== null || game !== null) {
52
- const orientation = worldProfileValue(world, 'mobileOrientation') ?? worldProfileValue(game, 'mobileOrientation');
53
- if (orientation === 'portrait' || orientation === 'landscape') {
54
- fields.mobileOrientation = orientation;
55
- }
56
- else {
57
- if (orientation !== undefined) {
58
- notes.push(`worldProfileData.mobileOrientation ${JSON.stringify(orientation)} is not "portrait" ` +
59
- 'or "landscape" — publishing with no orientation lock.');
60
- }
61
- fields.mobileOrientation = null;
62
- }
47
+ if ('mobileOrientation' in rejected) {
48
+ notes.push(`worldProfileData.mobileOrientation ${JSON.stringify(rejected.mobileOrientation)} is not `
49
+ + '"portrait" or "landscape" — publishing with no orientation lock.');
63
50
  }
64
51
  if (fields.primaryPlatform === 'mobile') {
65
52
  notes.push(fields.mobileOrientation
@@ -68,25 +55,4 @@ export function readPlatformIndicators(root) {
68
55
  }
69
56
  return { fields, notes };
70
57
  }
71
- /** Parse one work-copy JSON file, or null when it is missing, unreadable or not an object. */
72
- function readJsonObject(filePath) {
73
- let parsed;
74
- try {
75
- if (!fs.existsSync(filePath))
76
- return null;
77
- parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
78
- }
79
- catch {
80
- return null;
81
- }
82
- return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
83
- ? parsed
84
- : null;
85
- }
86
- function worldProfileValue(source, key) {
87
- const profile = source?.worldProfileData;
88
- if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
89
- return undefined;
90
- return profile[key];
91
- }
92
58
  //# sourceMappingURL=platform.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/publish/platform.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/publish/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAgB7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,MAAM,WAAW,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,WAAW,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QAC9C,MAAM,CAAC,eAAe,GAAG,WAAW,CAAC,eAAe,CAAC;IACvD,CAAC;IAED,IAAI,WAAW,CAAC,gBAAgB,EAAE,CAAC;QACjC,2FAA2F;QAC3F,MAAM,CAAC,iBAAiB,GAAG,WAAW,CAAC,iBAAiB,IAAI,IAAI,CAAC;IACnE,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAC;IACjC,IAAI,iBAAiB,IAAI,QAAQ,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CACR,6BAA6B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,eAAe,CAAC,oBAAoB;cACrF,kDAAkD,CACvD,CAAC;IACJ,CAAC;IACD,IAAI,mBAAmB,IAAI,QAAQ,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CACR,sCAAsC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,iBAAiB,CAAC,UAAU;cACtF,kEAAkE,CACvE,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,eAAe,KAAK,QAAQ,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CACR,MAAM,CAAC,iBAAiB;YACtB,CAAC,CAAC,yCAAyC,MAAM,CAAC,iBAAiB,GAAG;YACtE,CAAC,CAAC,6BAA6B,CAClC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC"}
@@ -647,11 +647,17 @@ are configured in **code** — there is no panel for them.
647
647
  …)\` for a gameplay action. The first strands phone players and the engine logs a
648
648
  \`[mobile-parity]\` error on game start; the raw listener strands them **silently** — the parity
649
649
  check cannot see a listener that never went through the engine.
650
+ - **\`bitmagic verify\` enforces this.** Every run reports the parity gaps, naming the key and the
651
+ system that registered it — a warning normally, and a **failure** on a game whose
652
+ \`primaryPlatform\` is \`mobile\`. \`bitmagic verify --platform mobile\` goes further and boots
653
+ the game as a phone: touch controls, phone viewport, the engine's real mobile branch.
650
654
  - **Say which platform the game is for.** \`"primaryPlatform": "mobile"\` at the top level of
651
655
  \`src/work/game.json\` is what bitmagic.ai catalogs it as, and \`bitmagic publish\` sends it —
652
- editing that file is the only way to set it. For a game that only works one way up, add
653
- \`"mobileOrientation": "portrait"\` (or \`"landscape"\`) to \`worldProfileData\` in
654
- \`src/work/world.json\`; published mobile builds lock to it.
656
+ editing that file is the only way to set it. It also decides what \`verify\` boots and \`judge\`
657
+ grades by default, so setting it is how a phone game gets checked as one. For a game that only
658
+ works one way up, add \`"mobileOrientation": "portrait"\` (or \`"landscape"\`) to
659
+ \`worldProfileData\` in \`src/work/world.json\`; published mobile builds lock to it, and the
660
+ mobile verify run turns the phone the same way.
655
661
 
656
662
  Details — button slots, icons, the joystick, the parity check: \`engine/agent-docs/control-system.md\`.
657
663
 
@@ -661,6 +667,7 @@ Details — button slots, icons, the joystick, the parity check: \`engine/agent-
661
667
  bitmagic check # typecheck against the vendored engine
662
668
  bitmagic reload # tell the open browser you have finished a round of edits
663
669
  bitmagic verify # boot the game in a real browser and report what broke
670
+ bitmagic verify --platform mobile # boot it as a phone: touch controls, phone viewport
664
671
  bitmagic judge # grade the verify screenshot against a quality rubric (paid, cheap)
665
672
  \`\`\`
666
673
 
@@ -751,7 +758,8 @@ bitmagic publish # verify-gate, build if needed, and ship to bitmagic.ai
751
758
  Follow this order; \`publish\` enforces it and refuses otherwise:
752
759
 
753
760
  1. Run \`bitmagic verify\` and get it passing. It writes \`.bitmagic/verify/result.json\`,
754
- \`.bitmagic/verify/screenshot.png\` and \`.bitmagic/verify/console.log\`.
761
+ \`.bitmagic/verify/screenshot.png\` and \`.bitmagic/verify/console.log\` — or the
762
+ \`.mobile\`-suffixed names when the run was a phone one.
755
763
 
756
764
  The publish thumbnail is \`.bitmagic/cover.webp\` when \`bitmagic cover\` has produced one, and
757
765
  that screenshot otherwise. The output names which one it used.
@@ -815,14 +823,16 @@ are developing, when rebuilding is cheap, rather than being blocked at the momen
815
823
  You can create game content from here — no web editor needed:
816
824
 
817
825
  \`\`\`
818
- bitmagic generate skybox --description "a stormy alien sky with two moons"
819
- bitmagic generate sound --prompt "heavy wooden door slamming" [--duration 3] [--loop]
820
- bitmagic generate image --name hero-portrait --prompt "..." [--width 512] [--height 512]
821
- bitmagic generate character --name guard --prompt "..." [--apply-to-player]
822
- bitmagic generate animation --family gait --name LimpStride --based-on walk [--dial limpSide=right]
823
- bitmagic generate animation --spec clip.json # melee and oneShot clips: a spec file
824
- bitmagic generate model --prompt "a mossy stone arch" [--height 3]
825
- bitmagic generate vehicle --prompt "a rusty orange pickup" | --preset VAN
826
+ bitmagic generate skybox --description "a stormy alien sky with two moons"
827
+ bitmagic generate background --description "layered pine forest at dusk"
828
+ bitmagic generate block-type --name mossy_brick --description "damp green moss on red brick"
829
+ bitmagic generate sound --prompt "heavy wooden door slamming" [--duration 3] [--loop]
830
+ bitmagic generate image --name hero-portrait --prompt "..." [--width 512] [--height 512]
831
+ bitmagic generate character --name guard --prompt "..." [--apply-to-player]
832
+ bitmagic generate animation --family gait --name LimpStride --based-on walk [--dial limpSide=right]
833
+ bitmagic generate animation --spec clip.json # melee and oneShot clips: a spec file
834
+ bitmagic generate model --prompt "a mossy stone arch" [--height 3]
835
+ bitmagic generate vehicle --prompt "a rusty orange pickup" | --preset VAN
826
836
  \`\`\`
827
837
 
828
838
  Run \`bitmagic generate <type> --help\` for the full flags of any one.
@@ -843,9 +853,8 @@ Four things worth knowing before you use these:
843
853
  the write settles; \`bitmagic reload\` forces it if you want it now.
844
854
 
845
855
  Three lanes make a 3D object, and the wrong one wastes a generation: \`generate model\` mints a NEW
846
- asset from a prompt, \`generate prop --asset <id>\` UPGRADES a placeholder (every instance changes
847
- with it), \`assets add <file>\` uploads one you already have. **Block types** are the one thing no
848
- subcommand makes; never hand-edit \`world.json\` to point at an asset you have not made.
856
+ asset, \`generate prop --asset <id>\` UPGRADES a placeholder, \`assets add <file>\` uploads your own.
857
+ Splats, video and \`.vox\`/\`.fbx\` still need the web editor; never reference an asset you did not make.
849
858
 
850
859
  ## Forging a whole level
851
860
 
@@ -1004,7 +1013,8 @@ that I would otherwise have defaulted differently.
1004
1013
 
1005
1014
  ## Platform
1006
1015
  \`primaryPlatform\` in \`game.json\` — \`desktop\` or \`mobile\`. Plus \`worldProfileData.mobileOrientation\`
1007
- in \`world.json\` when the game only works one way up.
1016
+ in \`world.json\` when the game only works one way up. If \`mobile\`: every action needs a touch
1017
+ button, and \`bitmagic verify\` fails the run otherwise.
1008
1018
 
1009
1019
  ## Slice 1 — the smallest playable thing
1010
1020
  - Core verb: the ONE thing the player does
@@ -1203,7 +1213,7 @@ These docs are shared with Bitmagic's hosted agent, so two things read oddly her
1203
1213
  |---|---|
1204
1214
  | \`configure_game\`, \`manage-achievements\`, \`manage-levels\`, \`spawnpoint upsert\` | edit \`src/work/world.json\` directly |
1205
1215
  | \`node bin/voxel.mjs\` / \`bin/world-edit.mjs\` | edit \`src/work/world.json\` directly |
1206
- | \`generate_character\`, \`design-vehicle\`, skybox/sound/image/animation generation | \`bitmagic generate <type>\` |
1216
+ | \`generate_character\`, \`design-vehicle\`, \`generateBlockTypeTool\`, \`backgroundImageGenerationTool\`, skybox/sound/image/animation generation | \`bitmagic generate <type>\` |
1207
1217
  | \`runWorldForger\` / \`run-world-forger\` | \`bitmagic forge\` |
1208
1218
 
1209
1219
  Everything else in the doc — the engine APIs, the world.json shape, the gotchas — still applies.
@@ -1254,16 +1264,18 @@ you work. Re-read it before editing it.
1254
1264
  export function renderGenerateSkill() {
1255
1265
  return `---
1256
1266
  name: generating-assets
1257
- description: Use when this Bitmagic game needs a new skybox, sound effect, image, character, animation or drivable vehicle, or when the creator has a model, sound, image or .vxl file of their own to add — anything that would otherwise require the web editor.
1267
+ description: Use when this Bitmagic game needs a new skybox, 2D background, custom block type, sound effect, image, character, animation or drivable vehicle, or when the creator has a model, sound, image or .vxl file of their own to add — anything that would otherwise require the web editor.
1258
1268
  ---
1259
1269
 
1260
1270
  # Generating game assets
1261
1271
 
1262
- The \`bitmagic\` CLI generates content directly into this project. Seven types work today:
1272
+ The \`bitmagic\` CLI generates content directly into this project. Nine types work today:
1263
1273
 
1264
1274
  | Command | Makes | Lands in |
1265
1275
  |---|---|---|
1266
1276
  | \`bitmagic generate skybox --description "..."\` | a 360° environment | \`worldProfileData.skyboxUrl\` |
1277
+ | \`bitmagic generate background --description "..."\` | a flat 2D backdrop | \`worldProfileData.skyboxUrl\` |
1278
+ | \`bitmagic generate block-type --name <id> --description "..."\` | a custom voxel block | \`worldProfileData.customBlockTypes[]\` |
1267
1279
  | \`bitmagic generate sound --prompt "..."\` | a sound effect | \`assets[]\` |
1268
1280
  | \`bitmagic generate image --name <id> --prompt "..."\` | a 2D image | \`assets[]\` |
1269
1281
  | \`bitmagic generate character --name <id> --prompt "..."\` | a rigged character | \`assets[]\` |
@@ -1297,6 +1309,21 @@ engine.getVehicleSpawner().spawnAndEnterFromAsset({ x: 0, z: 0 }, '<assetName>',
1297
1309
 
1298
1310
  Headlights, taillights and beacons are built and lit automatically — never ask for them.
1299
1311
 
1312
+ \`background\` and \`skybox\` write the **same field**. Pick one: \`skybox\` for a 360° panorama a
1313
+ free-look camera can turn in, \`background\` for a flat layered backdrop behind a fixed sidescroller
1314
+ or top-down camera. The engine wraps either onto the sky sphere, so a flat backdrop reads as a
1315
+ distant painted horizon rather than a full-screen image — and running one after the other replaces
1316
+ the first, silently.
1317
+
1318
+ \`block-type\` registers a block **before** anything can use it: \`--name\` is what
1319
+ \`create-voxel-asset\`, \`groundConfig.surfaceBlockType\` and building code refer to, and an
1320
+ unrecognised name falls back to flat vertex colour instead of failing. Names are lowercase
1321
+ snake_case, are compared case-insensitively, and cannot shadow a built-in (\`wood\`, \`leaves\`,
1322
+ \`stone\`, \`grass\`, \`sand\`, \`dirt\`, \`ice\`, \`asphalt\`, \`water\`, \`lava\`, \`marble\`, or the
1323
+ aliases \`trunk\`, \`foliage\`, \`rock\`, \`road\`). Re-running with the same \`--name\` **replaces**
1324
+ the entry rather than adding a second one. \`--side-description\` gives the side faces their own
1325
+ texture and generates a second one.
1326
+
1300
1327
  Cover art is the one generation that is **not** a \`generate\` subcommand:
1301
1328
 
1302
1329
  | Command | Makes | Lands in |
@@ -1352,7 +1379,8 @@ assets list\` shows what is there; \`bitmagic assets remove <id>\` takes one out
1352
1379
 
1353
1380
  ## What is not available
1354
1381
 
1355
- **Block types** are the one thing no subcommand makes those still need the web editor. Do not
1382
+ Gaussian splats, video, and the import formats \`assets add\` refuses (\`.gltf\`, \`.vox\`, \`.qb\`,
1383
+ \`.fbx\`, \`.spz\`, \`.ply\`, \`.sog\`, \`.mp4\`) still need the web editor's Assets tab. Do not
1356
1384
  hand-edit \`world.json\` to reference an asset you have neither generated nor added; the game will
1357
1385
  fail to load it.
1358
1386
  `;
@@ -1449,6 +1477,7 @@ description: Use after finishing a milestone — a mechanics-plan slice shipped,
1449
1477
  bitmagic verify # first: captures the screenshot the judge grades (no --fast)
1450
1478
  bitmagic judge # one vision-model call; prints a scorecard, exits 0
1451
1479
  bitmagic judge --genre platformer # sharpen the rubric: platformer, racing, shooter, combat, driving, tower-defense
1480
+ bitmagic judge --platform mobile # grade the phone frame: touch-HUD legibility, thumb reach, portrait framing
1452
1481
  bitmagic judge --min-score 7 # exit 4 when the overall score is below the bar
1453
1482
  bitmagic judge --json # the scorecard as one JSON document on stdout
1454
1483
  \`\`\`
@@ -1459,6 +1488,10 @@ model and prints back four dimension scores (visual-quality, readability, design
1459
1488
  and at most three findings — each naming what to change, most severe first. The scorecard is also
1460
1489
  written to \`.bitmagic/judge.json\`.
1461
1490
 
1491
+ A mobile-primary game is graded on its phone frame by default — the one
1492
+ \`bitmagic verify --platform mobile\` captured — with the rubric re-pointed at what a phone player
1493
+ sees. Pass \`--platform desktop\` to grade the other one.
1494
+
1462
1495
  It is paid (sparks, roughly the cost of one small model call) and deliberate — which is why it is
1463
1496
  a command you run, not something verify does for you.
1464
1497
 
@@ -1541,7 +1574,7 @@ are lost on the next upgrade.
1541
1574
  |---|---|
1542
1575
  | \`planning-a-game/\` | starting a game, a genre, or anything touching more than one gameplay feature — scoping the first playable slice and writing \`mechanics-plan.md\` |
1543
1576
  | \`finding-engine-apis/\` | you need an engine API and do not know its name or signature, or \`bitmagic check\` cannot resolve a symbol |
1544
- | \`generating-assets/\` | the game needs a skybox, sound, image, character, animation or vehicle |
1577
+ | \`generating-assets/\` | the game needs a skybox, a background, a custom block type, sound, image, character, animation or vehicle |
1545
1578
  | \`checking-usage/\` | asked what sparks were spent on, how much Pro allowance is left, or whether there is enough left to keep generating |
1546
1579
  | \`judging-quality/\` | a milestone is done and the game deserves an independent quality verdict — \`bitmagic judge\` |
1547
1580
 
@@ -1 +1 @@
1
- {"version":3,"file":"project-files.js","sourceRoot":"","sources":["../../src/scaffold/project-files.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project-files.js","sourceRoot":"","sources":["../../src/scaffold/project-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { VerifyPlatform } from './platform.js';
2
+ export interface VerifyArtifactPaths {
3
+ /** The publish thumbnail candidate on desktop. Not written by a `--fast` run. */
4
+ screenshotPath: string;
5
+ consolePath: string;
6
+ /** Where the automatic software-GL re-run's console goes, so the first run's evidence survives. */
7
+ retryConsolePath: string;
8
+ }
9
+ export declare function verifyArtifactPaths(artifactDir: string, platform: VerifyPlatform): VerifyArtifactPaths;
10
+ export declare const VERIFY_PLATFORMS: VerifyPlatform[];
11
+ /**
12
+ * Every screenshot any platform could write.
13
+ *
14
+ * A run deletes the ones it is not going to produce, extending the rule `--fast` already follows:
15
+ * a stale screenshot beside a fresh `result.json` is worse than none, because publish would ship
16
+ * a picture of some earlier state of the game — and after this change "earlier state" includes
17
+ * "the other platform, from an older run".
18
+ */
19
+ export declare function allVerifyScreenshotPaths(artifactDir: string): string[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What a verify run writes, per platform.
3
+ *
4
+ * One place, because a `both` run writes two of everything and the desktop names are a contract
5
+ * with two other commands: `bitmagic publish` reads `screenshot.png` as its thumbnail fallback
6
+ * and `bitmagic judge` grades it. Desktop therefore keeps exactly the names it has always had,
7
+ * and mobile infixes `.mobile` before the extension.
8
+ *
9
+ * A per-platform CONSOLE log is not cosmetic. Without it a `both` run would overwrite the desktop
10
+ * console — by far the most useful artifact when something breaks — with the mobile one, and the
11
+ * run would look like it only ever booted a phone.
12
+ */
13
+ import * as path from 'path';
14
+ export function verifyArtifactPaths(artifactDir, platform) {
15
+ const suffix = platform === 'mobile' ? '.mobile' : '';
16
+ return {
17
+ screenshotPath: path.join(artifactDir, `screenshot${suffix}.png`),
18
+ consolePath: path.join(artifactDir, `console${suffix}.log`),
19
+ retryConsolePath: path.join(artifactDir, `console.retry${suffix}.log`),
20
+ };
21
+ }
22
+ export const VERIFY_PLATFORMS = ['desktop', 'mobile'];
23
+ /**
24
+ * Every screenshot any platform could write.
25
+ *
26
+ * A run deletes the ones it is not going to produce, extending the rule `--fast` already follows:
27
+ * a stale screenshot beside a fresh `result.json` is worse than none, because publish would ship
28
+ * a picture of some earlier state of the game — and after this change "earlier state" includes
29
+ * "the other platform, from an older run".
30
+ */
31
+ export function allVerifyScreenshotPaths(artifactDir) {
32
+ return VERIFY_PLATFORMS.map((platform) => verifyArtifactPaths(artifactDir, platform).screenshotPath);
33
+ }
34
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../src/verify/artifacts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAW7B,MAAM,UAAU,mBAAmB,CACjC,WAAmB,EACnB,QAAwB;IAExB,MAAM,MAAM,GAAG,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,OAAO;QACL,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,MAAM,MAAM,CAAC;QACjE,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,MAAM,MAAM,CAAC;QAC3D,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,gBAAgB,MAAM,MAAM,CAAC;KACvE,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAqB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAExE;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,WAAmB;IAC1D,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,mBAAmB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,cAAc,CAAC,CAAC;AACvG,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { type BrowserLaunchPlan, type RendererChoice } from '../browser-gl.js';
2
2
  import { type VerifyObservations } from './classify.js';
3
+ import type { VerifyDeviceOptions } from './device.js';
3
4
  export interface StartInteractionOptions {
4
5
  /** Selector for the engine Play button. Its label is localizable and per-game overridable, so
5
6
  * never match by text. */
@@ -56,6 +57,15 @@ export interface HeadlessCheckOptions {
56
57
  * `browserGlArgs(process.env)`. `--mute-audio` is always added here, not by callers.
57
58
  */
58
59
  launch?: BrowserLaunchPlan;
60
+ /**
61
+ * Emulate a phone for this check — viewport, screen, deviceScaleFactor, `isMobile` and
62
+ * `hasTouch` from `verify/device.ts`. Absent means the desktop 1280x720 context, byte-identical
63
+ * to what every run did before this existed, which is what `publish`'s smoke check keeps using.
64
+ *
65
+ * Pair it with a `?platform=mobile` URL: this side makes the BROWSER a phone, that side makes
66
+ * the ENGINE take its mobile branch, and only both together verify what a player gets.
67
+ */
68
+ device?: VerifyDeviceOptions;
59
69
  /**
60
70
  * Capture the screenshot and measure its pixels (default true). `bitmagic verify --fast`
61
71
  * turns this off for the inner loop: `screenshotBytes`/`canvasFrame` come back null and
@@ -3,6 +3,7 @@ import { chromium } from 'playwright-core';
3
3
  import { browserGlArgs } from '../browser-gl.js';
4
4
  import { CliError } from '../errors.js';
5
5
  import { RESPAWN_LINE, } from './classify.js';
6
+ import { parseMobileParityGap } from './mobile-parity.js';
6
7
  import { DEFAULT_SETTLE_CONFIG, SETTLE_POLL_MS, isFreefalling, settleVerdict, } from './settle.js';
7
8
  import { deriveEventsObservation } from './events.js';
8
9
  export const DEFAULT_START_INTERACTION = {
@@ -66,7 +67,8 @@ export class VerifyBrowserSession {
66
67
  // browser phase throws (e.g. page.goto timing out) — artifacts are the point of a failing run.
67
68
  const consoleLines = [];
68
69
  const pageErrors = [];
69
- const context = await this.browser.newContext({ viewport: { width: 1280, height: 720 } });
70
+ const contextOptions = options.device ?? DESKTOP_CONTEXT_OPTIONS;
71
+ const context = await this.browser.newContext(contextOptions);
70
72
  try {
71
73
  const page = await context.newPage();
72
74
  await armContextLossProbe(page);
@@ -82,7 +84,7 @@ export class VerifyBrowserSession {
82
84
  // worth having. The console log (written in `finally` below) is the useful artifact here.
83
85
  if (options.captureScreenshot !== false) {
84
86
  try {
85
- fs.writeFileSync(options.screenshotPath, await page.screenshot());
87
+ fs.writeFileSync(options.screenshotPath, await page.screenshot(SCREENSHOT_OPTIONS));
86
88
  }
87
89
  catch {
88
90
  // Page may already be unusable (closed context, crashed renderer) — nothing to save.
@@ -93,7 +95,7 @@ export class VerifyBrowserSession {
93
95
  }
94
96
  const interaction = options.startInteraction === undefined
95
97
  ? null
96
- : await performStartInteraction(page, consoleLines, options.startInteraction);
98
+ : await performStartInteraction(page, consoleLines, options.startInteraction, options.device?.hasTouch === true);
97
99
  // Verify settles adaptively — a game that provably reached stable gameplay is observed
98
100
  // after a few seconds instead of the full fixed window, with `settleMs` as the hard cap.
99
101
  // The smoke check keeps the fixed wait: its game lives in an iframe this probe does not
@@ -142,7 +144,7 @@ export class VerifyBrowserSession {
142
144
  let screenshotBytes = null;
143
145
  let canvasFrame = null;
144
146
  if (options.captureScreenshot !== false) {
145
- const screenshotBuffer = await page.screenshot();
147
+ const screenshotBuffer = await page.screenshot(SCREENSHOT_OPTIONS);
146
148
  fs.writeFileSync(options.screenshotPath, screenshotBuffer);
147
149
  screenshotBytes = screenshotBuffer.length;
148
150
  canvasFrame = canvas === null
@@ -176,6 +178,9 @@ export class VerifyBrowserSession {
176
178
  menuVisibleAfterSettle: await isMenuVisible(page),
177
179
  };
178
180
  }
181
+ // Only meaningful on a run that emulated a phone: on desktop the engine never builds
182
+ // MobileControls at all, so zero affordances there is the correct state, not a finding.
183
+ const touchControls = options.device === undefined ? undefined : await countTouchAffordances(page);
179
184
  return {
180
185
  consoleLines,
181
186
  pageErrors,
@@ -188,6 +193,13 @@ export class VerifyBrowserSession {
188
193
  snapshot,
189
194
  settle,
190
195
  events,
196
+ touchControls,
197
+ viewport: contextOptions.viewport,
198
+ // From the console this run already captured — the engine logs the check on every game
199
+ // start, desktop included (see verify/mobile-parity.ts). Behind the same gate as the
200
+ // snapshot and event probes: the check only runs at game start, so a run that never
201
+ // starts one has observed nothing, and `publish`'s smoke check keeps its exact shape.
202
+ mobileParity: engineTarget === null ? undefined : parseMobileParityGap(consoleLines),
191
203
  };
192
204
  }
193
205
  finally {
@@ -199,6 +211,19 @@ export class VerifyBrowserSession {
199
211
  }
200
212
  }
201
213
  }
214
+ /**
215
+ * Every capture, on every platform.
216
+ *
217
+ * `scale: 'css'` rather than Playwright's default `'device'` because a phone context runs at
218
+ * deviceScaleFactor 3, and `measureCanvasFrame` below samples the captured image using the
219
+ * canvas's CSS-pixel rect. At device scale those coordinates would be off by 3x and the
220
+ * black-frame check would measure the wrong region of the image entirely. CSS scale keeps that
221
+ * invariant true on both paths — at DSF 1 the two are identical, so the desktop artifact is
222
+ * unchanged — and keeps the mobile PNG small enough for `bitmagic judge`'s image limit.
223
+ */
224
+ const SCREENSHOT_OPTIONS = { scale: 'css' };
225
+ /** The context every run used before device emulation existed, and still the default. */
226
+ const DESKTOP_CONTEXT_OPTIONS = { viewport: { width: 1280, height: 720 } };
202
227
  /** Window counter the init script below increments on every rAF tick. */
203
228
  const FRAME_COUNT_FLAG = '__bitmagicFrameCount';
204
229
  /**
@@ -505,8 +530,10 @@ async function measureCanvasFrame(page, screenshot, rect) {
505
530
  const image = new Image();
506
531
  image.src = input.dataUrl;
507
532
  await image.decode();
508
- // The screenshot is a viewport capture at deviceScaleFactor 1, so its pixels are the
509
- // page's CSS pixels; clamp to the image in case the canvas overhangs the viewport.
533
+ // The screenshot is a viewport capture taken at CSS scale (`SCREENSHOT_OPTIONS`), so its
534
+ // pixels are the page's CSS pixels whatever the device pixel ratio which is what lets
535
+ // this rect, read from getBoundingClientRect, index straight into the image on a phone
536
+ // context at deviceScaleFactor 3. Clamp in case the canvas overhangs the viewport.
510
537
  const left = Math.max(0, input.left);
511
538
  const top = Math.max(0, input.top);
512
539
  const width = Math.min(input.width, image.naturalWidth - left);
@@ -563,7 +590,8 @@ function hasStartMarker(consoleLines, marker) {
563
590
  return consoleLines.some((line) => line.includes(marker));
564
591
  }
565
592
  /**
566
- * Start gameplay the way a player does: wait for the engine Play button and click it. The button
593
+ * Start gameplay the way a player does: wait for the engine Play button and press it a click on
594
+ * the desktop run, a tap on the mobile one (see `tap` below). The button
567
595
  * only becomes visible once the world has finished loading, so "button appeared" doubles as a
568
596
  * load-complete signal. Games without the button (auto-starting genres, custom start UIs) are
569
597
  * accepted through the start marker instead.
@@ -572,7 +600,17 @@ function hasStartMarker(consoleLines, marker) {
572
600
  * failing game still leaves its menu screenshot and console log as artifacts, and classification
573
601
  * decides pass/fail from what is recorded here.
574
602
  */
575
- async function performStartInteraction(page, consoleLines, options) {
603
+ async function performStartInteraction(page, consoleLines, options,
604
+ /**
605
+ * Press Play with a touch tap instead of a mouse click.
606
+ *
607
+ * Derived from the context's own `hasTouch` rather than passed as its own flag, so the
608
+ * mismatch is unrepresentable: `tap()` throws outright on a context without touch. The mobile
609
+ * run must press Play the way its player does — the StartScreen binds `touchend` as well as
610
+ * `click`, and mobile gameplay start reads the transient user activation a real gesture leaves
611
+ * behind before it asks for fullscreen.
612
+ */
613
+ tap) {
576
614
  const button = page.locator(options.playButtonSelector).first();
577
615
  let playButtonSeen = false;
578
616
  let playButtonClicked = false;
@@ -592,7 +630,7 @@ async function performStartInteraction(page, consoleLines, options) {
592
630
  }
593
631
  if (playButtonSeen) {
594
632
  try {
595
- await button.click({ timeout: 5_000 });
633
+ await (tap ? button.tap({ timeout: 5_000 }) : button.click({ timeout: 5_000 }));
596
634
  playButtonClicked = true;
597
635
  }
598
636
  catch {
@@ -606,6 +644,38 @@ async function performStartInteraction(page, consoleLines, options) {
606
644
  }
607
645
  return { playButtonSeen, playButtonClicked };
608
646
  }
647
+ /**
648
+ * How many touch affordances the engine's MobileControls actually put on screen.
649
+ *
650
+ * The direct evidence that the mobile branch ran: `MobileControls` builds its UI only when
651
+ * `isMobileRuntime()` is true, so a phone-emulated run that ends with none of these elements
652
+ * either never took the branch or has no touch bindings at all. Matched by the engine's own
653
+ * stable class names rather than by structure, because the joystick and the action buttons are
654
+ * plain divs the game may restyle.
655
+ *
656
+ * Null on any failure, under the same "not observed is never a failure" contract as every other
657
+ * probe here.
658
+ */
659
+ async function countTouchAffordances(page) {
660
+ try {
661
+ return await page.evaluate(() => {
662
+ /* eslint-disable no-undef -- browser-side callback, see the canvas probe above. */
663
+ try {
664
+ return {
665
+ joysticks: document.querySelectorAll('.hud-mobile-joystick-outer').length,
666
+ buttons: document.querySelectorAll('.hud-mobile-button').length,
667
+ };
668
+ }
669
+ catch {
670
+ return null;
671
+ }
672
+ /* eslint-enable no-undef */
673
+ });
674
+ }
675
+ catch {
676
+ return null;
677
+ }
678
+ }
609
679
  /**
610
680
  * Whether the engine's start menu overlay is still displayed. After a successful start the
611
681
  * StartScreen fades out and sets `display: none`; a menu still up at the end of the run means