incanto 0.68.0 → 0.70.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 (153) hide show
  1. package/assets/catalog.json +9 -5
  2. package/bin/_behaviors-loader.mjs +22 -0
  3. package/bin/_read-json.mjs +28 -0
  4. package/bin/incanto-assets.mjs +19 -5
  5. package/bin/incanto-check.mjs +92 -15
  6. package/bin/incanto-editor.mjs +128 -5
  7. package/bin/incanto-env.mjs +3 -2
  8. package/bin/incanto-feel.mjs +24 -13
  9. package/bin/incanto-frame.mjs +8 -1
  10. package/bin/incanto-multiplay.mjs +11 -9
  11. package/bin/incanto-new.mjs +128 -5
  12. package/bin/incanto-play.mjs +158 -16
  13. package/bin/incanto-playtest.mjs +66 -23
  14. package/bin/incanto-serve.mjs +160 -0
  15. package/bin/incanto-skills.mjs +14 -2
  16. package/bin/incanto-verify.mjs +165 -44
  17. package/bin/incanto.mjs +4 -2
  18. package/dist/2d.d.ts +285 -36
  19. package/dist/2d.js +4 -4
  20. package/dist/3d.d.ts +158 -10
  21. package/dist/3d.js +8 -8
  22. package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
  23. package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-DaMxqfNE.d.ts} +33 -14
  24. package/dist/{behavior-DoFPYrgo.d.ts → behavior-DZExDn9o.d.ts} +809 -44
  25. package/dist/{create-game-IZIydDwI.js → create-game-Bwvh6q8A.js} +148 -61
  26. package/dist/{create-game-DbWtVTxD.js → create-game-C7ffQWW7.js} +103 -49
  27. package/dist/debug.d.ts +1 -1
  28. package/dist/debug.js +2 -3
  29. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  30. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  31. package/dist/editor.js +1066 -864
  32. package/dist/env.d.ts +1 -1
  33. package/dist/env.js +5 -3
  34. package/dist/{environment-presets-CybQXNqS.js → environment-presets-D6Q5BxeE.js} +299 -46
  35. package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
  36. package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
  37. package/dist/{gameplay-DM1eu_cV.js → gameplay-BfHkuzVb.js} +825 -221
  38. package/dist/gameplay.d.ts +187 -7
  39. package/dist/gameplay.js +1 -1
  40. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  41. package/dist/index.d.ts +156 -13
  42. package/dist/index.js +10 -12
  43. package/dist/json-CfTjpvW8.js +67 -0
  44. package/dist/{loader-CcB533FR.d.ts → loader-Cff09LMm.d.ts} +2 -2
  45. package/dist/net.d.ts +27 -3
  46. package/dist/net.js +2 -2
  47. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  48. package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CE0Qvy3V.js} +136 -11
  49. package/dist/{physics-3d-bG3n70Ky.js → physics-3d-CpH-2gn5.js} +104 -23
  50. package/dist/{teardown-D2NEmxPB.js → picking-CQJ_PJKh.js} +106 -14
  51. package/dist/react.d.ts +2 -2
  52. package/dist/react.js +2 -2
  53. package/dist/{register-3ta-2Xig.js → register-6DYnKZcy.js} +652 -831
  54. package/dist/{register-ibCjm-wH.js → register-Bkk0wSDB.js} +348 -30
  55. package/dist/{replay-CYvhVHHN.js → replay-DjAkAzMq.js} +224 -14
  56. package/dist/{replay-Dvn8aeBd.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
  57. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  58. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  59. package/dist/{loader-BC4PNtJX.js → save-slots-BXVg148r.js} +4558 -2294
  60. package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
  61. package/dist/{split-screen-DhrSzZIB.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
  62. package/dist/{split-screen-CYwDkbLF.js → split-screen-PL78oVXP.js} +159 -26
  63. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  64. package/dist/{src-D7RIqXYF.js → src-Cxfiv1Hg.js} +2 -17
  65. package/dist/test-iHYVUcDK.js +4036 -0
  66. package/dist/test.d.ts +542 -30
  67. package/dist/test.js +3 -3
  68. package/dist/touch-BnCyPA0G.js +519 -0
  69. package/dist/vite.d.ts +54 -3
  70. package/dist/vite.js +349 -17
  71. package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-C8aDbGmR.js} +56 -1
  72. package/editor/assets/agent8-D0MS174y.js +1 -0
  73. package/editor/assets/{debug-BBhKuBNV.js → debug-BnXkKuYu.js} +2 -2
  74. package/editor/assets/index-CIu3uc3l.js +11046 -0
  75. package/editor/index.html +1 -1
  76. package/package.json +7 -16
  77. package/schemas/scene.schema.json +30 -3
  78. package/skills/incanto-3d-character.md +14 -1
  79. package/skills/incanto-3d-models.md +12 -0
  80. package/skills/incanto-assets.md +25 -3
  81. package/skills/incanto-audio.md +27 -5
  82. package/skills/incanto-behaviors-and-scripts.md +83 -6
  83. package/skills/incanto-building-2d-games.md +106 -8
  84. package/skills/incanto-building-3d-games.md +118 -6
  85. package/skills/incanto-editor.md +46 -7
  86. package/skills/incanto-environment.md +19 -1
  87. package/skills/incanto-game-feel.md +70 -0
  88. package/skills/incanto-gameplay-behaviors.md +121 -14
  89. package/skills/incanto-hud.md +128 -7
  90. package/skills/incanto-localization.md +13 -5
  91. package/skills/incanto-multiplayer.md +83 -3
  92. package/skills/incanto-node-reference.md +222 -58
  93. package/skills/incanto-performance.md +52 -0
  94. package/skills/incanto-physics-and-input.md +123 -24
  95. package/skills/incanto-playtesting.md +78 -2
  96. package/skills/incanto-save-slots.md +188 -6
  97. package/skills/incanto-scene-json-authoring.md +69 -12
  98. package/skills/incanto-verifying-your-game.md +292 -12
  99. package/skills/incanto-web-integration.md +28 -0
  100. package/skills/incanto-your-first-game.md +5 -2
  101. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  102. package/templates-app/beacon-isle-3d/package.json +2 -2
  103. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  104. package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
  105. package/templates-app/beacon-isle-3d/src/main.ts +24 -4
  106. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  107. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  108. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  109. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  110. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  111. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  112. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  113. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  114. package/templates-app/molehill-2d/index.html +73 -0
  115. package/templates-app/molehill-2d/package.json +23 -0
  116. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  117. package/templates-app/molehill-2d/src/game.scene.json +1255 -0
  118. package/templates-app/molehill-2d/src/main.ts +41 -0
  119. package/templates-app/molehill-2d/tsconfig.json +13 -0
  120. package/templates-app/molehill-2d/verify.ts +247 -0
  121. package/templates-app/molehill-2d/vite.config.ts +12 -0
  122. package/templates-app/platformer-2d/index.html +0 -23
  123. package/templates-app/platformer-2d/package.json +2 -2
  124. package/templates-app/platformer-2d/src/behaviors.ts +26 -16
  125. package/templates-app/platformer-2d/src/game.scene.json +143 -625
  126. package/templates-app/platformer-2d/src/main.ts +35 -13
  127. package/templates-app/platformer-2d/tsconfig.json +1 -1
  128. package/templates-app/star-survivor/package.json +2 -2
  129. package/templates-app/star-survivor/src/game.scene.json +41 -195
  130. package/templates-app/star-survivor/src/main.ts +28 -7
  131. package/templates-app/star-survivor/tsconfig.json +1 -1
  132. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  133. package/templates-app/tps-3d/package.json +2 -2
  134. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  135. package/templates-app/tps-3d/src/game.scene.json +78 -217
  136. package/templates-app/tps-3d/src/main.ts +39 -17
  137. package/templates-app/tps-3d/tsconfig.json +1 -1
  138. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  139. package/templates-app/village-quest-3d/package.json +2 -2
  140. package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
  141. package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
  142. package/templates-app/village-quest-3d/src/main.ts +24 -4
  143. package/templates-app/village-quest-3d/src/village.scene.json +199 -838
  144. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  145. package/templates-app/village-quest-3d/verify.ts +14 -1
  146. package/dist/duplicate-IWIqk0HJ.js +0 -22
  147. package/dist/json-CwwhxQgb.js +0 -36
  148. package/dist/registry-CF70EArN.js +0 -212
  149. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  150. package/dist/test-8hoHeRmo.js +0 -2340
  151. package/dist/touch-DEAmqGdf.js +0 -225
  152. package/editor/assets/agent8-BrrHOjMJ.js +0 -1
  153. package/editor/assets/index-eVd0BToA.js +0 -10958
package/dist/vite.js CHANGED
@@ -1,8 +1,11 @@
1
- import { t as VERSION } from "./src-D7RIqXYF.js";
2
- import { n as diffSignatures } from "./frame-report-BSMny7oe.js";
3
- import { s as validateScene } from "./test-8hoHeRmo.js";
1
+ import { a as parseJsonText } from "./json-CfTjpvW8.js";
2
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
+ import { t as VERSION } from "./src-Cxfiv1Hg.js";
4
+ import { n as diffSignatures } from "./frame-report-Dlq13Gyj.js";
5
+ import { s as validateScene } from "./test-iHYVUcDK.js";
4
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
- import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
7
+ import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
8
+ import { fileURLToPath } from "node:url";
6
9
  //#region src/vite/frame-endpoint.ts
7
10
  /**
8
11
  * The dev server's half of `incanto-frame`.
@@ -71,7 +74,7 @@ function serveFrameEndpoints(host, version, opts = {}) {
71
74
  c.on("incanto:logs-report", onReport);
72
75
  }
73
76
  host.middlewares.use("/__incanto/ping", (_req, res) => {
74
- json$1(res, 200, {
77
+ json$2(res, 200, {
75
78
  incanto: version,
76
79
  frame: Boolean(channel),
77
80
  root: host.config?.root ?? process.cwd(),
@@ -81,7 +84,7 @@ function serveFrameEndpoints(host, version, opts = {}) {
81
84
  serveLogs(host, channels, waiting, () => nextId++, timeoutMs);
82
85
  host.middlewares.use("/__incanto/frame", (req, res) => {
83
86
  if (!channel) {
84
- json$1(res, 503, { error: "no-hmr-channel" });
87
+ json$2(res, 503, { error: "no-hmr-channel" });
85
88
  return;
86
89
  }
87
90
  const params = query(req);
@@ -95,13 +98,13 @@ function serveFrameEndpoints(host, version, opts = {}) {
95
98
  const id = nextId++;
96
99
  const timer = setTimeout(() => {
97
100
  waiting.delete(id);
98
- json$1(res, 504, { error: acked.delete(id) ? "page-not-drawing" : "no-page-connected" });
101
+ json$2(res, 504, { error: acked.delete(id) ? "page-not-drawing" : "no-page-connected" });
99
102
  }, timeoutMs + (driveMs > 0 ? driveMs * 2 + 2e3 : 0));
100
103
  waiting.set(id, (value) => {
101
104
  clearTimeout(timer);
102
105
  acked.delete(id);
103
106
  if (!value.ok) {
104
- json$1(res, 500, { error: "capture-failed" });
107
+ json$2(res, 500, { error: "capture-failed" });
105
108
  return;
106
109
  }
107
110
  const full = value.report;
@@ -119,7 +122,7 @@ function serveFrameEndpoints(host, version, opts = {}) {
119
122
  store.set(remember || DEFAULT_LABEL, signature);
120
123
  body.remembered = remember || DEFAULT_LABEL;
121
124
  }
122
- json$1(res, 200, body);
125
+ json$2(res, 200, body);
123
126
  });
124
127
  for (const c of channels) c.send({
125
128
  type: "custom",
@@ -142,21 +145,21 @@ function serveFrameEndpoints(host, version, opts = {}) {
142
145
  function serveLogs(host, channels, waiting, nextId, timeoutMs) {
143
146
  host.middlewares.use("/__incanto/logs", (_req, res) => {
144
147
  if (channels.length === 0) {
145
- json$1(res, 503, { error: "no-hmr-channel" });
148
+ json$2(res, 503, { error: "no-hmr-channel" });
146
149
  return;
147
150
  }
148
151
  const id = nextId();
149
152
  const timer = setTimeout(() => {
150
153
  waiting.delete(id);
151
- json$1(res, 504, { error: "no-page-connected" });
154
+ json$2(res, 504, { error: "no-page-connected" });
152
155
  }, timeoutMs);
153
156
  waiting.set(id, (value) => {
154
157
  clearTimeout(timer);
155
158
  if (!value.ok) {
156
- json$1(res, 500, { error: "report-failed" });
159
+ json$2(res, 500, { error: "report-failed" });
157
160
  return;
158
161
  }
159
- json$1(res, 200, { report: value.report });
162
+ json$2(res, 200, { report: value.report });
160
163
  });
161
164
  for (const c of channels) c.send({
162
165
  type: "custom",
@@ -178,7 +181,7 @@ function parseGrid(text) {
178
181
  const rows = Number(m[2]);
179
182
  return cols > 0 && rows > 0 ? [cols, rows] : void 0;
180
183
  }
181
- function json$1(res, status, body) {
184
+ function json$2(res, status, body) {
182
185
  res.statusCode = status;
183
186
  res.setHeader("content-type", "application/json");
184
187
  res.end(JSON.stringify(body));
@@ -255,6 +258,251 @@ async function listeningPorts(deps) {
255
258
  return out;
256
259
  }
257
260
  //#endregion
261
+ //#region src/vite/options.ts
262
+ /**
263
+ * A plugin option the plugin does not know is a hard error, like every other
264
+ * key this engine reads.
265
+ *
266
+ * Scene props have had `UNKNOWN_PROP` — "Valid props: […]" — since the first
267
+ * milestone. A vite plugin's options had nothing: `incantoScenes({
268
+ * strictBehavior: true })` type-checked green, built green and did NOTHING,
269
+ * because the scaffolded `tsconfig.json` never pointed the compiler at
270
+ * `vite.config.ts` in the first place.
271
+ *
272
+ * That `include` is fixed too, but it cannot be the whole answer. TypeScript's
273
+ * excess-property check only fires on a FRESH object literal — hoist the
274
+ * options into a `const` and it passes again — and a `vite.config.js` is never
275
+ * type-checked at all. This is the half that holds either way.
276
+ */
277
+ function onlyKnownOptions(plugin, opts, known) {
278
+ const valid = new Set(known);
279
+ for (const key of Object.keys(opts)) {
280
+ if (valid.has(key)) continue;
281
+ const near = known.find((k) => k.toLowerCase() === key.toLowerCase()) ?? closest(key, known);
282
+ throw new IncantoError("UNKNOWN_PROP", `${plugin}() got an option it does not know: '${key}'.` + (near ? ` Did you mean '${near}'?` : "") + ` Valid options: [${known.join(", ")}].`, { prop: key });
283
+ }
284
+ }
285
+ /** The one a typo probably meant, or null when nothing is close enough. */
286
+ function closest(written, known) {
287
+ let best = null;
288
+ let bestScore = 3;
289
+ for (const k of known) {
290
+ const d = distance(written.toLowerCase(), k.toLowerCase());
291
+ if (d < bestScore) {
292
+ bestScore = d;
293
+ best = k;
294
+ }
295
+ }
296
+ return best;
297
+ }
298
+ function distance(a, b) {
299
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
300
+ for (let j = 0; j <= b.length; j++) rows[0][j] = j;
301
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
302
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
303
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
304
+ }
305
+ return rows[a.length][b.length];
306
+ }
307
+ //#endregion
308
+ //#region src/vite/static-inspect.ts
309
+ /**
310
+ * Build a `FrameHost` whose channel is a long poll.
311
+ *
312
+ * `use(path, handler)` records middleware the caller dispatches; `hot` is a
313
+ * channel that queues outgoing requests and delivers the page's replies.
314
+ */
315
+ function createStaticInspectHost(root) {
316
+ const routes = [];
317
+ const listeners = /* @__PURE__ */ new Map();
318
+ const queue = [];
319
+ let waiter = null;
320
+ return {
321
+ config: { root },
322
+ middlewares: { use(path, handler) {
323
+ routes.push([path, handler]);
324
+ } },
325
+ hot: {
326
+ send(payload) {
327
+ const item = {
328
+ event: payload.event,
329
+ data: payload.data
330
+ };
331
+ if (waiter) {
332
+ const resolve = waiter;
333
+ waiter = null;
334
+ resolve(item);
335
+ return;
336
+ }
337
+ queue.push(item);
338
+ if (queue.length > 32) queue.shift();
339
+ },
340
+ on(event, cb) {
341
+ const list = listeners.get(event) ?? [];
342
+ list.push(cb);
343
+ listeners.set(event, list);
344
+ }
345
+ },
346
+ handleInspect(req, res) {
347
+ const url = (req.originalUrl ?? req.url ?? "").split("?")[0] ?? "";
348
+ if (url === "/__incanto/pull") {
349
+ const next = queue.shift();
350
+ if (next) {
351
+ json$1(res, next);
352
+ return true;
353
+ }
354
+ const timer = setTimeout(() => {
355
+ if (waiter) {
356
+ waiter = null;
357
+ json$1(res, null);
358
+ }
359
+ }, 25e3);
360
+ waiter = (value) => {
361
+ clearTimeout(timer);
362
+ json$1(res, value);
363
+ };
364
+ return true;
365
+ }
366
+ if (url === "/__incanto/push") {
367
+ readBody$1(req, (body) => {
368
+ const message = safeParse(body);
369
+ if (message?.event) for (const cb of listeners.get(message.event) ?? []) cb(message.data);
370
+ json$1(res, { ok: true });
371
+ });
372
+ return true;
373
+ }
374
+ for (const [path, handler] of routes) if (url === path || url.startsWith(`${path}/`)) {
375
+ handler(req, res);
376
+ return true;
377
+ }
378
+ return false;
379
+ },
380
+ clientScript: () => CLIENT
381
+ };
382
+ }
383
+ function json$1(res, body) {
384
+ res.statusCode = 200;
385
+ res.setHeader("content-type", "application/json");
386
+ res.end(JSON.stringify(body ?? {}));
387
+ }
388
+ function safeParse(text) {
389
+ try {
390
+ return JSON.parse(text);
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
395
+ function readBody$1(req, done) {
396
+ const stream = req;
397
+ let body = "";
398
+ stream.on("data", (chunk) => {
399
+ body += String(chunk);
400
+ });
401
+ stream.on("end", () => done(body));
402
+ }
403
+ /**
404
+ * The page half: installs a channel shaped like vite's HMR, so `wireDevChannel`
405
+ * cannot tell the difference.
406
+ *
407
+ * Injected by `incanto-serve` into the served `index.html`, never bundled — a
408
+ * production build stays exactly what the author deployed, and this only exists
409
+ * while they are looking at it.
410
+ */
411
+ const CLIENT = `(() => {
412
+ const listeners = new Map();
413
+ const post = (event, data) =>
414
+ fetch('/__incanto/push', {
415
+ method: 'POST',
416
+ headers: { 'content-type': 'application/json' },
417
+ body: JSON.stringify({ event: event, data: data }),
418
+ }).catch(() => {});
419
+ globalThis.__incantoInspect = {
420
+ send: (event, data) => post(event, data),
421
+ on: (event, cb) => {
422
+ const list = listeners.get(event) || [];
423
+ list.push(cb);
424
+ listeners.set(event, list);
425
+ },
426
+ off: (event, cb) => {
427
+ const list = (listeners.get(event) || []).filter((f) => f !== cb);
428
+ listeners.set(event, list);
429
+ },
430
+ };
431
+ const pull = () => {
432
+ fetch('/__incanto/pull')
433
+ .then((r) => r.json())
434
+ .then((message) => {
435
+ if (message && message.event) {
436
+ for (const cb of listeners.get(message.event) || []) cb(message.data);
437
+ }
438
+ pull();
439
+ })
440
+ .catch(() => setTimeout(pull, 1000));
441
+ };
442
+ pull();
443
+ })();`;
444
+ //#endregion
445
+ //#region src/vite/trim.ts
446
+ /**
447
+ * Leave out of the BUILD what this game can never run.
448
+ *
449
+ * A 2D game's `dist/` contains the 3D adapter and rapier3d. Measured on the
450
+ * shipped `platformer-2d` flagship:
451
+ *
452
+ * ```
453
+ * 2236503 assets/rapier-*.js # rapier3d
454
+ * 1692150 assets/rapier-*.js # rapier2d — the one it uses
455
+ * 471186 assets/3d-*.js # the 3D adapter
456
+ * ─────────
457
+ * 5942650 total → 45.6% of it can never execute
458
+ * ```
459
+ *
460
+ * Nothing is wrong at RUNTIME: the chunks are lazy and never requested. This is
461
+ * deploy weight — a CDN, a jam upload, an agent8 zip — and the only path that
462
+ * drags them in is the live game⇄editor switch: `createGame*` imports
463
+ * `incanto/editor`, and the editor imports `incanto/3d` so it can offer 3D node
464
+ * types.
465
+ *
466
+ * `editor: false` on `createGame*` is a RUNTIME option and cannot remove bytes
467
+ * from a bundle; it changed the artifact by 10 bytes while the docs said it
468
+ * "opts out". A build-time decision needs a build-time lever, which is this.
469
+ *
470
+ * ```ts
471
+ * import { incantoScenes, incantoTrim } from 'incanto/vite';
472
+ * export default defineConfig({
473
+ * plugins: [incantoScenes(), incantoTrim({ editor: false })],
474
+ * });
475
+ * ```
476
+ *
477
+ * Build only — the dev server keeps the editor, so `☰ debug ▸ ✎ edit this
478
+ * scene` works while you are building the game and is simply absent from what
479
+ * you ship.
480
+ */
481
+ const VIRTUAL = "\0incanto-trimmed-editor";
482
+ function incantoTrim(opts = {}) {
483
+ onlyKnownOptions("incantoTrim", opts, ["editor"]);
484
+ const keepEditor = opts.editor === true;
485
+ return {
486
+ name: "incanto:trim",
487
+ apply: "build",
488
+ enforce: "pre",
489
+ resolveId(id) {
490
+ if (!keepEditor && id === "incanto/editor") return VIRTUAL;
491
+ return null;
492
+ },
493
+ load(id) {
494
+ if (id !== VIRTUAL) return null;
495
+ return `export function mountEditor() {
496
+ throw new Error(
497
+ 'incanto: the scene editor was left out of this build by incantoTrim({ editor: false }). ' +
498
+ 'Run the dev server to edit scenes, or pass incantoTrim({ editor: true }) to include it.',
499
+ );
500
+ }
501
+ `;
502
+ }
503
+ };
504
+ }
505
+ //#endregion
258
506
  //#region src/vite/index.ts
259
507
  /**
260
508
  * incanto/vite — dev-server integration: validate every `*.scene.json` edit
@@ -275,7 +523,71 @@ async function listeningPorts(deps) {
275
523
  * Typed structurally (no vite dependency) — the returned objects satisfy
276
524
  * vite's Plugin interface.
277
525
  */
526
+ /**
527
+ * The BUILT-IN art, at the path the scenes already name.
528
+ *
529
+ * `incanto-assets` prints `incanto/assets/items/coin.png` as an asset's url and
530
+ * three shipped 2D templates carry exactly that — a BUNDLER specifier, which
531
+ * the game's own `main.ts` resolves with an import and a browser cannot. The
532
+ * editor loads the scene JSON as written, so every sprite in `platformer-2d`,
533
+ * `star-survivor` and `topdown-rpg` was a blank quad in the composer that ships
534
+ * with them, in the standalone editor and in the one you switch into from the
535
+ * running game alike.
536
+ *
537
+ * Serving the package's own `assets/` makes the written url resolve. Dev-only
538
+ * (this is `configureServer`), read-only, and confined to that directory.
539
+ */
540
+ function serveBuiltInAssets(server) {
541
+ const assets = builtInAssetsDir();
542
+ if (!assets) return;
543
+ server.middlewares.use("/incanto/assets", (req, res) => {
544
+ const asked = (req.url ?? "/").split("?")[0] ?? "/";
545
+ const file = normalize(join(assets, decodeURIComponent(asked)));
546
+ if (!file.startsWith(assets + sep) || !existsSync(file) || !statSync(file).isFile()) {
547
+ res.statusCode = 404;
548
+ res.end(`no built-in asset: ${asked}`);
549
+ return;
550
+ }
551
+ res.setHeader("content-type", ASSET_MIME[extname(file)] ?? "application/octet-stream");
552
+ res.end(readFileSync(file));
553
+ });
554
+ }
555
+ /**
556
+ * The package's own `assets/`, found by WALKING UP rather than by counting
557
+ * directories.
558
+ *
559
+ * Shipped it is `…/incanto/dist/vite.js` -> `…/incanto/assets`; in this repo
560
+ * the same module is `…/packages/engine/src/vite/index.ts`, one level deeper.
561
+ * A fixed `../..` is right in exactly one of those, and the one it would have
562
+ * been wrong in is the one every test runs from.
563
+ */
564
+ function builtInAssetsDir() {
565
+ let at = dirname(fileURLToPath(import.meta.url));
566
+ for (let up = 0; up < 5; up++) {
567
+ const candidate = join(at, "assets");
568
+ if (existsSync(join(candidate, "catalog.json"))) return candidate;
569
+ const parent = dirname(at);
570
+ if (parent === at) break;
571
+ at = parent;
572
+ }
573
+ return null;
574
+ }
575
+ const ASSET_MIME = {
576
+ ".png": "image/png",
577
+ ".jpg": "image/jpeg",
578
+ ".jpeg": "image/jpeg",
579
+ ".webp": "image/webp",
580
+ ".gif": "image/gif",
581
+ ".svg": "image/svg+xml",
582
+ ".json": "application/json; charset=utf-8",
583
+ ".mp3": "audio/mpeg",
584
+ ".ogg": "audio/ogg",
585
+ ".wav": "audio/wav",
586
+ ".glb": "model/gltf-binary",
587
+ ".gltf": "model/gltf+json"
588
+ };
278
589
  function incantoScenes(opts = {}) {
590
+ onlyKnownOptions("incantoScenes", opts, ["strictBehaviors", "root"]);
279
591
  return {
280
592
  name: "incanto-scenes",
281
593
  /**
@@ -292,19 +604,20 @@ function incantoScenes(opts = {}) {
292
604
  server.middlewares.use("/api/scene", (req, res) => {
293
605
  serveSceneFile(req, res, root);
294
606
  });
607
+ serveBuiltInAssets(server);
295
608
  },
296
609
  async handleHotUpdate(ctx) {
297
610
  if (!ctx.file.endsWith(".scene.json")) return void 0;
298
611
  let json;
299
612
  try {
300
- json = JSON.parse(await ctx.read());
613
+ json = parseJsonText(await ctx.read());
301
614
  } catch (e) {
302
615
  report(ctx, `not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
303
616
  return [];
304
617
  }
305
618
  const result = validateScene(json, {
306
619
  strictBehaviors: opts.strictBehaviors,
307
- resolveScene: (path) => JSON.parse(readFileSync(resolve(dirname(ctx.file), path), "utf-8"))
620
+ resolveScene: (path) => parseJsonText(readFileSync(resolve(dirname(ctx.file), path), "utf-8"))
308
621
  });
309
622
  if (!result.ok && result.error) {
310
623
  const e = result.error;
@@ -559,6 +872,20 @@ async function serveSceneFile(req, res, root) {
559
872
  }
560
873
  const shape = sceneFacts(parsed);
561
874
  if (!shape.ok) return json(res, 422, { error: `not an incanto scene — ${shape.why}. The file was NOT written.` });
875
+ if (existsSync(abs)) {
876
+ let existing = null;
877
+ let readable = true;
878
+ try {
879
+ existing = JSON.parse(readFileSync(abs, "utf-8"));
880
+ } catch {
881
+ readable = false;
882
+ }
883
+ const already = readable ? sceneFacts(existing) : {
884
+ ok: false,
885
+ why: "it is not JSON"
886
+ };
887
+ if (!already.ok) return json(res, 409, { error: `refusing to overwrite ${file} — ${already.why}. The editor only replaces scenes, and only creates new files.` });
888
+ }
562
889
  try {
563
890
  writeFileSync(abs, raw.endsWith("\n") ? raw : `${raw}\n`);
564
891
  } catch (error) {
@@ -599,6 +926,11 @@ function readEnv() {
599
926
  * ```
600
927
  */
601
928
  function incantoLibrary(opts = {}) {
929
+ onlyKnownOptions("incantoLibrary", opts, [
930
+ "token",
931
+ "url",
932
+ "authUrl"
933
+ ]);
602
934
  return {
603
935
  name: "incanto-library",
604
936
  apply: "serve",
@@ -734,4 +1066,4 @@ function libraryItem(row) {
734
1066
  };
735
1067
  }
736
1068
  //#endregion
737
- export { discoverScenes, incantoLibrary, incantoScenes, listeningPorts, maskToken, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };
1069
+ export { createStaticInspectHost, discoverScenes, incantoLibrary, incantoScenes, incantoTrim, listeningPorts, maskToken, onlyKnownOptions, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };
@@ -1,5 +1,11 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  //#region src/core/drive-script.ts
3
+ /** The three buttons a `Clickable` can be bound to, by the words people use. */
4
+ const BUTTONS = {
5
+ left: 0,
6
+ middle: 1,
7
+ right: 2
8
+ };
3
9
  const num = (raw) => {
4
10
  if (raw === void 0) return null;
5
11
  const n = Number(raw);
@@ -75,6 +81,44 @@ function parseDrive(text) {
75
81
  });
76
82
  break;
77
83
  }
84
+ case "at": {
85
+ if (rest.length === 1 && rest[0]) {
86
+ steps.push({
87
+ kind: "atNode",
88
+ path: rest[0]
89
+ });
90
+ break;
91
+ }
92
+ const x = num(rest[0]);
93
+ const y = num(rest[1]);
94
+ if (x === null || y === null || rest.length > 2) return bad("at needs two numbers (canvas px) or one node path");
95
+ steps.push({
96
+ kind: "at",
97
+ x,
98
+ y
99
+ });
100
+ break;
101
+ }
102
+ case "click": {
103
+ const which = BUTTONS[(rest.length === 0 ? "left" : rest[0]) ?? "left"];
104
+ if (which === void 0 || rest.length > 1) return bad("click takes nothing, or left|middle|right — say where with `at`");
105
+ steps.push({
106
+ kind: "click",
107
+ button: which
108
+ });
109
+ break;
110
+ }
111
+ case "mouse": {
112
+ const [which, dir] = rest.length === 1 ? ["left", rest[0]] : [rest[0], rest[1]];
113
+ const button = BUTTONS[which ?? ""] ?? num(which ?? "");
114
+ if (button === null || button === void 0 || dir !== "down" && dir !== "up") return bad("mouse needs a button (left|middle|right, or 0|1|2) and down|up");
115
+ steps.push({
116
+ kind: "mouse",
117
+ button,
118
+ down: dir === "down"
119
+ });
120
+ break;
121
+ }
78
122
  case "step": {
79
123
  const ms = num(rest[0]);
80
124
  if (ms === null || ms < 0) return bad("step needs a duration in ms");
@@ -103,6 +147,13 @@ const RANK = {
103
147
  error: 3
104
148
  };
105
149
  /** Under this, with the loop running, is a frame rate somebody can feel. */
150
+ /**
151
+ * How stale the newest frame may be before the rate stops describing NOW.
152
+ *
153
+ * A live game is always a frame or two behind; a third of a second is about
154
+ * twenty dropped frames, which is not a hiccup.
155
+ */
156
+ const STALE_FRAME_MS = 350;
106
157
  const SLOW_FPS = 50;
107
158
  /** How many distinct messages to carry. Beyond this it is a log file, not a report. */
108
159
  const MAX_GROUPS = 20;
@@ -167,7 +218,11 @@ function logText(r) {
167
218
  if (lines.length === 0) lines.push("nothing wrong — no warnings, no errors, no failed assets");
168
219
  if (!r.stats.running) lines.push(`not running — the loop is stopped (${r.stats.nodes} nodes in the scene)`);
169
220
  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`);
170
- 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" : ""}`);
221
+ else {
222
+ const age = r.stats.lastFrameAgeMs;
223
+ const stale = age !== null && age > STALE_FRAME_MS;
224
+ 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" : ""}` + (stale ? ` — measured ${(age / 1e3).toFixed(1)}s ago; nothing has drawn since, so this is the rate the game HAD, not the rate it has` : ""));
225
+ }
171
226
  return lines.join("\n");
172
227
  }
173
228
  //#endregion
@@ -0,0 +1 @@
1
+ import{i as e,n as t}from"./index-CIu3uc3l.js";async function n(e,n){return new a((await t(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,e,{diagnostics:n})}var r=[500,1e3,2e3,4e3,8e3],i=8,a=class{raw;active=new Map;reconnecting=!1;disposed=!1;diagnostics;wait;constructor(e,t,n){this.diagnostics=n?.diagnostics??null,this.wait=n?.wait??(e=>new Promise(t=>{setTimeout(t,e)})),this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{for(let t=0;t<i;t++){let n=!1,i=null;try{n=await this.rawConnect()!==!1}catch(e){i=e}if(this.disposed)return;if(n){for(let e of this.active.values())e.off(),e.off=e.make();t>0&&e(this.diagnostics,`info`,`[incanto] multiplayer reconnected after ${t+1} attempts.`);return}t===0&&e(this.diagnostics,`warn`,`[incanto] the multiplayer connection dropped and the server would not take it back${i?` (${i instanceof Error?i.message:String(i)})`:``}. Retrying — other players cannot see this one until it comes back.`);let a=r[Math.min(t,r.length-1)]??8e3;if(await this.wait(a),this.disposed)return}e(this.diagnostics,`error`,`[incanto] multiplayer gave up after ${i} reconnect attempts — this client is offline and the others cannot see it. Call connect() again to retry.`)}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{n as createAgent8Server};