incanto 0.48.0 → 0.50.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 (54) hide show
  1. package/bin/incanto-model.mjs +13 -1
  2. package/bin/incanto-play.mjs +42 -17
  3. package/bin/incanto-playtest.mjs +7 -0
  4. package/bin/incanto-verify.mjs +10 -2
  5. package/dist/2d.d.ts +21 -12
  6. package/dist/2d.js +57 -3
  7. package/dist/3d.d.ts +38 -1
  8. package/dist/3d.js +114 -4
  9. package/dist/{create-game-CniOiWzN.js → create-game-BLDjy_PW.js} +5 -5
  10. package/dist/{create-game-akjPkFv5.js → create-game-DqqxEax1.js} +5 -5
  11. package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
  12. package/dist/{environment-presets-D1b0ydTS.js → environment-presets-XFuqu5jv.js} +15 -20
  13. package/dist/{gameplay-BQOeAid6.js → gameplay-DbaI313d.js} +96 -11
  14. package/dist/gameplay.d.ts +17 -0
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +40 -2
  17. package/dist/index.js +7 -7
  18. package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
  19. package/dist/net.js +3 -3
  20. package/dist/{physics-2d-BmgXBNDB.js → physics-2d-_9VBOHn6.js} +4 -4
  21. package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DrpF5hcG.js} +9 -9
  22. package/dist/react.js +1 -1
  23. package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
  24. package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
  25. package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
  26. package/dist/{replay-CAphXMyM.d.ts → replay-DYdy1wb0.d.ts} +5 -1
  27. package/dist/{replay-DilbZgQI.js → replay-j-m6lJ4W.js} +23 -3
  28. package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
  29. package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
  30. package/dist/{src-Bl1Kire1.js → src-B3HrKuAi.js} +1 -1
  31. package/dist/{test-E4-otKqK.js → test-D16igj4C.js} +245 -38
  32. package/dist/test.d.ts +39 -7
  33. package/dist/test.js +2 -2
  34. package/dist/vite.js +2 -2
  35. package/editor/assets/{agent8-_007gPF8.js → agent8-t3kl5q9K.js} +1 -1
  36. package/editor/assets/{debug-0DI_MJaq.js → debug-Bu3eeAlO.js} +1 -1
  37. package/editor/assets/{index-B-6eYZEi.js → index-Df5g8ofT.js} +92 -92
  38. package/editor/index.html +1 -1
  39. package/package.json +1 -1
  40. package/schemas/scene.schema.json +10 -3
  41. package/skills/incanto-3d-character.md +13 -2
  42. package/skills/incanto-3d-models.md +40 -0
  43. package/skills/incanto-assets.md +15 -0
  44. package/skills/incanto-building-2d-games.md +57 -5
  45. package/skills/incanto-building-3d-games.md +7 -0
  46. package/skills/incanto-gameplay-behaviors.md +29 -1
  47. package/skills/incanto-hud.md +39 -0
  48. package/skills/incanto-node-reference.md +5 -3
  49. package/skills/incanto-physics-and-input.md +1 -1
  50. package/skills/incanto-playtesting.md +22 -4
  51. package/skills/incanto-verifying-your-game.md +14 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/package.json +1 -1
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { E as Signal, _ as InputMap, b as behaviorSchema, f as Node, g as resolveOrderGroups, m as ORDER_GROUP_BASE, v as diagnose } from "./loader-r49nDwB4.js";
1
+ import { E as Signal, _ as InputMap, b as behaviorSchema, f as Node, g as resolveOrderGroups, m as ORDER_GROUP_BASE, v as diagnose } from "./loader-DwazzlQb.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { l as registerNode } from "./registry-IyWCGe4q.js";
@@ -2558,28 +2558,73 @@ var UiText = class extends HudWidgetBase {
2558
2558
  static props = {
2559
2559
  ...HudWidgetBase.props,
2560
2560
  text: { default: "" },
2561
+ format: { default: "" },
2561
2562
  size: { default: 16 },
2562
2563
  color: { default: "#ffffff" },
2563
2564
  shadow: { default: true }
2564
2565
  };
2565
2566
  text = "";
2567
+ /**
2568
+ * A template with a `{}` slot that `setText` fills — `"Gems {} / 8"`.
2569
+ *
2570
+ * Empty (the default) means `setText` replaces the whole line. Resolved at
2571
+ * PAINT like `text`, so `"@t:hud.gems"` works and changing locale re-reads
2572
+ * it: the value lives here, the words live in the strings table.
2573
+ */
2574
+ format = "";
2566
2575
  size = 16;
2567
2576
  color = "#ffffff";
2568
2577
  shadow = true;
2569
2578
  last = "\0";
2579
+ /** null until something has actually set a value — see `shown`. */
2580
+ slot = null;
2581
+ /**
2582
+ * Put a signal's value on screen — `scoreChanged → setText`.
2583
+ *
2584
+ * A `connections[]` handler must be a METHOD and `text` is a property, so the
2585
+ * one wire every game needs (score to screen) could not be declared: every
2586
+ * HUD began with a behavior whose whole job was one assignment.
2587
+ */
2588
+ setText(value) {
2589
+ this.slot = String(value ?? "");
2590
+ if (!this.format) this.text = this.slot;
2591
+ }
2592
+ /**
2593
+ * What it actually paints, after the template and the locale.
2594
+ *
2595
+ * Before the first value arrives, a formatted widget shows `text` — its
2596
+ * authored opening line. Filling the slot with nothing instead would put
2597
+ * `"Gems / 8"` on screen for every game's first frame, and a score line
2598
+ * that starts blank looks broken rather than empty.
2599
+ */
2600
+ get shown() {
2601
+ if (this.format && this.slot !== null) return this._t(this.format).replace("{}", this.slot);
2602
+ return this._t(this.text);
2603
+ }
2570
2604
  _build() {
2571
2605
  const el = document.createElement("div");
2572
2606
  el.style.cssText = `font-size:${this.size}px;color:${this.color};${this.shadow ? "text-shadow:0 1px 3px rgba(0,0,0,.7);" : ""}white-space:pre;`;
2573
2607
  return el;
2574
2608
  }
2575
2609
  _sync() {
2576
- const shown = this._t(this.text);
2610
+ const shown = this.shown;
2577
2611
  if (shown !== this.last && this._element) {
2578
2612
  this.last = shown;
2579
2613
  this._element.textContent = shown;
2580
2614
  }
2581
2615
  }
2582
2616
  };
