incanto 0.56.0 → 0.58.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 (100) hide show
  1. package/README.md +6 -4
  2. package/bin/incanto-check.mjs +27 -0
  3. package/bin/incanto-new.mjs +29 -7
  4. package/bin/incanto-playtest.mjs +28 -2
  5. package/bin/incanto-verify.mjs +96 -17
  6. package/bin/incanto.mjs +106 -0
  7. package/dist/2d.d.ts +59 -7
  8. package/dist/2d.js +3 -3
  9. package/dist/3d.d.ts +34 -4
  10. package/dist/3d.js +5 -5
  11. package/dist/{behavior-CyQoSu4n.d.ts → behavior-l08AEbq9.d.ts} +5 -0
  12. package/dist/{create-game-YvaNWhe6.js → create-game-BZwWJIns.js} +146 -7
  13. package/dist/{create-game-CqJkfMzm.js → create-game-D10bU5_J.js} +83 -78
  14. package/dist/debug.d.ts +1 -1
  15. package/dist/{duplicate-MNLMAcbz.js → duplicate-BPLZDZpd.js} +1 -1
  16. package/dist/{environment-presets-DLHqgNAF.js → environment-presets-CvvQr_bJ.js} +106 -24
  17. package/dist/{frame-report-njybhZon.js → frame-report-BSMny7oe.js} +1 -1
  18. package/dist/{frame-report-DZ70IY26.d.ts → frame-report-DCnHFmto.d.ts} +1 -1
  19. package/dist/{gameplay-C_SPzmUe.js → gameplay-BVphcxmE.js} +89 -13
  20. package/dist/gameplay.d.ts +26 -1
  21. package/dist/gameplay.js +1 -1
  22. package/dist/index.d.ts +45 -5
  23. package/dist/index.js +8 -9
  24. package/dist/{loader-DhI1jFW_.d.ts → loader-BbEMTuWg.d.ts} +1 -1
  25. package/dist/{loader-BTkHYrQn.js → loader-BcrRSjxB.js} +682 -682
  26. package/dist/net.d.ts +2 -2
  27. package/dist/net.js +1 -1
  28. package/dist/{pathfinding-CXGCpRQe.d.ts → pathfinding-mEN4V1CU.d.ts} +1 -1
  29. package/dist/{physics-2d-CfWAggJ1.js → physics-2d-EqA-jddf.js} +4 -3
  30. package/dist/{physics-3d-NzDwWPrF.js → physics-3d-Dnz4fsXX.js} +5 -4
  31. package/dist/quiet-rapier-BAJ4K94N.js +46 -0
  32. package/dist/react.d.ts +1 -1
  33. package/dist/react.js +1 -1
  34. package/dist/{register-en63AEZO.js → register-C6ZBFRjd.js} +63 -60
  35. package/dist/{register-p48lHE2o.js → register-Ch70uByv.js} +1615 -1502
  36. package/dist/{replay-ePMz26jw.d.ts → replay-Dw6gMlYA.d.ts} +1 -1
  37. package/dist/{replay-t1pP0gQg.js → replay-s7I2GstT.js} +31 -11
  38. package/dist/sheet-grid-BT6N_Bjs.js +59 -0
  39. package/dist/{split-screen-BsdOHbzP.d.ts → split-screen-B0baBwxI.d.ts} +5 -1
  40. package/dist/{split-screen-CSb_uZ6W.js → split-screen-DLsUrleX.js} +14 -3
  41. package/dist/{sprite-animation-7qvUxF6Z.js → sprite-animation-C0wXLBZJ.js} +8 -4
  42. package/dist/{src-51GA-9m-.js → src-C1J09Op6.js} +55 -2
  43. package/dist/{teardown-C7uVSJvx.js → teardown-Cs113S9F.js} +74 -2
  44. package/dist/{test-Bu6SPNsV.js → test-ZBga8kQ9.js} +60 -25
  45. package/dist/test.d.ts +5 -5
  46. package/dist/test.js +2 -2
  47. package/dist/{touch-BnMyy9tr.js → touch-DESwnpOc.js} +173 -1
  48. package/dist/vite.d.ts +1 -1
  49. package/dist/vite.js +3 -3
  50. package/editor/assets/{agent8-X0Nesj-k.js → agent8-BQQjE9UQ.js} +1 -1
  51. package/editor/assets/{debug-TXqu-ftX.js → debug-C9UCsXBS.js} +1 -1
  52. package/editor/assets/{index-DTdwKKIv.js → index-CBgfM3WD.js} +62 -62
  53. package/editor/index.html +1 -1
  54. package/package.json +2 -1
  55. package/skills/incanto-assets.md +5 -1
  56. package/skills/incanto-audio.md +7 -1
  57. package/skills/incanto-building-2d-games.md +13 -0
  58. package/skills/incanto-building-3d-games.md +3 -3
  59. package/skills/incanto-gameplay-behaviors.md +2 -1
  60. package/skills/incanto-hud.md +6 -0
  61. package/skills/incanto-performance.md +5 -0
  62. package/skills/incanto-physics-and-input.md +14 -0
  63. package/skills/incanto-playtesting.md +19 -5
  64. package/skills/incanto-save-slots.md +2 -1
  65. package/skills/incanto-verifying-your-game.md +15 -4
  66. package/templates-app/beacon-isle-3d/package.json +1 -1
  67. package/templates-app/beacon-isle-3d/src/game.scene.json +7 -6
  68. package/templates-app/platformer-2d/PROJECT/Context.md +70 -0
  69. package/templates-app/platformer-2d/PROJECT/Requirements.md +63 -0
  70. package/templates-app/platformer-2d/PROJECT/Status.md +60 -0
  71. package/templates-app/platformer-2d/PROJECT/Structure.md +77 -0
  72. package/templates-app/platformer-2d/docs/project-2d-rules.md +61 -0
  73. package/templates-app/platformer-2d/index.html +99 -0
  74. package/templates-app/platformer-2d/package.json +23 -0
  75. package/templates-app/platformer-2d/src/behaviors.ts +541 -0
  76. package/templates-app/platformer-2d/src/game.scene.json +2061 -0
  77. package/templates-app/platformer-2d/src/main.ts +68 -0
  78. package/templates-app/platformer-2d/tsconfig.json +13 -0
  79. package/templates-app/platformer-2d/verify.ts +275 -0
  80. package/templates-app/platformer-2d/vite.config.ts +12 -0
  81. package/templates-app/star-survivor/PROJECT/Context.md +55 -0
  82. package/templates-app/star-survivor/PROJECT/Requirements.md +47 -0
  83. package/templates-app/star-survivor/PROJECT/Status.md +44 -0
  84. package/templates-app/star-survivor/PROJECT/Structure.md +63 -0
  85. package/templates-app/star-survivor/docs/project-2d-rules.md +53 -0
  86. package/templates-app/star-survivor/index.html +232 -0
  87. package/templates-app/star-survivor/package.json +23 -0
  88. package/templates-app/star-survivor/src/behaviors.ts +624 -0
  89. package/templates-app/star-survivor/src/game.scene.json +464 -0
  90. package/templates-app/star-survivor/src/main.ts +49 -0
  91. package/templates-app/star-survivor/tsconfig.json +13 -0
  92. package/templates-app/star-survivor/verify.ts +193 -0
  93. package/templates-app/star-survivor/vite.config.ts +12 -0
  94. package/templates-app/tps-3d/package.json +1 -1
  95. package/templates-app/tps-3d/src/game.scene.json +6 -3
  96. package/templates-app/tps-3d/verify.ts +17 -1
  97. package/templates-app/village-quest-3d/package.json +1 -1
  98. package/templates-app/village-quest-3d/src/grove.scene.json +14 -13
  99. package/templates-app/village-quest-3d/src/village.scene.json +5 -5
  100. package/dist/log-report-CPFm4OXf.js +0 -173
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Headless proof that Hollow Horde actually PLAYS — the browserless half of the
3
+ * agent loop (author → VERIFY → fix). Drives the real `game.scene.json` through
4
+ * `incanto/test`'s `runScript` at a fixed, seeded timestep and asserts the survivor
5
+ * loop end to end:
6
+ *
7
+ * MOVE — holding `move` walks the player (top-down CharacterBody2D).
8
+ * ATTACK — the auto-attack fires bolts at a nearby enemy; the enemy DIES, which
9
+ * drops an XP shard (group 'xp') and tallies a kill (EnemyDrop).
10
+ * LEVEL — gaining XP raises the level (and headlessly auto-takes an upgrade).
11
+ * WIN — surviving the clock sets the score to scoreToWin → ScoreKeeper `won`.
12
+ * LOSE — draining the player's Health to 0 → `died` → loseLife → `lost`.
13
+ *
14
+ * Run with `bun verify.ts` (or `bun run verify`).
15
+ */
16
+ import type { Node } from 'incanto';
17
+ import { registerBehavior } from 'incanto';
18
+ import { auditScene, registerAllNodes, runScript } from 'incanto/test';
19
+ import { Director, EnemyDrop, EnemyFace, GridFloor, Survivor } from './src/behaviors';
20
+ import sceneJson from './src/game.scene.json';
21
+
22
+ const behaviors = { Survivor, Director, EnemyDrop, EnemyFace, GridFloor };
23
+
24
+ const ok = (label: string, cond: boolean): void => {
25
+ if (!cond) {
26
+ console.error(`FAIL: ${label}`);
27
+ process.exitCode = 1;
28
+ } else {
29
+ console.log(`pass: ${label}`);
30
+ }
31
+ };
32
+
33
+ // ---- AUDIT ---------------------------------------------------------------------------
34
+ // The check that fails on a WARNING, not just an error. Three of the five
35
+ // shipped templates had no audit block, which is how every one of them came to
36
+ // ship a scene whose saveable state had no uid to key it under: the save came
37
+ // back empty, the load reported no problem, and nothing here asked.
38
+ {
39
+ registerAllNodes();
40
+ for (const [name, ctor] of Object.entries(behaviors)) {
41
+ registerBehavior(name, ctor as never, { replace: true });
42
+ }
43
+ const warnings = auditScene(sceneJson as unknown as Parameters<typeof auditScene>[0]);
44
+ ok(`auditScene is clean (${warnings.length} warnings)`, warnings.length === 0);
45
+ for (const w of warnings) console.error(` warn: ${w}`);
46
+ }
47
+
48
+ interface Vec {
49
+ position: number[];
50
+ }
51
+ interface SurvivorB {
52
+ kills: number;
53
+ level: number;
54
+ elapsed: number;
55
+ addXp(n: number): void;
56
+ }
57
+ interface HealthB {
58
+ kill(): void;
59
+ }
60
+
61
+ // ---- MOVE --------------------------------------------------------------------
62
+ {
63
+ let startX = 0;
64
+ let moved = false;
65
+ await runScript(sceneJson, {
66
+ durationMs: 1200,
67
+ seed: 1,
68
+ behaviors,
69
+ steps: [
70
+ {
71
+ atMs: 100,
72
+ do: (ctx) => {
73
+ startX = (ctx.getNode('Player') as unknown as Vec).position[0] ?? 0;
74
+ },
75
+ },
76
+ { atMs: 150, vector: ['move', 1, 0] },
77
+ {
78
+ atMs: 900,
79
+ label: 'holding move walked the player',
80
+ do: (ctx) => {
81
+ moved = ((ctx.getNode('Player') as unknown as Vec).position[0] ?? 0) > startX + 40;
82
+ },
83
+ },
84
+ ],
85
+ });
86
+ ok('holding move walked the player', moved);
87
+ }
88
+
89
+ // ---- AUTO-ATTACK kills an enemy → XP drop + kill tally ------------------------
90
+ {
91
+ let kills = 0;
92
+ let xpShards = 0;
93
+ const result = await runScript(sceneJson, {
94
+ durationMs: 5000,
95
+ seed: 2,
96
+ behaviors,
97
+ steps: [
98
+ {
99
+ atMs: 300,
100
+ label: 'spawn a goblin in attack range so the auto-attack cuts it down',
101
+ do: (ctx) => {
102
+ const player = ctx.getNode('Player') as unknown as Vec & { behavior: SurvivorB };
103
+ // turn the magnet off so the dropped shard lingers in the world for the assert
104
+ (player.behavior as unknown as { magnet: number }).magnet = 0;
105
+ const spawner = ctx.getNode('GoblinSpawner').behavior as unknown as {
106
+ spawn(): Node | null;
107
+ };
108
+ const enemy = spawner.spawn() as unknown as Vec | null;
109
+ if (enemy) enemy.position = [(player.position[0] ?? 0) + 200, player.position[1] ?? 0];
110
+ for (let i = 0; i < 260; i++) {
111
+ if ((player.behavior as unknown as SurvivorB).kills >= 1) break;
112
+ ctx.engine.step();
113
+ }
114
+ kills = (player.behavior as unknown as SurvivorB).kills;
115
+ xpShards = ctx.scene.tree.getNodesInGroup('xp').length;
116
+ },
117
+ },
118
+ ],
119
+ });
120
+ ok('the auto-attack killed an enemy', kills >= 1);
121
+ ok('a dead enemy dropped an XP shard', xpShards >= 1);
122
+ ok('runScript reported no failures (attack path)', result.ok);
123
+ }
124
+
125
+ // ---- XP → LEVEL UP -----------------------------------------------------------
126
+ {
127
+ let leveled = false;
128
+ await runScript(sceneJson, {
129
+ durationMs: 800,
130
+ seed: 3,
131
+ behaviors,
132
+ steps: [
133
+ {
134
+ atMs: 200,
135
+ label: 'gaining XP raises the level',
136
+ do: (ctx) => {
137
+ const s = ctx.getNode('Player').behavior as unknown as SurvivorB;
138
+ const before = s.level;
139
+ s.addXp(60);
140
+ leveled = s.level > before;
141
+ },
142
+ },
143
+ ],
144
+ });
145
+ ok('gaining XP raised the level', leveled);
146
+ }
147
+
148
+ // ---- WIN (survive the clock) -------------------------------------------------
149
+ {
150
+ let won = 0;
151
+ await runScript(sceneJson, {
152
+ durationMs: 1200,
153
+ seed: 4,
154
+ behaviors,
155
+ steps: [
156
+ { atMs: 100, do: (ctx) => ctx.scene.root.on('won', () => won++) },
157
+ {
158
+ atMs: 300,
159
+ label: 'fast-forward the survive clock → WIN',
160
+ do: (ctx) => {
161
+ const s = ctx.getNode('Player').behavior as unknown as SurvivorB;
162
+ s.elapsed = 89.9;
163
+ for (let i = 0; i < 12 && won === 0; i++) ctx.engine.step();
164
+ },
165
+ },
166
+ ],
167
+ });
168
+ ok('surviving the clock emitted `won`', won === 1);
169
+ }
170
+
171
+ // ---- LOSE (HP to 0) ----------------------------------------------------------
172
+ {
173
+ let lost = 0;
174
+ await runScript(sceneJson, {
175
+ durationMs: 1200,
176
+ seed: 5,
177
+ behaviors,
178
+ steps: [
179
+ { atMs: 100, do: (ctx) => ctx.scene.root.on('lost', () => lost++) },
180
+ {
181
+ atMs: 300,
182
+ label: 'drain the player to 0 HP → died → loseLife → lost',
183
+ do: (ctx) => {
184
+ (ctx.getNode('Player/Vitals').behavior as unknown as HealthB).kill();
185
+ for (let i = 0; i < 6 && lost === 0; i++) ctx.engine.step();
186
+ },
187
+ },
188
+ ],
189
+ });
190
+ ok('player death lost the game (`lost`)', lost === 1);
191
+ }
192
+
193
+ console.log(process.exitCode ? '\nVERIFY FAILED' : '\nVERIFY OK — Hollow Horde plays end to end');
@@ -0,0 +1,12 @@
1
+ import { incantoLibrary, incantoScenes } from 'incanto/vite';
2
+ import { defineConfig } from 'vite';
3
+
4
+ export default defineConfig({
5
+ base: './',
6
+ // Dev-server only, and both are why the editor is useful here:
7
+ // - incantoScenes(): validates every *.scene.json the moment you save it,
8
+ // AND serves this project's scenes, so the editor (☰ debug ▸ edit this
9
+ // scene ▸ scenes) can open, create and save any scene in the project.
10
+ // - incantoLibrary(): the agent8 asset catalog behind the 📚 buttons.
11
+ plugins: [incantoScenes(), incantoLibrary()],
12
+ });
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.56.0",
16
+ "incanto": "^0.58.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -499,7 +499,8 @@
499
499
  }
500
500
  ]
501
501
  }
502
- ]
502
+ ],
503
+ "uid": "n_nx7dsn65lts2arru"
503
504
  },
504
505
  {
505
506
  "name": "Camera",
@@ -647,7 +648,8 @@
647
648
  "pitch": 0.6
648
649
  }
649
650
  }
650
- ]
651
+ ],
652
+ "uid": "n_17sotvbult7pgelw"
651
653
  }
652
654
  ]
653
655
  },
@@ -705,7 +707,8 @@
705
707
  }
706
708
  ]
707
709
  }
708
- ]
710
+ ],
711
+ "uid": "n_h46qaejxdqadqc1c"
709
712
  },
710
713
  "connections": [
711
714
  {
@@ -17,7 +17,8 @@
17
17
  * headlessly, so this isolates the gameplay logic.
18
18
  */
19
19
  import type { Node } from 'incanto';
20
- import { runScript } from 'incanto/test';
20
+ import { registerBehavior } from 'incanto';
21
+ import { auditScene, registerAllNodes, runScript } from 'incanto/test';
21
22
  import { HudUpdater, Shoot } from './src/behaviors';
22
23
  import sceneJson from './src/game.scene.json';
23
24
 
@@ -45,6 +46,21 @@ const ok = (label: string, cond: boolean): void => {
45
46
  }
46
47
  };
47
48
 
49
+ // ---- AUDIT ---------------------------------------------------------------------------
50
+ // The check that fails on a WARNING, not just an error. Three of the five
51
+ // shipped templates had no audit block, which is how every one of them came to
52
+ // ship a scene whose saveable state had no uid to key it under: the save came
53
+ // back empty, the load reported no problem, and nothing here asked.
54
+ {
55
+ registerAllNodes();
56
+ for (const [name, ctor] of Object.entries(behaviors)) {
57
+ registerBehavior(name, ctor as never, { replace: true });
58
+ }
59
+ const warnings = auditScene(sceneJson as unknown as Parameters<typeof auditScene>[0]);
60
+ ok(`auditScene is clean (${warnings.length} warnings)`, warnings.length === 0);
61
+ for (const w of warnings) console.error(` warn: ${w}`);
62
+ }
63
+
48
64
  // ---- MOVE + WIN path ----------------------------------------------------------
49
65
  {
50
66
  let won = 0;
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.56.0",
16
+ "incanto": "^0.58.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -18,7 +18,7 @@
18
18
  "shadows": {
19
19
  "mapSize": 2048
20
20
  },
21
- "exposure": 1.0,
21
+ "exposure": 1,
22
22
  "ambient": {
23
23
  "color": "#a8b4e0",
24
24
  "intensity": 0.95
@@ -260,7 +260,7 @@
260
260
  "props": {
261
261
  "mesh": "sphere",
262
262
  "size": [0.07, 0.07, 0.07],
263
- "position": [-0.1, 0.24, 1.0],
263
+ "position": [-0.1, 0.24, 1],
264
264
  "material": {
265
265
  "color": "#ff3030",
266
266
  "emissive": "#ff3030",
@@ -274,7 +274,7 @@
274
274
  "props": {
275
275
  "mesh": "sphere",
276
276
  "size": [0.07, 0.07, 0.07],
277
- "position": [0.1, 0.24, 1.0],
277
+ "position": [0.1, 0.24, 1],
278
278
  "material": {
279
279
  "color": "#ff3030",
280
280
  "emissive": "#ff3030",
@@ -377,7 +377,7 @@
377
377
  "props": {
378
378
  "mesh": "sphere",
379
379
  "size": [0.07, 0.07, 0.07],
380
- "position": [-0.1, 0.24, 1.0],
380
+ "position": [-0.1, 0.24, 1],
381
381
  "material": {
382
382
  "color": "#ff3030",
383
383
  "emissive": "#ff3030",
@@ -391,7 +391,7 @@
391
391
  "props": {
392
392
  "mesh": "sphere",
393
393
  "size": [0.07, 0.07, 0.07],
394
- "position": [0.1, 0.24, 1.0],
394
+ "position": [0.1, 0.24, 1],
395
395
  "material": {
396
396
  "color": "#ff3030",
397
397
  "emissive": "#ff3030",
@@ -494,7 +494,7 @@
494
494
  "props": {
495
495
  "mesh": "sphere",
496
496
  "size": [0.07, 0.07, 0.07],
497
- "position": [-0.1, 0.24, 1.0],
497
+ "position": [-0.1, 0.24, 1],
498
498
  "material": {
499
499
  "color": "#ff3030",
500
500
  "emissive": "#ff3030",
@@ -508,7 +508,7 @@
508
508
  "props": {
509
509
  "mesh": "sphere",
510
510
  "size": [0.07, 0.07, 0.07],
511
- "position": [0.1, 0.24, 1.0],
511
+ "position": [0.1, 0.24, 1],
512
512
  "material": {
513
513
  "color": "#ff3030",
514
514
  "emissive": "#ff3030",
@@ -631,7 +631,7 @@
631
631
  "view": "free",
632
632
  "camDistance": 4.6,
633
633
  "eyeHeight": 1.1,
634
- "maxSpeed": 4.0,
634
+ "maxSpeed": 4,
635
635
  "sprintMultiplier": 1.8,
636
636
  "pitchMin": -0.5,
637
637
  "pitchMax": 1.1,
@@ -743,7 +743,8 @@
743
743
  "max": 100,
744
744
  "invulnerableFor": 0.8
745
745
  }
746
- }
746
+ },
747
+ "uid": "n_3hp4w1vicxhqr375"
747
748
  },
748
749
  {
749
750
  "name": "Camera",
@@ -768,7 +769,7 @@
768
769
  {
769
770
  "name": "Quest",
770
771
  "type": "UiText",
771
- "uid": "n_gq_quest",
772
+ "uid": "n_nmkf915b9r8d37nf",
772
773
  "props": {
773
774
  "text": "Wolves left: 3",
774
775
  "size": 16,
@@ -779,7 +780,7 @@
779
780
  {
780
781
  "name": "HP",
781
782
  "type": "UiBar",
782
- "uid": "n_gq_hp",
783
+ "uid": "n_kdm2vun7p0o3t4l2",
783
784
  "props": {
784
785
  "anchor": "topRight",
785
786
  "value": 100,
@@ -790,7 +791,7 @@
790
791
  {
791
792
  "name": "Banner",
792
793
  "type": "UiBanner",
793
- "uid": "n_gq_banner",
794
+ "uid": "n_21yz0pgnwqvz81yo",
794
795
  "props": {
795
796
  "anchor": "center"
796
797
  }
@@ -798,7 +799,7 @@
798
799
  {
799
800
  "name": "Hint",
800
801
  "type": "UiText",
801
- "uid": "n_gq_hint",
802
+ "uid": "n_dg8p7etf1n6lc495",
802
803
  "props": {
803
804
  "text": "click/F strike · Shift sprint · portal returns home",
804
805
  "size": 12,
@@ -3448,7 +3448,7 @@
3448
3448
  {
3449
3449
  "name": "Quest",
3450
3450
  "type": "UiText",
3451
- "uid": "n_vq_quest",
3451
+ "uid": "n_ouw4yqcp59yn0xwp",
3452
3452
  "props": {
3453
3453
  "text": "@t:quest.talkElder",
3454
3454
  "size": 16,
@@ -3459,7 +3459,7 @@
3459
3459
  {
3460
3460
  "name": "Banner",
3461
3461
  "type": "UiBanner",
3462
- "uid": "n_vq_banner",
3462
+ "uid": "n_rqkjlucsenuwggtc",
3463
3463
  "props": {
3464
3464
  "anchor": "center"
3465
3465
  }
@@ -3467,7 +3467,7 @@
3467
3467
  {
3468
3468
  "name": "Dialogue",
3469
3469
  "type": "UiDialogue",
3470
- "uid": "n_vq_dialogue",
3470
+ "uid": "n_ui4xf7paa0mgjzmh",
3471
3471
  "props": {
3472
3472
  "anchor": "bottom",
3473
3473
  "charsPerSecond": 45
@@ -3476,7 +3476,7 @@
3476
3476
  {
3477
3477
  "name": "RestartBtn",
3478
3478
  "type": "UiButton",
3479
- "uid": "n_vq_restart",
3479
+ "uid": "n_5eupths7miw4jilp",
3480
3480
  "props": {
3481
3481
  "text": "@t:ui.playAgain",
3482
3482
  "anchor": "bottom",
@@ -3486,7 +3486,7 @@
3486
3486
  {
3487
3487
  "name": "Hint",
3488
3488
  "type": "UiText",
3489
- "uid": "n_vq_hint",
3489
+ "uid": "n_aoy8da41ca8tq4mr",
3490
3490
  "props": {
3491
3491
  "text": "@t:ui.hint",
3492
3492
  "size": 12,
@@ -1,173 +0,0 @@
1
- //#region src/core/drive-script.ts
2
- const num = (raw) => {
3
- if (raw === void 0) return null;
4
- const n = Number(raw);
5
- return Number.isFinite(n) ? n : null;
6
- };
7
- /**
8
- * `vector move 0 1; step 2000` → steps, or an error naming the bad command.
9
- *
10
- * Semicolons and newlines both separate, so a whole plan fits in one shell
11
- * argument and a longer one fits in a file. `#` starts a comment.
12
- *
13
- * A command it does not know REFUSES THE WHOLE SCRIPT. Skipping it would leave
14
- * an agent looking at a frame that never moved, believing it had walked
15
- * somewhere — the failure mode this exists to prevent.
16
- */
17
- function parseDrive(text) {
18
- const steps = [];
19
- let totalMs = 0;
20
- const lines = text.split(/[;\n]/).map((l) => l.replace(/#.*$/, "").trim()).filter(Boolean);
21
- for (const line of lines) {
22
- const [word, ...rest] = line.split(/\s+/);
23
- const bad = (why) => ({
24
- steps: [],
25
- error: `${why}: "${line}"`,
26
- totalMs: 0
27
- });
28
- switch (word) {
29
- case "press":
30
- case "release": {
31
- const action = rest[0];
32
- if (!action) return bad(`${word} needs an action name`);
33
- steps.push(word === "press" ? {
34
- kind: "press",
35
- action
36
- } : {
37
- kind: "release",
38
- action
39
- });
40
- break;
41
- }
42
- case "vector": {
43
- const action = rest[0];
44
- const x = num(rest[1]);
45
- const y = num(rest[2]);
46
- if (!action || x === null || y === null) return bad("vector needs an action and two numbers");
47
- steps.push({
48
- kind: "vector",
49
- action,
50
- x,
51
- y
52
- });
53
- break;
54
- }
55
- case "key": {
56
- const code = rest[0];
57
- const dir = rest[1];
58
- if (!code || dir !== "down" && dir !== "up") return bad("key needs a code and down|up");
59
- steps.push({
60
- kind: "key",
61
- code,
62
- down: dir === "down"
63
- });
64
- break;
65
- }
66
- case "pointer": {
67
- const dx = num(rest[0]);
68
- const dy = num(rest[1]);
69
- if (dx === null || dy === null) return bad("pointer needs two numbers");
70
- steps.push({
71
- kind: "pointer",
72
- dx,
73
- dy
74
- });
75
- break;
76
- }
77
- case "step": {
78
- const ms = num(rest[0]);
79
- if (ms === null || ms < 0) return bad("step needs a duration in ms");
80
- steps.push({
81
- kind: "step",
82
- ms
83
- });
84
- totalMs += ms;
85
- break;
86
- }
87
- default: return bad("not a command");
88
- }
89
- }
90
- return {
91
- steps,
92
- error: null,
93
- totalMs
94
- };
95
- }
96
- //#endregion
97
- //#region src/core/log-report.ts
98
- const RANK = {
99
- debug: 0,
100
- info: 1,
101
- warn: 2,
102
- error: 3
103
- };
104
- /** Under this, with the loop running, is a frame rate somebody can feel. */
105
- const SLOW_FPS = 50;
106
- /** How many distinct messages to carry. Beyond this it is a log file, not a report. */
107
- const MAX_GROUPS = 20;
108
- /** The engine logs values, not strings — format them the way a console would. */
109
- function format(parts) {
110
- return parts.map((p) => {
111
- if (typeof p === "string") return p;
112
- if (p instanceof Error) return p.message;
113
- try {
114
- return JSON.stringify(p);
115
- } catch {
116
- return String(p);
117
- }
118
- }).join(" ");
119
- }
120
- function logReport(input) {
121
- const counts = {
122
- debug: 0,
123
- info: 0,
124
- warn: 0,
125
- error: 0
126
- };
127
- const byMessage = /* @__PURE__ */ new Map();
128
- for (const e of input.entries) {
129
- counts[e.level] += 1;
130
- const message = format(e.parts);
131
- const key = `${e.level}\0${message}`;
132
- const seen = byMessage.get(key);
133
- if (seen) {
134
- seen.count += 1;
135
- seen.lastMs = Math.max(seen.lastMs, e.timeMs);
136
- } else byMessage.set(key, {
137
- level: e.level,
138
- message,
139
- count: 1,
140
- lastMs: e.timeMs
141
- });
142
- }
143
- const grouped = [...byMessage.values()].sort((a, b) => RANK[b.level] - RANK[a.level] || b.lastMs - a.lastMs).slice(0, MAX_GROUPS);
144
- const hidden = input.hidden === true;
145
- const slow = !hidden && input.stats.running && input.stats.fps > 0 && input.stats.fps < SLOW_FPS;
146
- return {
147
- hidden,
148
- ok: counts.error === 0 && input.stats.errors === 0 && input.assetErrors.length === 0,
149
- slow,
150
- counts,
151
- grouped,
152
- worst: grouped.find((g) => g.level === "error") ?? grouped[0] ?? null,
153
- stats: input.stats,
154
- assetErrors: input.assetErrors
155
- };
156
- }
157
- /** The report as a person or an agent reads it: what is wrong, then the numbers. */
158
- function logText(r) {
159
- const lines = [];
160
- if (r.stats.errors > 0) lines.push(`${r.stats.errors} error${r.stats.errors === 1 ? "" : "s"} were swallowed to keep the game alive — it is running, and something in it is broken`);
161
- for (const a of r.assetErrors) lines.push(`asset ${a.ref} never loaded: ${a.error} (${a.url})`);
162
- for (const g of r.grouped) {
163
- if (g.level === "debug") continue;
164
- lines.push(`[${g.level}] ${g.message}${g.count > 1 ? ` ×${g.count}` : ""}`);
165
- }
166
- if (lines.length === 0) lines.push("nothing wrong — no warnings, no errors, no failed assets");
167
- if (!r.stats.running) lines.push(`not running — the loop is stopped (${r.stats.nodes} nodes in the scene)`);
168
- else if (r.hidden) lines.push(`${r.stats.nodes} nodes · frame rate not measurable — the page is hidden (a window covered by another counts), and the browser throttles it. Bring it to the front`);
169
- else lines.push(`${Math.round(r.stats.fps)} fps · ${r.stats.frameMs.toFixed(1)}ms per frame · ${r.stats.nodes} nodes${r.slow ? " — slow enough to feel" : ""}`);
170
- return lines.join("\n");
171
- }
172
- //#endregion
173
- export { logText as n, parseDrive as r, logReport as t };