incanto 0.55.0 → 0.57.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 (61) hide show
  1. package/bin/incanto-verify.mjs +9 -2
  2. package/dist/2d.d.ts +59 -7
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +34 -4
  5. package/dist/3d.js +5 -5
  6. package/dist/{behavior-CyQoSu4n.d.ts → behavior-l08AEbq9.d.ts} +5 -0
  7. package/dist/{create-game-CzK9_pzg.js → create-game-C5jQYPah.js} +28 -71
  8. package/dist/{create-game-BRt6XKmP.js → create-game-DpbUrMOQ.js} +146 -7
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-MNLMAcbz.js → duplicate-BPLZDZpd.js} +1 -1
  11. package/dist/{environment-presets-DAbEdEwh.js → environment-presets-CvvQr_bJ.js} +118 -24
  12. package/dist/{frame-report-njybhZon.js → frame-report-BSMny7oe.js} +1 -1
  13. package/dist/{frame-report-DZ70IY26.d.ts → frame-report-DCnHFmto.d.ts} +1 -1
  14. package/dist/{gameplay-CZ2yq37J.js → gameplay-BVphcxmE.js} +163 -14
  15. package/dist/gameplay.d.ts +81 -2
  16. package/dist/gameplay.js +2 -2
  17. package/dist/index.d.ts +45 -5
  18. package/dist/index.js +8 -9
  19. package/dist/{loader-DhI1jFW_.d.ts → loader-BbEMTuWg.d.ts} +1 -1
  20. package/dist/{loader-BTkHYrQn.js → loader-BcrRSjxB.js} +682 -682
  21. package/dist/net.d.ts +2 -2
  22. package/dist/net.js +1 -1
  23. package/dist/{pathfinding-CXGCpRQe.d.ts → pathfinding-mEN4V1CU.d.ts} +1 -1
  24. package/dist/{physics-2d-CfWAggJ1.js → physics-2d-BLcvEFDR.js} +2 -2
  25. package/dist/{physics-3d-C_ZJ6f_d.js → physics-3d-QBrfIT2Y.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-en63AEZO.js → register-C6ZBFRjd.js} +63 -60
  29. package/dist/{register-p48lHE2o.js → register-Ch70uByv.js} +1615 -1502
  30. package/dist/{replay-ePMz26jw.d.ts → replay-Dw6gMlYA.d.ts} +1 -1
  31. package/dist/{replay-t1pP0gQg.js → replay-s7I2GstT.js} +31 -11
  32. package/dist/sheet-grid-BT6N_Bjs.js +59 -0
  33. package/dist/{split-screen-BsdOHbzP.d.ts → split-screen-B0baBwxI.d.ts} +5 -1
  34. package/dist/{split-screen-CSb_uZ6W.js → split-screen-DLsUrleX.js} +14 -3
  35. package/dist/{sprite-animation-7qvUxF6Z.js → sprite-animation-C0wXLBZJ.js} +8 -4
  36. package/dist/{src-BVOVHRL0.js → src-CGjmPw65.js} +55 -2
  37. package/dist/{teardown-C7uVSJvx.js → teardown-Cs113S9F.js} +74 -2
  38. package/dist/{test-Ca5eqELC.js → test-it1VekWs.js} +28 -19
  39. package/dist/test.d.ts +4 -4
  40. package/dist/test.js +2 -2
  41. package/dist/{touch-BnMyy9tr.js → touch-DESwnpOc.js} +173 -1
  42. package/dist/vite.d.ts +1 -1
  43. package/dist/vite.js +3 -3
  44. package/editor/assets/{agent8-Cz4oHPtm.js → agent8-CGT7r3Mb.js} +1 -1
  45. package/editor/assets/{debug-r-Fi9nZh.js → debug-BxWSIHG3.js} +1 -1
  46. package/editor/assets/{index-BfqmvXYD.js → index-CV1m-aX5.js} +62 -62
  47. package/editor/index.html +1 -1
  48. package/package.json +1 -1
  49. package/skills/incanto-assets.md +5 -1
  50. package/skills/incanto-audio.md +7 -1
  51. package/skills/incanto-building-3d-games.md +18 -5
  52. package/skills/incanto-gameplay-behaviors.md +21 -1
  53. package/skills/incanto-hud.md +6 -0
  54. package/skills/incanto-performance.md +5 -0
  55. package/skills/incanto-physics-and-input.md +14 -0
  56. package/skills/incanto-save-slots.md +2 -1
  57. package/skills/incanto-verifying-your-game.md +4 -0
  58. package/templates-app/beacon-isle-3d/package.json +1 -1
  59. package/templates-app/tps-3d/package.json +1 -1
  60. package/templates-app/village-quest-3d/package.json +1 -1
  61. package/dist/log-report-CPFm4OXf.js +0 -173
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-BfqmvXYD.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-CV1m-aX5.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -182,7 +182,11 @@ frame 40. Those frames draw whatever is at the wrong end of the sheet.
182
182
  ```
183
183
 
184
184
  The suggested sizes are the ones that actually divide your image, so the fix is
185
- usually in the message. `bunx incanto-assets info <name>` prints the real frame
185
+ usually in the message. **All three nodes that cut a grid out of an image check
186
+ it** — `AnimatedSprite2D`, `AnimatedSprite3D` and `TileMap2D` — because they do
187
+ the same floor-division and it fails the same silent way. An
188
+ `AnimatedSprite3D` with a sheet and no `frameWidth`/`frameHeight` also says so
189
+ rather than drawing a mob-shaped hole. `bunx incanto-assets info <name>` prints the real frame
186
190
  size for a built-in, and the editor's 📚 picker fills it in from the catalog's
187
191
  metadata — or leaves it BLANK when the catalog does not carry it, because an
188
192
  invented frame size is exactly this bug.
@@ -350,6 +350,11 @@ background loop authored in scene JSON. Wire `crossfadeTo` from gameplay:
350
350
  this.engine.music.crossfadeTo('/audio/boss.mp3', 3);
351
351
  ```
352
352
 
353
+ **A track that will never play says so.** A 404 or an undecodable file lands in
354
+ the engine log (`[incanto] music '/audio/boss.mp3' will not play — could not
355
+ load.`) instead of being indistinguishable from a game with no music. The
356
+ autoplay gate is still just the gate — a track waiting for a tap is not an error.
357
+
353
358
  > Large music files are NOT bundled — reference them by URL (see §4).
354
359
 
355
360
  **Headless the state machine still runs and `current` still updates** — nothing
@@ -448,7 +453,8 @@ bricking the game.
448
453
  ### Quality tiers
449
454
 
450
455
  ```ts
451
- import { qualityEnvironment, readDeviceHints, suggestQuality, setEnvironment3D } from 'incanto';
456
+ import { qualityEnvironment, readDeviceHints, suggestQuality } from 'incanto';
457
+ import { setEnvironment3D } from 'incanto/3d'; // it applies a 3D environment
452
458
 
453
459
  const tier = engine.settings.get('quality') ?? suggestQuality(readDeviceHints());
454
460
  setEnvironment3D(engine, qualityEnvironment(tier));
@@ -203,11 +203,24 @@ A nameplate, a sign, a damage number:
203
203
  - It IS a `Sprite3D`, so `anchor`, `tint`, `opacity`, `alphaTest`, `renderOrder`
204
204
  and `orderGroup` all work exactly as they do there.
205
205
 
206
- A **damage number** is this node plus two lines: set `text`, then float it up
207
- and fade `opacity` to 0 over ~0.6 s and `queueFree()`. Re-rasterising is
208
- handled — the texture is rebuilt only when the text (or size/colour/font)
209
- changes, and the old one is disposed, so a number per hit does not leak a
210
- texture per hit.
206
+ A **damage number** is this node plus the `FloatAway` behavior, and no
207
+ TypeScript of your own:
208
+
209
+ ```json
210
+ { "name": "Hit", "type": "Label3D",
211
+ "props": { "text": "12", "height": 0.35, "color": "#ff5a5a" },
212
+ "script": { "name": "FloatAway", "props": { "rise": 1.2, "seconds": 0.7, "drift": 0.3 } } }
213
+ ```
214
+
215
+ It rises with an ease-out (constant speed reads as a lift, not a hit), holds at
216
+ full opacity for `hold` of its life so the number is readable, fades, and frees
217
+ itself — so a hundred numbers a fight leave nothing in the tree. `drift` keeps
218
+ ten at once from stacking into one. Spawning is `duplicateNode` + set `text` and
219
+ `position`.
220
+
221
+ Re-rasterising is handled: the texture is rebuilt only when the text (or
222
+ size/colour/font) changes, and the old one is disposed, so a number per hit does
223
+ not leak a texture per hit.
211
224
 
212
225
  | Prop | Default | Notes |
213
226
  |---|---|---|
@@ -20,7 +20,8 @@ with `gameplay: false`, or register a subset yourself:
20
20
  import { registerGameplayBehaviors } from 'incanto/gameplay';
21
21
  registerGameplayBehaviors(); // all of them (default in createGame)
22
22
 
23
- import { Health, Pickup, registerBehavior } from 'incanto/gameplay';
23
+ import { registerBehavior } from 'incanto';
24
+ import { Health, Pickup } from 'incanto/gameplay';
24
25
  registerBehavior('Health', Health); // or pick just what you use
25
26
  ```
26
27
 
@@ -828,6 +829,25 @@ import { CameraShake, Cooldown, hitStop, screenFlash } from 'incanto/gameplay';
828
829
  tested. (A stepped frame still HAPPENS while paused: `update` runs with
829
830
  `dt = 0`, which is how the pause menu's own key polling keeps working.)
830
831
 
832
+ ### `FloatAway` — rise, fade, gone
833
+
834
+ The second half of a damage number, a pickup puff, a `+50`:
835
+
836
+ ```json
837
+ { "name": "Hit", "type": "Label3D",
838
+ "props": { "text": "12", "height": 0.35 },
839
+ "script": { "name": "FloatAway", "props": { "rise": 1.2, "seconds": 0.7, "drift": 0.3 } } }
840
+ ```
841
+
842
+ `rise` (world units, negative sinks) · `seconds` · `hold` (fraction of the life
843
+ at full opacity before the fade) · `drift` (sideways, so ten at once do not
844
+ stack) · `freeOnEnd` (default true).
845
+
846
+ Works on anything with `position` and `opacity` — a `Label3D`, a `Sprite3D`
847
+ puff, a 2D `Label`. It reads the start position ONCE, so the ease does not
848
+ compound, and it frees the node a frame after it is invisible rather than on the
849
+ frame it gets there (which reads as a flicker).
850
+
831
851
  ## Did the effect fire?
832
852
 
833
853
  Sound has `engine.audio`; vision has **`engine.effects`**, and the question is
@@ -167,6 +167,12 @@ widget use `UiButton`'s `pressed`.
167
167
  Setting `.value` from a behavior updates the control and does **NOT** re-emit
168
168
  `changed` — restoring a saved setting must not fire the handler that saved it.
169
169
 
170
+ **Every prop is live.** `color`, `size`, `width`, `background`, `label` and
171
+ `anchor` are re-read each frame, so flashing the score red, growing a health bar
172
+ or moving a widget to another corner all work from a behavior — and from the
173
+ editor's inspector, which is the same code path. They used to be baked in when
174
+ the widget was built and could never be changed afterwards.
175
+
170
176
  ## Playable on a controller (focus navigation)
171
177
 
172
178
  A menu you can only click is not playable on a gamepad, and "add controller
@@ -92,6 +92,11 @@ that built a mesh), dispose it in the behavior's `onExitTree`.
92
92
  | `renderScale` | `0.25`–`2` | image softness only | **yes** |
93
93
  | antialiasing | follows the tier | jagged edges at `low` | **no — boot only** |
94
94
 
95
+ `maxFps` and `renderScale` apply in **2D as well as 3D** now — they persisted for
96
+ every game and were applied by the 3D boot only, so a 2D options menu saved a
97
+ number and changed nothing. `quality` remains a 3D tier (there are no shadows,
98
+ bloom or clouds to drop in 2D).
99
+
95
100
  ### quality
96
101
 
97
102
  **Water follows the tier too, and it is the biggest single saving.**
@@ -192,6 +192,20 @@ engine.input.pressAction('jump'); // one justPressed frame, held until
192
192
  engine.input.releaseAction('jump'); // …released (one justReleased frame)
193
193
  engine.input.setActionVector('move', 1, 0); // analog vector2; (0, 0) clears
194
194
  ```
195
+
196
+ **An action name you typed that does not exist says so.** A controller tolerates
197
+ a missing action — a game with no sprint key must not crash — but a value you
198
+ NAMED and never declared is a typo, and it used to be indistinguishable from the
199
+ tolerated case:
200
+
201
+ ```
202
+ [incanto] /Game/Player/Ctl: Unknown input action 'movement'.
203
+ Declared actions: [move, jump]. The character will not move.
204
+ ```
205
+
206
+ Once per node and action, and only when the prop differs from its default —
207
+ leaving `sprintAction` alone in a game with no sprint key is the ordinary case
208
+ and stays silent.
195
209
  Injected state combines with key state (vectors clamped to unit length).
196
210
 
197
211
  ## Patterns
@@ -146,7 +146,8 @@ The engine does not route scenes — deliberately, because only your game knows
146
146
  what a key means. What it does is record the key, so the routing is a lookup:
147
147
 
148
148
  ```ts
149
- import { createGame2D, loadScene } from 'incanto';
149
+ import { loadScene } from 'incanto';
150
+ import { createGame2D } from 'incanto/2d';
150
151
  import level1 from './level1.scene.json';
151
152
  import level2 from './level2.scene.json';
152
153
 
@@ -399,6 +399,10 @@ shouting the same thing every frame has one problem, not six hundred.
399
399
 
400
400
  ### `bunx incanto-frame` — what is actually on screen
401
401
 
402
+ Works for **2D and 3D** games alike (as does `incanto-logs`, and `game.frame()`).
403
+ Both used to be answered by the 3D boot only, so a 2D preview that was open and
404
+ drawing reported "no page answered".
405
+
402
406
  The rung above framing. `framing` PREDICTS what the camera should see from the
403
407
  scene graph; this measures what the GPU drew — the only one that can catch a
404
408
  black screen, a material that failed, or a shader drawing where it should not.
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.55.0",
17
+ "incanto": "^0.57.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -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.55.0",
16
+ "incanto": "^0.57.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -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.55.0",
16
+ "incanto": "^0.57.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -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 };