2617
+ /**
2618
+ * A bar fed the wrong argument shows a plausible wrong number forever.
2619
+ *
2620
+ * The trap is one signal away: `Health.damaged` is `(amount, current)`, so
2621
+ * wiring it to `setValue` paints the DAMAGE as the health and looks like a
2622
+ * working health bar. Say which signal has the shape a bar wants.
2623
+ */
2624
+ function requireNumber(value, node, method) {
2625
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2626
+ throw new IncantoError("PROP_TYPE_MISMATCH", `UiBar '${node.getPath()}'.${method} needs a number, got ${JSON.stringify(value)}. Check what the wired signal passes FIRST — 'damaged' is (amount, current), 'healthChanged' is (current, max), which is the shape a bar wants.`);
2627
+ }
2583
2628
  /** A labeled progress bar (health, stamina, reload, boss HP). */
2584
2629
  var UiBar = class extends HudWidgetBase {
2585
2630
  static typeName = "UiBar";
@@ -2612,6 +2657,21 @@ var UiBar = class extends HudWidgetBase {
2612
2657
  get ratio() {
2613
2658
  return this.max > 0 ? Math.min(1, Math.max(0, this.value / this.max)) : 0;
2614
2659
  }
2660
+ /**
2661
+ * Drive the bar from a signal — `healthChanged → setValue`.
2662
+ *
2663
+ * Takes the max as a second argument because that is the shape the signal
2664
+ * already has, so a bar whose ceiling moves (a max-HP upgrade) needs no
2665
+ * second wire.
2666
+ */
2667
+ setValue(value, max) {
2668
+ this.value = requireNumber(value, this, "setValue");
2669
+ if (max !== void 0) this.max = requireNumber(max, this, "setValue");
2670
+ }
2671
+ /** Move the ceiling on its own, leaving the fill where it is. */
2672
+ setMax(max) {
2673
+ this.max = requireNumber(max, this, "setMax");
2674
+ }
2615
2675
  _build() {
2616
2676
  const wrap = document.createElement("div");
2617
2677
  wrap.style.cssText = "display:flex;align-items:center;gap:6px;";
@@ -62,7 +62,11 @@ interface ReplayJson {
62
62
  /** File-format marker for forward compatibility. */
63
63
  replay: 1;
64
64
  /** Tick timestamps (ms) exactly as the loop delivered them. */
65
- ticks: number[];
65
+ /**
66
+ * One entry per recorded FRAME: the `nowMs` it was ticked with, or `null` for
67
+ * a fixed `step()`. Older recordings are all numbers and still replay.
68
+ */
69
+ ticks: Array<number | null>;
66
70
  events: ReplayEvent[];
67
71
  }
68
72
  interface Recorder {
@@ -1,4 +1,4 @@
1
- import { M as translationKey } from "./register-BSXV8T9F.js";
1
+ import { M as translationKey } from "./register-uvaZj1KX.js";
2
2
  //#region src/core/audit.ts
3
3
  const ANIMATED_TYPES = new Set([
4
4
  "Particles2D",
@@ -109,13 +109,25 @@ function auditStrings(scene) {
109
109
  }
110
110
  //#endregion
111
111
  //#region src/core/replay.ts
112
+ /**
113
+ * Every entry point that can change what the game is being told, because one
114
+ * left out is a replay that reproduces a character standing still.
115
+ *
116
+ * `setActionVector` was the omission that mattered: the playtest driver moves
117
+ * with it and presses buttons with `pressAction`, so its replays recorded all
118
+ * the jumping and none of the walking, and played back as a character hopping
119
+ * on the spot where the real run had walked off a ledge.
120
+ */
112
121
  const RECORDED_METHODS = [
113
122
  "handleKey",
114
123
  "handleMouseButton",
115
124
  "handlePointerMove",
116
125
  "handleWheel",
117
126
  "pressAction",
118
- "releaseAction"
127
+ "releaseAction",
128
+ "setActionVector",
129
+ "setPointerPosition",
130
+ "clearInjected"
119
131
  ];
120
132
  /**
121
133
  * Tap the engine's input entry points and tick clock. Recording starts at
@@ -139,17 +151,23 @@ function startRecording(engine) {
139
151
  };
140
152
  }
141
153
  const originalTick = engine.tick.bind(engine);
154
+ const originalStep = engine.step.bind(engine);
142
155
  const engineAny = engine;
143
156
  engineAny.tick = (nowMs) => {
144
157
  ticks.push(nowMs);
145
158
  originalTick(nowMs);
146
159
  };
160
+ engineAny.step = () => {
161
+ ticks.push(null);
162
+ originalStep();
163
+ };
147
164
  let stopped = false;
148
165
  return { stop() {
149
166
  if (!stopped) {
150
167
  stopped = true;
151
168
  for (const [name, fn] of originals) input[name] = fn;
152
169
  engineAny.tick = originalTick;
170
+ engineAny.step = originalStep;
153
171
  }
154
172
  return {
155
173
  replay: 1,
@@ -173,7 +191,9 @@ function replay(engine, recording, opts) {
173
191
  if (typeof fn === "function") fn.apply(engine.input, args);
174
192
  cursor += 1;
175
193
  }
176
- engine.tick(recording.ticks[i]);
194
+ const at = recording.ticks[i];
195
+ if (at === null) engine.step();
196
+ else engine.tick(at);
177
197
  opts?.onTick?.(i);
178
198
  }
179
199
  }
@@ -0,0 +1,44 @@
1
+ //#region src/core/sprite-animation.d.ts
2
+ interface AnimationDef {
3
+ /**
4
+ * Two numbers = inclusive [start, end] frame RANGE.
5
+ * Any other length = explicit frame list.
6
+ */
7
+ frames: number[];
8
+ fps: number;
9
+ loop?: boolean;
10
+ }
11
+ /**
12
+ * A clip, or the NAME of another clip in the same map.
13
+ *
14
+ * The alias exists because the character a game is built from and the character
15
+ * a controller describes are not the same vocabulary.
16
+ * `CharacterController2D`/`3D` emit `idle`/`run`/`jump`/`fall`/`wallSlide`/
17
+ * `dash`; the sheets in the engine's own catalog carry `idle`/`move`/`attack`,
18
+ * and no sheet anywhere ships a `fall`. So the documented one-line wiring —
19
+ * `movementStateChanged → play` — asked for clips that do not exist, and the
20
+ * hard error was correct and useless: there is nothing to rename it TO.
21
+ *
22
+ * An alias is the missing half. `"fall": "idle"` says what to show while
23
+ * falling, in the scene, next to everything else about that character.
24
+ */
25
+ type AnimationEntry = AnimationDef | string;
26
+ /**
27
+ * Follow aliases to the clip that actually plays.
28
+ *
29
+ * Chains are allowed (`fall → jump → idle`) because they are how a fallback
30
+ * ladder reads, and a cycle is a hard error rather than a hang.
31
+ */
32
+ declare function resolveAnimation(animations: Record<string, AnimationEntry>, name: string, owner: string): {
33
+ name: string;
34
+ def: AnimationDef;
35
+ };
36
+ /**
37
+ * `[start, end]` is a RANGE; anything else is an explicit list.
38
+ *
39
+ * Two ascending numbers are overwhelmingly a range in every sheet format, and
40
+ * a two-frame flip-book is spelled `[3, 3, 4, 4]` or a descending pair.
41
+ */
42
+ declare function resolveFrames(frames: number[], name: string): number[];
43
+ //#endregion
44
+ export { resolveFrames as i, AnimationEntry as n, resolveAnimation as r, AnimationDef as t };
@@ -1,3 +1,4 @@
1
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
1
2
  import { n as jsonEquals } from "./json-BLk7H2Qa.js";
2
3
  //#region src/core/particle-presets.ts
3
4
  const PARTICLE_PRESETS = {
@@ -264,4 +265,65 @@ var ParticleSim = class {
264
265
  }
265
266
  };
266
267
  //#endregion
267
- export { applyParticlePreset as i, PARTICLE_PRESETS as n, PARTICLE_PRESET_NAMES as r, ParticleSim as t };
268
+ //#region src/core/sprite-animation.ts
269
+ /**
270
+ * The animation MAP a spritesheet node is driven by — shared by 2D and 3D.
271
+ *
272
+ * Core, not an adapter: no three.js, and the two sprite nodes had grown
273
+ * byte-identical copies of the frame reader.
274
+ */
275
+ /**
276
+ * Follow aliases to the clip that actually plays.
277
+ *
278
+ * Chains are allowed (`fall → jump → idle`) because they are how a fallback
279
+ * ladder reads, and a cycle is a hard error rather than a hang.
280
+ */
281
+ function resolveAnimation(animations, name, owner) {
282
+ const seen = [];
283
+ let key = name;
284
+ for (;;) {
285
+ const entry = animations[key];
286
+ if (entry === void 0) {
287
+ const via = seen.length > 0 ? ` (via ${seen.join(" → ")})` : "";
288
+ throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${key}' on '${owner}'${via}. Available: [${Object.keys(animations).join(", ")}].`);
289
+ }
290
+ if (typeof entry !== "string") return {
291
+ name: key,
292
+ def: entry
293
+ };
294
+ if (seen.includes(key)) throw new IncantoError("UNKNOWN_ANIMATION", `Animation aliases on '${owner}' point in a circle: ${[...seen, key].join(" → ")}.`);
295
+ seen.push(key);
296
+ key = entry;
297
+ }
298
+ }
299
+ /**
300
+ * Every alias resolves — checked once, at ready, rather than on the frame the
301
+ * character first jumps.
302
+ *
303
+ * A cosmetic map that only fails the moment a state is first entered is the
304
+ * shape of bug that reaches players: the game loads, plays, and dies on a
305
+ * ledge twenty minutes in.
306
+ */
307
+ function validateAnimationAliases(animations, owner) {
308
+ for (const [name, entry] of Object.entries(animations)) if (typeof entry === "string") resolveAnimation(animations, name, owner);
309
+ }
310
+ /**
311
+ * `[start, end]` is a RANGE; anything else is an explicit list.
312
+ *
313
+ * Two ascending numbers are overwhelmingly a range in every sheet format, and
314
+ * a two-frame flip-book is spelled `[3, 3, 4, 4]` or a descending pair.
315
+ */
316
+ function resolveFrames(frames, name) {
317
+ if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
318
+ if (frames.length === 2) {
319
+ const [start, end] = frames;
320
+ if (end >= start) {
321
+ const out = [];
322
+ for (let f = start; f <= end; f++) out.push(f);
323
+ return out;
324
+ }
325
+ }
326
+ return [...frames];
327
+ }
328
+ //#endregion
329
+ export { PARTICLE_PRESETS as a, ParticleSim as i, resolveFrames as n, PARTICLE_PRESET_NAMES as o, validateAnimationAliases as r, applyParticlePreset as s, resolveAnimation as t };
@@ -161,6 +161,6 @@ function newUid() {
161
161
  //#endregion
162
162
  //#region src/index.ts
163
163
  /** Engine version. Kept in sync with package.json by the release pipeline. */
164
- const VERSION = "0.48.0";
164
+ const VERSION = "0.50.0";
165
165
  //#endregion
166
166
  export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };