incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
- import { i as instancedScenes, r as auditScene } from "./replay-BEHLDcQV.js";
2
- import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-D6ISmuFE.js";
3
- export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
1
+ import { i as instancedScenes, r as auditScene } from "./replay-DEvp3kyV.js";
2
+ import { A as geometricPicker, C as facingReport, D as playtest, E as findPlayer, F as sceneAssetUrls, M as pointAtNode, N as describeFraming, O as playtestFailed, P as framingText, S as collectionProblems, T as failingReplays, _ as feelReport, a as registerAllNodes, b as takeCollectionSnapshots, c as feelsRung, d as ladderVerdict, f as saysRung, g as playMultiplayer, h as multiplayText, i as findFloatingProps, j as geometricView, k as playtestText, l as firstLine, m as multiplayProblems, n as createPlaySession, o as runScript, p as unmeasuredPlays, r as describeCapture, s as validateScene, t as captureScene, u as ladderText, v as feelText, w as facingText, x as agreementProblems, y as takeAgreementSnapshots } from "./test-CmN54kWv.js";
3
+ export { agreementProblems, auditScene, captureScene, collectionProblems, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, feelsRung, findFloatingProps, findPlayer, firstLine, framingText, geometricPicker, geometricView, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestFailed, playtestText, pointAtNode, registerAllNodes, runScript, saysRung, sceneAssetUrls, takeAgreementSnapshots, takeCollectionSnapshots, unmeasuredPlays, validateScene };
@@ -0,0 +1,519 @@
1
+ import { r as jsonKind, t as jsonClone } from "./json-CfTjpvW8.js";
2
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
+ //#region src/core/nearest.ts
4
+ /**
5
+ * The one name you probably meant, out of the list the error already prints.
6
+ *
7
+ * Every `UNKNOWN_*` error in this engine lists what WOULD have worked, which is
8
+ * the right instinct and, past a couple of dozen options, not an answer. Two
9
+ * measured cases, from one session of building a game out of the skills:
10
+ *
11
+ * ```
12
+ * Unknown node type 'Label2D'. Registered types: [Node, Timer, Respawn,
13
+ * AudioPlayer, HudLayer, UiText, … 64 of them …].
14
+ *
15
+ * Unknown prop 'size' on 'Label'. Valid props: [position, rotation, static,
16
+ * scale, renderOrder, orderGroup, visible, text, fontSize, color, font, align,
17
+ * opacity].
18
+ * ```
19
+ *
20
+ * Both answers are in those lists — `Label`, and `fontSize`. Nothing points at
21
+ * them, and the wall of names reads as "none of these is what you want". The
22
+ * scene loader has had `nearestKey` for its own key check since the round that
23
+ * closed that dictionary; the same three lines belong on every one of them.
24
+ *
25
+ * Two rules, because typos come in two shapes:
26
+ *
27
+ * a small EDIT distance `Label2D` → `Label`, `Sprite2D` → `Sprite3D`,
28
+ * `colorrect2d` → `ColorRect2D` (case only, d = 0)
29
+ * a CONTAINED word `size` → `fontSize`, `speed` → `fallSpeed` — far
30
+ * apart by edit distance, and obviously the same word
31
+ */
32
+ /** Case-insensitive Levenshtein distance. */
33
+ function editDistance(a, b) {
34
+ const x = a.toLowerCase();
35
+ const y = b.toLowerCase();
36
+ const rows = Array.from({ length: x.length + 1 }, () => Array(y.length + 1).fill(0));
37
+ for (let i = 0; i <= x.length; i++) rows[i][0] = i;
38
+ for (let j = 0; j <= y.length; j++) rows[0][j] = j;
39
+ for (let i = 1; i <= x.length; i++) for (let j = 1; j <= y.length; j++) {
40
+ const cost = x[i - 1] === y[j - 1] ? 0 : 1;
41
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
42
+ }
43
+ return rows[x.length][y.length];
44
+ }
45
+ /**
46
+ * The closest candidate, or null when nothing is close enough to name.
47
+ *
48
+ * Null matters as much as a hit: a suggestion that is not the answer sends its
49
+ * reader somewhere wrong, so a name that resembles nothing gets the list alone,
50
+ * exactly as before.
51
+ */
52
+ function nearestName(name, known) {
53
+ let best = null;
54
+ let bestScore = Number.POSITIVE_INFINITY;
55
+ const lower = name.toLowerCase();
56
+ for (const candidate of known) {
57
+ if (candidate === name) continue;
58
+ const d = editDistance(name, candidate);
59
+ const low = candidate.toLowerCase();
60
+ const contained = lower.length >= 3 && (low.includes(lower) || lower.includes(low)) && Math.abs(low.length - lower.length) <= 8;
61
+ const score = d <= 2 ? d : contained ? 2.5 : Number.POSITIVE_INFINITY;
62
+ if (score < bestScore) {
63
+ bestScore = score;
64
+ best = candidate;
65
+ }
66
+ }
67
+ return best;
68
+ }
69
+ /** ` Did you mean "X"?`, or nothing at all. */
70
+ function didYouMean(name, known) {
71
+ const guess = nearestName(name, known);
72
+ return guess ? ` Did you mean "${guess}"?` : "";
73
+ }
74
+ //#endregion
75
+ //#region src/core/prop-range.ts
76
+ function describe(range) {
77
+ const { min, max, exclusiveMin, exclusiveMax, allow } = range;
78
+ let body;
79
+ if (min !== void 0 && max !== void 0) body = `${exclusiveMin ? "above " : ""}${min}..${max}${exclusiveMax ? " exclusive" : ""}`;
80
+ else if (min !== void 0) body = exclusiveMin ? `greater than ${min}` : `at least ${min}`;
81
+ else if (max !== void 0) body = exclusiveMax ? `less than ${max}` : `at most ${max}`;
82
+ else body = "a finite number";
83
+ return allow && allow.length > 0 ? `${body} (or ${allow.join(", ")})` : body;
84
+ }
85
+ /**
86
+ * Throws `PROP_TYPE_MISMATCH` when a number falls outside its documented range.
87
+ *
88
+ * A non-number is ignored: the kind check has already run and reporting the
89
+ * same value twice, under two codes, tells its reader nothing new.
90
+ *
91
+ * @param value the authored value
92
+ * @param label `Type.prop`, for the message
93
+ * @param range what the prop documents
94
+ */
95
+ function validatePropRange(value, label, range) {
96
+ if (typeof value !== "number") return;
97
+ if (range.allow?.includes(value)) return;
98
+ if (!Number.isFinite(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${label}' must be a finite number, got ${value}.`, { prop: label });
99
+ const belowMin = range.min !== void 0 && (range.exclusiveMin ? value <= range.min : value < range.min);
100
+ const aboveMax = range.max !== void 0 && (range.exclusiveMax ? value >= range.max : value > range.max);
101
+ if (!belowMin && !aboveMax) return;
102
+ throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${label}' must be ${describe(range)}, got ${value}.`, { prop: label });
103
+ }
104
+ //#endregion
105
+ //#region src/core/registry.ts
106
+ /**
107
+ * Validate a prop value against its schema default:
108
+ * - JSON kind must match (unless the default is null = "any")
109
+ * - numbers must be finite (NaN/Infinity corrupt JSON files)
110
+ * - non-empty default arrays are FIXED templates: same length, element-wise match
111
+ * (empty default arrays are unconstrained — variable-length lists)
112
+ */
113
+ function validatePropValue(value, def, at, options, kinds) {
114
+ if (options && typeof value === "string" && !options.includes(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' is ${JSON.stringify(value)}, which is not one of: ${options.map((o) => JSON.stringify(o)).join(", ")}.`, {
115
+ prop: at,
116
+ validOptions: [...options]
117
+ });
118
+ const expected = jsonKind(def);
119
+ if (expected === "null") return;
120
+ const actual = jsonKind(value);
121
+ const accepted = kinds ?? [expected];
122
+ if (!accepted.includes(actual)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' expects ${listKinds(accepted)} (default ${JSON.stringify(def)}), got ${actual} (${JSON.stringify(value)}).`, { prop: at });
123
+ if (actual !== expected) return;
124
+ if (typeof value === "number" && !Number.isFinite(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' is ${value} — props must be finite numbers.`, { prop: at });
125
+ if (Array.isArray(def) && Array.isArray(value) && def.length > 0) {
126
+ if (value.length !== def.length) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' expects an array of length ${def.length} (default ${JSON.stringify(def)}), got length ${value.length}.`, { prop: at });
127
+ for (let i = 0; i < def.length; i++) validatePropValue(value[i], def[i], `${at}[${i}]`);
128
+ }
129
+ }
130
+ /** `['string', 'number']` -> `"string or number"`. */
131
+ function listKinds(kinds) {
132
+ if (kinds.length <= 1) return kinds[0] ?? "nothing";
133
+ return `${kinds.slice(0, -1).join(", ")} or ${kinds[kinds.length - 1]}`;
134
+ }
135
+ /**
136
+ * Global node-type registry (Godot's ClassDB role).
137
+ *
138
+ * Registration is EXPLICIT — never an import-time side effect, so
139
+ * `sideEffects: false` tree-shaking can never silently drop node types.
140
+ */
141
+ const types = /* @__PURE__ */ new Map();
142
+ function registerNode(ctor, opts) {
143
+ const existing = types.get(ctor.typeName);
144
+ if (existing && existing !== ctor && !opts?.replace) throw new IncantoError("DUPLICATE_NODE_TYPE", `Node type '${ctor.typeName}' is already registered by a different class. Replacing an implementation (e.g. under hot reload) needs registerNode(ctor, { replace: true }).`, { nodeType: ctor.typeName });
145
+ for (const [key, def] of Object.entries(ctor.props ?? {})) if (def.kinds && !def.kinds.includes(jsonKind(def.default))) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${ctor.typeName}.${key}' declares kinds [${def.kinds.join(", ")}], which do not include its own default's kind (${jsonKind(def.default)}). The default has to be writable in a scene.`, {
146
+ prop: key,
147
+ nodeType: ctor.typeName
148
+ });
149
+ types.set(ctor.typeName, ctor);
150
+ }
151
+ function getNodeType(typeName) {
152
+ const ctor = types.get(typeName);
153
+ if (!ctor) {
154
+ const registered = [...types.keys()];
155
+ throw new IncantoError("UNKNOWN_NODE_TYPE", `Unknown node type '${typeName}'.${didYouMean(typeName, registered)} Registered types: [${registered.join(", ")}].` + missingRegistrarHint(typeName), {
156
+ nodeType: typeName,
157
+ validOptions: registered
158
+ });
159
+ }
160
+ return ctor;
161
+ }
162
+ /**
163
+ * "Unknown node type 'Node2D'" next to a list with no 2D types in it is the
164
+ * most confusing error this engine can produce: the type is not misspelled and
165
+ * not missing — a REGISTRAR was never called. Say which one.
166
+ *
167
+ * The case that produced this: the multiplayer boot registers `registerNodesNet()`
168
+ * and loads a 2D scene, which dies on its own root node.
169
+ */
170
+ function missingRegistrarHint(typeName) {
171
+ const suffix = typeName.endsWith("2D") ? "2D" : typeName.endsWith("3D") ? "3D" : "";
172
+ if (!suffix || types.has(`Node${suffix}`)) return "";
173
+ const dim = suffix.toLowerCase();
174
+ return ` '${typeName}' is a ${dim} node and no ${dim} type is registered: call registerNodes${suffix}() (from 'incanto/${dim}') before loading the scene — createGame${suffix}() does it for you. Registrars compose: a multiplayer game calls registerNodes${suffix}() AND registerNodesNet().`;
175
+ }
176
+ function registeredTypes() {
177
+ return [...types.keys()];
178
+ }
179
+ /** Test isolation helper. */
180
+ function clearRegistry() {
181
+ types.clear();
182
+ }
183
+ /**
184
+ * Effective prop schema for a type: own static `props` merged over every
185
+ * ancestor's (subclass keys win).
186
+ */
187
+ function getNodeSchema(typeName) {
188
+ return mergeStaticProps(getNodeType(typeName));
189
+ }
190
+ /** Walk a constructor's prototype chain collecting static `signals` (deduped). */
191
+ function mergeStaticSignals(ctor) {
192
+ const out = [];
193
+ let current = ctor;
194
+ while (current) {
195
+ if (Object.hasOwn(current, "signals") && current.signals) out.unshift(...current.signals);
196
+ const parent = Object.getPrototypeOf(current);
197
+ current = typeof parent === "function" ? parent : null;
198
+ }
199
+ return [...new Set(out)];
200
+ }
201
+ /** Every signal a registered type declares (its own + inherited). */
202
+ function getNodeSignals(typeName) {
203
+ return mergeStaticSignals(getNodeType(typeName));
204
+ }
205
+ /** Walk a constructor's prototype chain merging static `props` (subclass wins). */
206
+ function mergeStaticProps(ctor) {
207
+ const chain = [];
208
+ let current = ctor;
209
+ while (current) {
210
+ if (Object.hasOwn(current, "props") && current.props) chain.unshift(current.props);
211
+ const parent = Object.getPrototypeOf(current);
212
+ current = typeof parent === "function" ? parent : null;
213
+ }
214
+ return Object.assign({}, ...chain);
215
+ }
216
+ /** Walk a constructor's prototype chain merging static `propAliases` (legacy
217
+ * prop key → its current canonical key; subclass wins). Lets a renamed prop keep
218
+ * loading old scenes (e.g. 2D `zIndex` → `renderOrder`) without a format bump. */
219
+ function mergeStaticAliases(ctor) {
220
+ const chain = [];
221
+ let current = ctor;
222
+ while (current) {
223
+ if (Object.hasOwn(current, "propAliases") && current.propAliases) chain.unshift(current.propAliases);
224
+ const parent = Object.getPrototypeOf(current);
225
+ current = typeof parent === "function" ? parent : null;
226
+ }
227
+ return Object.assign({}, ...chain);
228
+ }
229
+ /**
230
+ * Assign schema defaults (cloned) then the given props onto a target,
231
+ * hard-validating every key and JSON kind. Shared by nodes and behaviors.
232
+ * `aliases` maps legacy keys onto current ones (back-compat for renamed props).
233
+ */
234
+ function applySchemaProps(target, schema, props, label, aliases = {}, extraOptions = {}) {
235
+ for (const [key, def] of Object.entries(schema)) target[key] = jsonClone(def.default);
236
+ if (props) applyGivenProps(target, schema, props, label, aliases, extraOptions);
237
+ requireNonEmpty(target, schema, label);
238
+ }
239
+ function applyGivenProps(target, schema, props, label, aliases, extraOptions) {
240
+ for (const [rawKey, value] of Object.entries(props)) {
241
+ const key = schema[rawKey] ? rawKey : aliases[rawKey] ?? rawKey;
242
+ const def = schema[key];
243
+ if (!def) {
244
+ const valid = Object.keys(schema);
245
+ throw new IncantoError("UNKNOWN_PROP", `Unknown prop '${rawKey}' on '${label}'.${didYouMean(rawKey, valid)} Valid props: [${valid.join(", ")}].`, {
246
+ prop: rawKey,
247
+ nodeType: label,
248
+ validOptions: valid
249
+ });
250
+ }
251
+ const extra = extraOptions[key];
252
+ const options = def.options && extra && extra.length > 0 ? [...def.options, ...extra] : def.options;
253
+ validatePropValue(value, def.default, `${label}.${key}`, options, def.kinds);
254
+ if (def.range && jsonKind(value) === jsonKind(def.default)) validatePropRange(value, `${label}.${key}`, def.range);
255
+ target[key] = jsonClone(value);
256
+ }
257
+ }
258
+ function requireNonEmpty(target, schema, label) {
259
+ for (const [key, def] of Object.entries(schema)) {
260
+ if (!def.required) continue;
261
+ if (!isEmptyValue(target[key])) continue;
262
+ throw new IncantoError("PROP_TYPE_MISMATCH", `'${label}' needs a "${key}" — it is empty.`, {
263
+ prop: key,
264
+ nodeType: label
265
+ });
266
+ }
267
+ }
268
+ /**
269
+ * "Not a configuration" — what `required` refuses.
270
+ *
271
+ * The first cut only knew about `''`, which is what the two props that
272
+ * introduced the flag hold. Three of the ones declared since default to an
273
+ * ARRAY — `MoveTo.to`, `Patrol.points`, `WaveSpawner.waves` — and for those the
274
+ * flag was inert: a `MoveTo` with no `to` loaded clean and moved nowhere, which
275
+ * is exactly the failure the flag was added to stop. Found by running the
276
+ * shipped package as a consumer instead of trusting the declaration.
277
+ */
278
+ function isEmptyValue(value) {
279
+ if (value === "" || value === void 0 || value === null) return true;
280
+ if (Array.isArray(value)) return value.length === 0;
281
+ if (typeof value === "object") return Object.keys(value).length === 0;
282
+ return false;
283
+ }
284
+ /**
285
+ * Instantiate a registered type, assign schema defaults (cloned), then the
286
+ * given props — validating every key and its JSON kind against the default.
287
+ */
288
+ function createNode(typeName, props, extraOptions) {
289
+ const ctor = getNodeType(typeName);
290
+ const node = new ctor();
291
+ applySchemaProps(node, getNodeSchema(typeName), props, typeName, mergeStaticAliases(ctor), extraOptions);
292
+ return node;
293
+ }
294
+ //#endregion
295
+ //#region src/core/touch.ts
296
+ /** Drags inside this fraction of the radius read as neutral. */
297
+ const DEADZONE = .08;
298
+ /**
299
+ * Pure joystick math: drag offset (px) → normalized direction, deadzoned at
300
+ * the center and clamped to unit length at the rim.
301
+ */
302
+ function joystickVector(dx, dy, radius) {
303
+ let x = dx / radius;
304
+ let y = dy / radius;
305
+ const len = Math.hypot(x, y);
306
+ if (len < DEADZONE) return {
307
+ x: 0,
308
+ y: 0
309
+ };
310
+ if (len > 1) {
311
+ x /= len;
312
+ y /= len;
313
+ }
314
+ return {
315
+ x,
316
+ y
317
+ };
318
+ }
319
+ /**
320
+ * On-screen touch controls driven by the scene's own input map: every action
321
+ * declared with `"touch": "joystick"` gets a left-side virtual stick feeding
322
+ * `setActionVector`, every `"touch": "button"` a right-side button feeding
323
+ * `pressAction`/`releaseAction`. The game never knows the difference between
324
+ * touch and keyboard — both arrive as actions.
325
+ *
326
+ * `attachTouchControls(engine, container)` shows them automatically on
327
+ * coarse-pointer devices ('auto' in createGame); `force` shows them anywhere.
328
+ * The container should be `position: relative/absolute` over the canvas.
329
+ */
330
+ function attachTouchControls(engine, container, opts = {}) {
331
+ const controls = engine.input.touchControls();
332
+ if (controls.length === 0) return null;
333
+ if (!opts.force && !isCoarsePointer()) return null;
334
+ const doc = opts.doc ?? (typeof document !== "undefined" ? document : null);
335
+ if (!doc) return null;
336
+ if (typeof container.appendChild !== "function") return null;
337
+ return new TouchControls(engine, container, controls, doc);
338
+ }
339
+ function isCoarsePointer() {
340
+ return typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches;
341
+ }
342
+ /**
343
+ * Retarget moves and ups back to this element — and never at the cost of the press.
344
+ *
345
+ * `setPointerCapture` THROWS `NotFoundError` for a pointerId that is not
346
+ * currently active, and `?.` guards a missing method rather than a throwing
347
+ * one. It ran BEFORE `feed()` — the try/catch that would have absorbed it was
348
+ * the very next line — so a synthetic `pointerdown` lost its press entirely.
349
+ *
350
+ * Real fingers are unaffected: a real touch is an active pointer. What it broke
351
+ * was every `dispatchEvent`-based driver, which is how a touch game is tested
352
+ * at all. And the instrument agreed with the bug: happy-dom's
353
+ * `setPointerCapture` throws nothing, so a harness printed `ALL CHECKS PASSED`
354
+ * while Chrome lost the same game. Controlled A/B, one page, one autopilot, one
355
+ * variable:
356
+ *
357
+ * capture throws gameover @6.48s, 7/9 shards, 19 uncaught errors
358
+ * capture guarded WON @6.30s, 9/9 shards, 0 errors
359
+ *
360
+ * Worse than losing the press: `activePointer` is assigned first, so the
361
+ * matching `pointerup` still fired `releaseAction` — a release the state
362
+ * machine never earned.
363
+ */
364
+ function capturePointer(el, pointerId) {
365
+ try {
366
+ el.setPointerCapture?.(pointerId);
367
+ } catch {}
368
+ }
369
+ var TouchControls = class {
370
+ elements = [];
371
+ detachers = [];
372
+ constructor(engine, container, controls, doc) {
373
+ let buttonIndex = 0;
374
+ let joystickIndex = 0;
375
+ for (const control of controls) if (control.kind === "joystick") {
376
+ this.buildJoystick(engine, container, control.action, joystickIndex, doc);
377
+ joystickIndex += 1;
378
+ } else {
379
+ this.buildButton(engine, container, control.action, buttonIndex, doc);
380
+ buttonIndex += 1;
381
+ }
382
+ }
383
+ dispose() {
384
+ for (const detach of this.detachers) detach();
385
+ for (const el of this.elements) el.remove();
386
+ this.elements.length = 0;
387
+ this.detachers.length = 0;
388
+ }
389
+ buildJoystick(engine, container, action, index, doc) {
390
+ const base = doc.createElement("div");
391
+ style(base, {
392
+ position: "absolute",
393
+ left: `calc(${24 + index * 140}px + env(safe-area-inset-left))`,
394
+ bottom: "calc(24px + env(safe-area-inset-bottom))",
395
+ width: "120px",
396
+ height: "120px",
397
+ borderRadius: "50%",
398
+ background: "rgba(255,255,255,0.10)",
399
+ border: "2px solid rgba(255,255,255,0.25)",
400
+ touchAction: "none",
401
+ pointerEvents: "auto",
402
+ zIndex: "20"
403
+ });
404
+ const knob = doc.createElement("div");
405
+ style(knob, {
406
+ position: "absolute",
407
+ left: "50%",
408
+ top: "50%",
409
+ width: "48px",
410
+ height: "48px",
411
+ marginLeft: "-24px",
412
+ marginTop: "-24px",
413
+ borderRadius: "50%",
414
+ background: "rgba(255,255,255,0.35)",
415
+ pointerEvents: "none"
416
+ });
417
+ base.appendChild(knob);
418
+ container.appendChild(base);
419
+ this.elements.push(base);
420
+ let activePointer = null;
421
+ let cx = 0;
422
+ let cy = 0;
423
+ let radius = 60;
424
+ const apply = (clientX, clientY) => {
425
+ const v = joystickVector(clientX - cx, clientY - cy, radius);
426
+ feed(() => engine.input.setActionVector(action, v.x, v.y));
427
+ knob.style.transform = `translate(${v.x * radius * .6}px, ${v.y * radius * .6}px)`;
428
+ };
429
+ const onDown = (e) => {
430
+ activePointer = e.pointerId;
431
+ const rect = base.getBoundingClientRect();
432
+ cx = rect.left + rect.width / 2;
433
+ cy = rect.top + rect.height / 2;
434
+ radius = rect.width / 2;
435
+ capturePointer(base, e.pointerId);
436
+ apply(e.clientX, e.clientY);
437
+ };
438
+ const onMove = (e) => {
439
+ if (e.pointerId === activePointer) apply(e.clientX, e.clientY);
440
+ };
441
+ const release = (e) => {
442
+ if (e.pointerId !== activePointer) return;
443
+ activePointer = null;
444
+ knob.style.transform = "";
445
+ feed(() => engine.input.setActionVector(action, 0, 0));
446
+ };
447
+ base.addEventListener("pointerdown", onDown);
448
+ base.addEventListener("pointermove", onMove);
449
+ base.addEventListener("pointerup", release);
450
+ base.addEventListener("pointercancel", release);
451
+ this.detachers.push(() => {
452
+ base.removeEventListener("pointerdown", onDown);
453
+ base.removeEventListener("pointermove", onMove);
454
+ base.removeEventListener("pointerup", release);
455
+ base.removeEventListener("pointercancel", release);
456
+ });
457
+ }
458
+ buildButton(engine, container, action, index, doc) {
459
+ const button = doc.createElement("div");
460
+ button.textContent = action;
461
+ style(button, {
462
+ position: "absolute",
463
+ right: "calc(24px + env(safe-area-inset-right))",
464
+ bottom: `calc(${24 + index * 80}px + env(safe-area-inset-bottom))`,
465
+ width: "64px",
466
+ height: "64px",
467
+ borderRadius: "50%",
468
+ background: "rgba(255,255,255,0.12)",
469
+ border: "2px solid rgba(255,255,255,0.3)",
470
+ color: "rgba(255,255,255,0.85)",
471
+ font: "600 12px system-ui, sans-serif",
472
+ display: "flex",
473
+ alignItems: "center",
474
+ justifyContent: "center",
475
+ userSelect: "none",
476
+ touchAction: "none",
477
+ pointerEvents: "auto",
478
+ zIndex: "20"
479
+ });
480
+ container.appendChild(button);
481
+ this.elements.push(button);
482
+ let activePointer = null;
483
+ const onDown = (e) => {
484
+ activePointer = e.pointerId;
485
+ feed(() => engine.input.pressAction(action));
486
+ capturePointer(button, e.pointerId);
487
+ button.style.background = "rgba(255,255,255,0.3)";
488
+ };
489
+ const release = (e) => {
490
+ if (e.pointerId !== activePointer) return;
491
+ activePointer = null;
492
+ feed(() => engine.input.releaseAction(action));
493
+ button.style.background = "rgba(255,255,255,0.12)";
494
+ };
495
+ button.addEventListener("pointerdown", onDown);
496
+ button.addEventListener("pointerup", release);
497
+ button.addEventListener("pointercancel", release);
498
+ this.detachers.push(() => {
499
+ button.removeEventListener("pointerdown", onDown);
500
+ button.removeEventListener("pointerup", release);
501
+ button.removeEventListener("pointercancel", release);
502
+ });
503
+ }
504
+ };
505
+ /**
506
+ * A gesture can outlive its action by one frame (scene swap redeclares the
507
+ * input map while a finger is down; createGame rebuilds the overlay right
508
+ * after) — swallow only that race, never normal errors.
509
+ */
510
+ function feed(fn) {
511
+ try {
512
+ fn();
513
+ } catch {}
514
+ }
515
+ function style(el, rules) {
516
+ for (const [key, value] of Object.entries(rules)) el.style[key] = value;
517
+ }
518
+ //#endregion
519
+ export { applySchemaProps as a, getNodeSchema as c, mergeStaticProps as d, mergeStaticSignals as f, nearestName as g, didYouMean as h, joystickVector as i, getNodeSignals as l, registeredTypes as m, attachTouchControls as n, clearRegistry as o, registerNode as p, capturePointer as r, createNode as s, TouchControls as t, getNodeType as u };
package/dist/vite.d.ts CHANGED
@@ -76,6 +76,24 @@ interface FrameEndpointOptions {
76
76
  }
77
77
  declare function serveFrameEndpoints(host: FrameHost, version: string, opts?: FrameEndpointOptions): void;
78
78
  //#endregion
79
+ //#region src/vite/options.d.ts
80
+ /**
81
+ * A plugin option the plugin does not know is a hard error, like every other
82
+ * key this engine reads.
83
+ *
84
+ * Scene props have had `UNKNOWN_PROP` — "Valid props: […]" — since the first
85
+ * milestone. A vite plugin's options had nothing: `incantoScenes({
86
+ * strictBehavior: true })` type-checked green, built green and did NOTHING,
87
+ * because the scaffolded `tsconfig.json` never pointed the compiler at
88
+ * `vite.config.ts` in the first place.
89
+ *
90
+ * That `include` is fixed too, but it cannot be the whole answer. TypeScript's
91
+ * excess-property check only fires on a FRESH object literal — hoist the
92
+ * options into a `const` and it passes again — and a `vite.config.js` is never
93
+ * type-checked at all. This is the half that holds either way.
94
+ */
95
+ declare function onlyKnownOptions(plugin: string, opts: object, known: readonly string[]): void;
96
+ //#endregion
79
97
  //#region src/vite/static-inspect.d.ts
80
98
  interface StaticInspectHost extends FrameHost {
81
99
  /** Handle `/__incanto/pull` and `/__incanto/push`; returns false if not ours. */
@@ -92,41 +110,6 @@ interface StaticInspectHost extends FrameHost {
92
110
  declare function createStaticInspectHost(root: string): StaticInspectHost;
93
111
  //#endregion
94
112
  //#region src/vite/trim.d.ts
95
- /**
96
- * Leave out of the BUILD what this game can never run.
97
- *
98
- * A 2D game's `dist/` contains the 3D adapter and rapier3d. Measured on the
99
- * shipped `platformer-2d` flagship:
100
- *
101
- * ```
102
- * 2236503 assets/rapier-*.js # rapier3d
103
- * 1692150 assets/rapier-*.js # rapier2d — the one it uses
104
- * 471186 assets/3d-*.js # the 3D adapter
105
- * ─────────
106
- * 5942650 total → 45.6% of it can never execute
107
- * ```
108
- *
109
- * Nothing is wrong at RUNTIME: the chunks are lazy and never requested. This is
110
- * deploy weight — a CDN, a jam upload, an agent8 zip — and the only path that
111
- * drags them in is the live game⇄editor switch: `createGame*` imports
112
- * `incanto/editor`, and the editor imports `incanto/3d` so it can offer 3D node
113
- * types.
114
- *
115
- * `editor: false` on `createGame*` is a RUNTIME option and cannot remove bytes
116
- * from a bundle; it changed the artifact by 10 bytes while the docs said it
117
- * "opts out". A build-time decision needs a build-time lever, which is this.
118
- *
119
- * ```ts
120
- * import { incantoScenes, incantoTrim } from 'incanto/vite';
121
- * export default defineConfig({
122
- * plugins: [incantoScenes(), incantoTrim({ editor: false })],
123
- * });
124
- * ```
125
- *
126
- * Build only — the dev server keeps the editor, so `☰ debug ▸ ✎ edit this
127
- * scene` works while you are building the game and is simply absent from what
128
- * you ship.
129
- */
130
113
  interface IncantoTrimOptions {
131
114
  /**
132
115
  * Keep the in-game scene editor in the build. Default `false` when this
@@ -267,7 +250,7 @@ interface LibraryReq {
267
250
  interface LibraryRes {
268
251
  statusCode: number;
269
252
  setHeader(name: string, value: string): void;
270
- end(body?: string): void;
253
+ end(body?: string | Uint8Array): void;
271
254
  }
272
255
  interface IncantoLibraryOptions {
273
256
  /**
@@ -327,4 +310,4 @@ declare function serveDeviceAuth(req: LibraryReq, res: LibraryRes, opts?: Incant
327
310
  /** @internal Exported for tests — the whole request/response behaviour. */
328
311
  declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
329
312
  //#endregion
330
- export { type FrameHost, type FrameReq, type FrameRes, IncantoLibraryOptions, IncantoScenesOptions, type IncantoTrimOptions, ProjectFileEntry, type StaticInspectHost, createStaticInspectHost, discoverScenes, incantoLibrary, incantoScenes, incantoTrim, listeningPorts, maskToken, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };
313
+ export { type FrameHost, type FrameReq, type FrameRes, IncantoLibraryOptions, IncantoScenesOptions, type IncantoTrimOptions, ProjectFileEntry, type StaticInspectHost, createStaticInspectHost, discoverScenes, incantoLibrary, incantoScenes, incantoTrim, listeningPorts, maskToken, onlyKnownOptions, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };