incanto 0.1.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 (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
package/dist/test.d.ts ADDED
@@ -0,0 +1,158 @@
1
+ import { c as JsonValue, i as SceneJson } from "./schema-CcoWb32N.js";
2
+ import { O as LogEntry, T as Node, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAQq7HGM.js";
3
+ import { t as LoadSceneOptions } from "./loader-Mo0KghCv.js";
4
+ import { t as IncantoError } from "./errors-BMFaY68Q.js";
5
+
6
+ //#region src/test/index.d.ts
7
+ /**
8
+ * Register every built-in node set (core + 2d + 3d + net) AND the built-in
9
+ * gameplay behaviors — so headless verification (validateScene/runScript) of a
10
+ * game built on `incanto/gameplay` works without re-passing them. Idempotent;
11
+ * a user behavior passed via `behaviors` (registered after) still wins.
12
+ */
13
+ declare function registerAllNodes(): void;
14
+ interface NodeCapture {
15
+ /** Absolute path ('/Root/Player') — stable, grep-able. */
16
+ path: string;
17
+ type: string;
18
+ uid?: string;
19
+ groups?: string[];
20
+ tags?: Record<string, JsonValue>;
21
+ /** The attached behavior's name, if any. */
22
+ script?: string;
23
+ /** CURRENT value of every schema prop (not delta — captures runtime state). */
24
+ props: Record<string, JsonValue>;
25
+ }
26
+ interface SceneCapture {
27
+ name: string;
28
+ dimension?: "2d" | "3d";
29
+ nodes: NodeCapture[];
30
+ }
31
+ /** Snapshot the live tree: every node, every schema prop's current value. */
32
+ declare function captureScene(scene: Scene): SceneCapture;
33
+ /**
34
+ * Render a capture as one grep-able line per node, showing only NON-DEFAULT
35
+ * props — the same delta discipline as scene JSON, so the interesting state
36
+ * stands out.
37
+ */
38
+ declare function describeCapture(capture: SceneCapture): string;
39
+ interface ValidateSceneOptions {
40
+ /** Real behavior classes to register (hot-replacing) before loading. */
41
+ behaviors?: Record<string, BehaviorCtor>;
42
+ /** Hard-fail on unregistered behaviors instead of stubbing them. */
43
+ strictBehaviors?: boolean;
44
+ resolveScene?: LoadSceneOptions["resolveScene"];
45
+ }
46
+ interface ValidationResult {
47
+ ok: boolean;
48
+ /** The hard load error, when ok is false — `details` is machine-readable. */
49
+ error?: IncantoError;
50
+ }
51
+ /**
52
+ * Run every hard load-time check headlessly. Unregistered behaviors are
53
+ * stubbed by default (structure-only validation, no TypeScript needed) —
54
+ * pass the real classes via `behaviors` to validate script props too.
55
+ */
56
+ declare function validateScene(json: unknown, opts?: ValidateSceneOptions): ValidationResult;
57
+ interface RunContext {
58
+ engine: Engine;
59
+ scene: Scene;
60
+ /** Simulated time of the current step, in ms. */
61
+ timeMs: number;
62
+ /** Resolve a node path relative to the scene root. */
63
+ getNode(path: string): Node;
64
+ /** Snapshot the scene right now. */
65
+ capture(): SceneCapture;
66
+ }
67
+ interface ScriptStep {
68
+ /** Simulated time at which this step applies (before that frame's tick). */
69
+ atMs: number;
70
+ /** Press a button action (held until `release`). */
71
+ press?: string;
72
+ /** Release a previously pressed button action. */
73
+ release?: string;
74
+ /** Set a vector2 action: [action, x, y]. (0,0) clears. */
75
+ vector?: [string, number, number];
76
+ /** Raw key-code fallback: [code, isDown]. */
77
+ key?: [string, boolean];
78
+ /** Arbitrary imperative hook. */
79
+ do?: (ctx: RunContext) => void;
80
+ /** Inline check: return false or throw to record a failure (run continues). */
81
+ assert?: (ctx: RunContext) => boolean | undefined;
82
+ /** Names this step in failure reports. */
83
+ label?: string;
84
+ }
85
+ interface RunFailure {
86
+ atMs: number;
87
+ label?: string;
88
+ message: string;
89
+ }
90
+ interface RunScriptOptions {
91
+ /** Total simulated duration in ms. */
92
+ durationMs: number;
93
+ steps?: ScriptStep[];
94
+ /** Real behavior classes (hot-replacing). Unregistered ones still hard-fail. */
95
+ behaviors?: Record<string, BehaviorCtor>;
96
+ /** Fixed-update rate (default 60). */
97
+ fixedHz?: number;
98
+ /** Seed for engine.rng — same seed, same run. */
99
+ seed?: number;
100
+ /** Physics: 'auto' (default — by scene dimension), '2d', '3d', or false. */
101
+ physics?: "2d" | "3d" | "auto" | false;
102
+ /** Collect a snapshot every N simulated ms. */
103
+ snapshotEveryMs?: number;
104
+ resolveScene?: LoadSceneOptions["resolveScene"];
105
+ }
106
+ interface RunResult {
107
+ /** True when no step assertion failed. */
108
+ ok: boolean;
109
+ failures: RunFailure[];
110
+ snapshots: Array<{
111
+ atMs: number;
112
+ capture: SceneCapture;
113
+ }>;
114
+ /** Everything the game logged via engine.log / this.log. */
115
+ logs: readonly LogEntry[];
116
+ finalCapture: SceneCapture;
117
+ /** Human/agent-readable run report. */
118
+ describe(): string;
119
+ }
120
+ /**
121
+ * Load a scene and play it headlessly at a fixed timestep, driving input by
122
+ * ACTION intent. This is the e2e loop without a browser: script → simulate →
123
+ * snapshot → assert.
124
+ */
125
+ declare function runScript(json: unknown, opts: RunScriptOptions): Promise<RunResult>;
126
+ interface PlaySessionOptions {
127
+ behaviors?: Record<string, BehaviorCtor>;
128
+ /** Stub unregistered behaviors (structure-only play — the CLI default without --behaviors). */
129
+ stubMissingBehaviors?: boolean;
130
+ seed?: number;
131
+ fixedHz?: number;
132
+ physics?: "2d" | "3d" | "auto" | false;
133
+ resolveScene?: LoadSceneOptions["resolveScene"];
134
+ }
135
+ interface PlaySession {
136
+ engine: Engine;
137
+ scene: Scene;
138
+ /** Simulated time so far, ms. */
139
+ readonly timeMs: number;
140
+ /** Advance the simulation by ~ms (whole fixed steps). */
141
+ step(ms: number): void;
142
+ /** The CURRENT runtime state in scene-JSON format — loadable, diffable. */
143
+ capture(): SceneJson;
144
+ /** Grep-able one-line-per-node text view of the current state. */
145
+ describe(): string;
146
+ /** Engine log entries since the last drain. */
147
+ drainLogs(): LogEntry[];
148
+ dispose(): void;
149
+ }
150
+ /**
151
+ * The headless play loop behind `incanto-play`: load → feed inputs by intent
152
+ * (`session.engine.input.pressAction(...)`) → `step(ms)` → `capture()` the
153
+ * state AS A SCENE FILE. What a screenshot is to humans, the capture is to
154
+ * agents: complete, structured, and in the exact format they already read.
155
+ */
156
+ declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
157
+ //#endregion
158
+ export { NodeCapture, PlaySession, PlaySessionOptions, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
package/dist/test.js ADDED
@@ -0,0 +1,275 @@
1
+ import { _ as registerBehavior, n as loadScene } from "./loader-CGs_G-r0.js";
2
+ import { a as Engine } from "./particle-sim-DYuSUxvK.js";
3
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
+ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
+ import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
6
+ import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
7
+ import { t as registerNodes2D } from "./register-DPEV9_9t.js";
8
+ import { t as registerNodes3D } from "./register-CNlYAS1_.js";
9
+ import { t as registerNodesNet } from "./register-Dasmnurl.js";
10
+ //#region src/test/index.ts
11
+ /**
12
+ * incanto/test — the browserless verification harness.
13
+ *
14
+ * The agent loop is "author → VERIFY → fix", and production environments
15
+ * (e.g. the agent8 VM) have no browser. This module makes the scene state
16
+ * itself the screenshot:
17
+ *
18
+ * - `validateScene(json)` — every hard load error, without a browser
19
+ * - `runScript(json, ...)` — scripted play by ACTION intent at a fixed
20
+ * timestep, with inline assertions and periodic snapshots
21
+ * - `captureScene(scene)` / `describeCapture(capture)` — a structural,
22
+ * grep-able "text screenshot" of the live tree
23
+ *
24
+ * Deterministic by construction: fixed timestep, seeded `engine.rng`,
25
+ * no requestAnimationFrame.
26
+ */
27
+ /**
28
+ * Register every built-in node set (core + 2d + 3d + net) AND the built-in
29
+ * gameplay behaviors — so headless verification (validateScene/runScript) of a
30
+ * game built on `incanto/gameplay` works without re-passing them. Idempotent;
31
+ * a user behavior passed via `behaviors` (registered after) still wins.
32
+ */
33
+ function registerAllNodes() {
34
+ registerNodes2D();
35
+ registerNodes3D();
36
+ registerNodesNet();
37
+ registerGameplayBehaviors();
38
+ }
39
+ /** Snapshot the live tree: every node, every schema prop's current value. */
40
+ function captureScene(scene) {
41
+ const nodes = [];
42
+ const walk = (node) => {
43
+ const ctor = node.constructor;
44
+ const schema = mergeStaticProps(ctor);
45
+ const props = {};
46
+ for (const key of Object.keys(schema)) props[key] = jsonClone(node[key]);
47
+ const capture = {
48
+ path: node.getPath(),
49
+ type: ctor.typeName,
50
+ props
51
+ };
52
+ if (node.uid) capture.uid = node.uid;
53
+ if (node.groups.size > 0) capture.groups = [...node.groups];
54
+ if (Object.keys(node.tags).length > 0) capture.tags = jsonClone(node.tags);
55
+ if (typeof node.script?.name === "string") capture.script = node.script.name;
56
+ nodes.push(capture);
57
+ for (const child of node.children) walk(child);
58
+ };
59
+ walk(scene.root);
60
+ const out = {
61
+ name: scene.name,
62
+ nodes
63
+ };
64
+ if (scene.dimension) out.dimension = scene.dimension;
65
+ return out;
66
+ }
67
+ /**
68
+ * Render a capture as one grep-able line per node, showing only NON-DEFAULT
69
+ * props — the same delta discipline as scene JSON, so the interesting state
70
+ * stands out.
71
+ */
72
+ function describeCapture(capture) {
73
+ const lines = [`scene ${capture.name}${capture.dimension ? ` (${capture.dimension})` : ""}`];
74
+ for (const node of capture.nodes) {
75
+ const schema = defaultsFor(node.type);
76
+ const deltas = Object.entries(node.props).filter(([key, value]) => !schema || !jsonEquals(value, schema[key]?.default ?? null)).map(([key, value]) => `${key}=${JSON.stringify(value)}`);
77
+ const extras = [
78
+ node.script ? `script=${node.script}` : "",
79
+ node.groups ? `groups=${node.groups.join(",")}` : "",
80
+ ...deltas
81
+ ].filter(Boolean);
82
+ lines.push(`${node.path} ${node.type}${extras.length ? ` ${extras.join(" ")}` : ""}`);
83
+ }
84
+ return lines.join("\n");
85
+ }
86
+ function defaultsFor(typeName) {
87
+ try {
88
+ return getNodeSchema(typeName);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Run every hard load-time check headlessly. Unregistered behaviors are
95
+ * stubbed by default (structure-only validation, no TypeScript needed) —
96
+ * pass the real classes via `behaviors` to validate script props too.
97
+ */
98
+ function validateScene(json, opts = {}) {
99
+ registerAllNodes();
100
+ for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
101
+ try {
102
+ loadScene(json, {
103
+ resolveScene: opts.resolveScene,
104
+ stubMissingBehaviors: !opts.strictBehaviors
105
+ }).root.free();
106
+ return { ok: true };
107
+ } catch (e) {
108
+ if (e instanceof IncantoError) return {
109
+ ok: false,
110
+ error: e
111
+ };
112
+ throw e;
113
+ }
114
+ }
115
+ /**
116
+ * Load a scene and play it headlessly at a fixed timestep, driving input by
117
+ * ACTION intent. This is the e2e loop without a browser: script → simulate →
118
+ * snapshot → assert.
119
+ */
120
+ async function runScript(json, opts) {
121
+ registerAllNodes();
122
+ for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
123
+ const engine = new Engine({
124
+ seed: opts.seed,
125
+ fixedHz: opts.fixedHz,
126
+ scheduler: () => () => {}
127
+ });
128
+ const scene = loadScene(structuredClone(json), { resolveScene: opts.resolveScene });
129
+ engine.setScene(scene);
130
+ const physics = opts.physics ?? "auto";
131
+ if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
132
+ const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.js").then((n) => n.r);
133
+ await enablePhysics2D(engine);
134
+ } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
135
+ const { enablePhysics3D } = await import("./physics-3d-Dl67vOLT.js").then((n) => n.r);
136
+ await enablePhysics3D(engine);
137
+ }
138
+ const failures = [];
139
+ const snapshots = [];
140
+ const ctx = {
141
+ engine,
142
+ scene,
143
+ timeMs: 0,
144
+ getNode: (path) => scene.root.getNode(path),
145
+ capture: () => captureScene(scene)
146
+ };
147
+ const applyStep = (step) => {
148
+ if (step.press) engine.input.pressAction(step.press);
149
+ if (step.release) engine.input.releaseAction(step.release);
150
+ if (step.vector) engine.input.setActionVector(...step.vector);
151
+ if (step.key) engine.input.handleKey(step.key[0], step.key[1]);
152
+ step.do?.(ctx);
153
+ if (step.assert) try {
154
+ if (step.assert(ctx) === false) failures.push({
155
+ atMs: step.atMs,
156
+ label: step.label,
157
+ message: "assert returned false"
158
+ });
159
+ } catch (e) {
160
+ failures.push({
161
+ atMs: step.atMs,
162
+ label: step.label,
163
+ message: e instanceof Error ? e.message : String(e)
164
+ });
165
+ }
166
+ };
167
+ const stepMs = 1e3 / (opts.fixedHz ?? 60);
168
+ const totalTicks = Math.round(opts.durationMs / stepMs);
169
+ const steps = [...opts.steps ?? []].sort((a, b) => a.atMs - b.atMs);
170
+ let stepIndex = 0;
171
+ let nextSnapshot = opts.snapshotEveryMs ?? Number.POSITIVE_INFINITY;
172
+ for (let i = 1; i <= totalTicks; i++) {
173
+ const t = i * stepMs;
174
+ ctx.timeMs = t;
175
+ while (stepIndex < steps.length && steps[stepIndex].atMs <= t) {
176
+ applyStep(steps[stepIndex]);
177
+ stepIndex += 1;
178
+ }
179
+ engine.step();
180
+ if (t >= nextSnapshot - 1e-6) {
181
+ snapshots.push({
182
+ atMs: Math.round(t),
183
+ capture: captureScene(scene)
184
+ });
185
+ nextSnapshot += opts.snapshotEveryMs;
186
+ }
187
+ }
188
+ for (; stepIndex < steps.length; stepIndex++) {
189
+ const missed = steps[stepIndex];
190
+ failures.push({
191
+ atMs: missed.atMs,
192
+ label: missed.label,
193
+ message: `step never executed — atMs ${missed.atMs} is beyond the ${opts.durationMs}ms run`
194
+ });
195
+ }
196
+ const finalCapture = captureScene(scene);
197
+ const logs = [...engine.log.entries()];
198
+ engine.dispose();
199
+ const ok = failures.length === 0;
200
+ return {
201
+ ok,
202
+ failures,
203
+ snapshots,
204
+ logs,
205
+ finalCapture,
206
+ describe: () => {
207
+ return [
208
+ `run ${ok ? "OK" : "FAILED"} — ${opts.durationMs}ms simulated, ${failures.length} failure(s), ${logs.length} log(s)`,
209
+ ...failures.map((f) => ` ✗ at ${f.atMs}ms${f.label ? ` [${f.label}]` : ""}: ${f.message}`),
210
+ describeCapture(finalCapture)
211
+ ].join("\n");
212
+ }
213
+ };
214
+ }
215
+ /**
216
+ * The headless play loop behind `incanto-play`: load → feed inputs by intent
217
+ * (`session.engine.input.pressAction(...)`) → `step(ms)` → `capture()` the
218
+ * state AS A SCENE FILE. What a screenshot is to humans, the capture is to
219
+ * agents: complete, structured, and in the exact format they already read.
220
+ */
221
+ async function createPlaySession(json, opts = {}) {
222
+ registerAllNodes();
223
+ for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
224
+ const engine = new Engine({
225
+ seed: opts.seed,
226
+ fixedHz: opts.fixedHz,
227
+ scheduler: () => () => {}
228
+ });
229
+ const scene = loadScene(structuredClone(json), {
230
+ resolveScene: opts.resolveScene,
231
+ stubMissingBehaviors: opts.stubMissingBehaviors,
232
+ engine
233
+ });
234
+ engine.setScene(scene);
235
+ const physics = opts.physics ?? "auto";
236
+ if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
237
+ const { enablePhysics2D } = await import("./physics-2d-KuMWPTf6.js").then((n) => n.r);
238
+ await enablePhysics2D(engine);
239
+ } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
240
+ const { enablePhysics3D } = await import("./physics-3d-Dl67vOLT.js").then((n) => n.r);
241
+ await enablePhysics3D(engine);
242
+ }
243
+ const stepMs = 1e3 / (opts.fixedHz ?? 60);
244
+ let timeMs = 0;
245
+ let logCursor = 0;
246
+ return {
247
+ engine,
248
+ scene,
249
+ get timeMs() {
250
+ return timeMs;
251
+ },
252
+ step(ms) {
253
+ const ticks = Math.max(1, Math.round(ms / stepMs));
254
+ for (let i = 0; i < ticks; i++) engine.step();
255
+ timeMs += ticks * stepMs;
256
+ },
257
+ capture() {
258
+ return scene.toJSON();
259
+ },
260
+ describe() {
261
+ return describeCapture(captureScene(scene));
262
+ },
263
+ drainLogs() {
264
+ const all = engine.log.entries();
265
+ const fresh = all.filter((e) => e.seq > logCursor);
266
+ if (all.length > 0) logCursor = all[all.length - 1].seq;
267
+ return [...fresh];
268
+ },
269
+ dispose() {
270
+ engine.dispose();
271
+ }
272
+ };
273
+ }
274
+ //#endregion
275
+ export { captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
@@ -0,0 +1,208 @@
1
+ //#region src/core/rendering-options.ts
2
+ function resolveRendering(environment, fallback, devicePixelRatio, explicit) {
3
+ const rendering = environment?.rendering ?? {};
4
+ const scenePixelRatio = rendering.pixelRatio === "device" ? devicePixelRatio : rendering.pixelRatio;
5
+ return {
6
+ antialias: explicit?.antialias ?? rendering.antialias ?? fallback.antialias,
7
+ pixelRatio: explicit?.pixelRatio ?? scenePixelRatio ?? fallback.pixelRatio
8
+ };
9
+ }
10
+ //#endregion
11
+ //#region src/core/touch.ts
12
+ /** Drags inside this fraction of the radius read as neutral. */
13
+ const DEADZONE = .08;
14
+ /**
15
+ * Pure joystick math: drag offset (px) → normalized direction, deadzoned at
16
+ * the center and clamped to unit length at the rim.
17
+ */
18
+ function joystickVector(dx, dy, radius) {
19
+ let x = dx / radius;
20
+ let y = dy / radius;
21
+ const len = Math.hypot(x, y);
22
+ if (len < DEADZONE) return {
23
+ x: 0,
24
+ y: 0
25
+ };
26
+ if (len > 1) {
27
+ x /= len;
28
+ y /= len;
29
+ }
30
+ return {
31
+ x,
32
+ y
33
+ };
34
+ }
35
+ /**
36
+ * On-screen touch controls driven by the scene's own input map: every action
37
+ * declared with `"touch": "joystick"` gets a left-side virtual stick feeding
38
+ * `setActionVector`, every `"touch": "button"` a right-side button feeding
39
+ * `pressAction`/`releaseAction`. The game never knows the difference between
40
+ * touch and keyboard — both arrive as actions.
41
+ *
42
+ * `attachTouchControls(engine, container)` shows them automatically on
43
+ * coarse-pointer devices ('auto' in createGame); `force` shows them anywhere.
44
+ * The container should be `position: relative/absolute` over the canvas.
45
+ */
46
+ function attachTouchControls(engine, container, opts = {}) {
47
+ const controls = engine.input.touchControls();
48
+ if (controls.length === 0) return null;
49
+ if (!opts.force && !isCoarsePointer()) return null;
50
+ const doc = opts.doc ?? (typeof document !== "undefined" ? document : null);
51
+ if (!doc) return null;
52
+ if (typeof container.appendChild !== "function") return null;
53
+ return new TouchControls(engine, container, controls, doc);
54
+ }
55
+ function isCoarsePointer() {
56
+ return typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches;
57
+ }
58
+ var TouchControls = class {
59
+ elements = [];
60
+ detachers = [];
61
+ constructor(engine, container, controls, doc) {
62
+ let buttonIndex = 0;
63
+ let joystickIndex = 0;
64
+ for (const control of controls) if (control.kind === "joystick") {
65
+ this.buildJoystick(engine, container, control.action, joystickIndex, doc);
66
+ joystickIndex += 1;
67
+ } else {
68
+ this.buildButton(engine, container, control.action, buttonIndex, doc);
69
+ buttonIndex += 1;
70
+ }
71
+ }
72
+ dispose() {
73
+ for (const detach of this.detachers) detach();
74
+ for (const el of this.elements) el.remove();
75
+ this.elements.length = 0;
76
+ this.detachers.length = 0;
77
+ }
78
+ buildJoystick(engine, container, action, index, doc) {
79
+ const base = doc.createElement("div");
80
+ style(base, {
81
+ position: "absolute",
82
+ left: `${24 + index * 140}px`,
83
+ bottom: "24px",
84
+ width: "120px",
85
+ height: "120px",
86
+ borderRadius: "50%",
87
+ background: "rgba(255,255,255,0.10)",
88
+ border: "2px solid rgba(255,255,255,0.25)",
89
+ touchAction: "none",
90
+ pointerEvents: "auto",
91
+ zIndex: "20"
92
+ });
93
+ const knob = doc.createElement("div");
94
+ style(knob, {
95
+ position: "absolute",
96
+ left: "50%",
97
+ top: "50%",
98
+ width: "48px",
99
+ height: "48px",
100
+ marginLeft: "-24px",
101
+ marginTop: "-24px",
102
+ borderRadius: "50%",
103
+ background: "rgba(255,255,255,0.35)",
104
+ pointerEvents: "none"
105
+ });
106
+ base.appendChild(knob);
107
+ container.appendChild(base);
108
+ this.elements.push(base);
109
+ let activePointer = null;
110
+ let cx = 0;
111
+ let cy = 0;
112
+ let radius = 60;
113
+ const apply = (clientX, clientY) => {
114
+ const v = joystickVector(clientX - cx, clientY - cy, radius);
115
+ feed(() => engine.input.setActionVector(action, v.x, v.y));
116
+ knob.style.transform = `translate(${v.x * radius * .6}px, ${v.y * radius * .6}px)`;
117
+ };
118
+ const onDown = (e) => {
119
+ activePointer = e.pointerId;
120
+ const rect = base.getBoundingClientRect();
121
+ cx = rect.left + rect.width / 2;
122
+ cy = rect.top + rect.height / 2;
123
+ radius = rect.width / 2;
124
+ base.setPointerCapture?.(e.pointerId);
125
+ apply(e.clientX, e.clientY);
126
+ };
127
+ const onMove = (e) => {
128
+ if (e.pointerId === activePointer) apply(e.clientX, e.clientY);
129
+ };
130
+ const release = (e) => {
131
+ if (e.pointerId !== activePointer) return;
132
+ activePointer = null;
133
+ knob.style.transform = "";
134
+ feed(() => engine.input.setActionVector(action, 0, 0));
135
+ };
136
+ base.addEventListener("pointerdown", onDown);
137
+ base.addEventListener("pointermove", onMove);
138
+ base.addEventListener("pointerup", release);
139
+ base.addEventListener("pointercancel", release);
140
+ this.detachers.push(() => {
141
+ base.removeEventListener("pointerdown", onDown);
142
+ base.removeEventListener("pointermove", onMove);
143
+ base.removeEventListener("pointerup", release);
144
+ base.removeEventListener("pointercancel", release);
145
+ });
146
+ }
147
+ buildButton(engine, container, action, index, doc) {
148
+ const button = doc.createElement("div");
149
+ button.textContent = action;
150
+ style(button, {
151
+ position: "absolute",
152
+ right: "24px",
153
+ bottom: `${24 + index * 80}px`,
154
+ width: "64px",
155
+ height: "64px",
156
+ borderRadius: "50%",
157
+ background: "rgba(255,255,255,0.12)",
158
+ border: "2px solid rgba(255,255,255,0.3)",
159
+ color: "rgba(255,255,255,0.85)",
160
+ font: "600 12px system-ui, sans-serif",
161
+ display: "flex",
162
+ alignItems: "center",
163
+ justifyContent: "center",
164
+ userSelect: "none",
165
+ touchAction: "none",
166
+ pointerEvents: "auto",
167
+ zIndex: "20"
168
+ });
169
+ container.appendChild(button);
170
+ this.elements.push(button);
171
+ let activePointer = null;
172
+ const onDown = (e) => {
173
+ activePointer = e.pointerId;
174
+ button.setPointerCapture?.(e.pointerId);
175
+ feed(() => engine.input.pressAction(action));
176
+ button.style.background = "rgba(255,255,255,0.3)";
177
+ };
178
+ const release = (e) => {
179
+ if (e.pointerId !== activePointer) return;
180
+ activePointer = null;
181
+ feed(() => engine.input.releaseAction(action));
182
+ button.style.background = "rgba(255,255,255,0.12)";
183
+ };
184
+ button.addEventListener("pointerdown", onDown);
185
+ button.addEventListener("pointerup", release);
186
+ button.addEventListener("pointercancel", release);
187
+ this.detachers.push(() => {
188
+ button.removeEventListener("pointerdown", onDown);
189
+ button.removeEventListener("pointerup", release);
190
+ button.removeEventListener("pointercancel", release);
191
+ });
192
+ }
193
+ };
194
+ /**
195
+ * A gesture can outlive its action by one frame (scene swap redeclares the
196
+ * input map while a finger is down; createGame rebuilds the overlay right
197
+ * after) — swallow only that race, never normal errors.
198
+ */
199
+ function feed(fn) {
200
+ try {
201
+ fn();
202
+ } catch {}
203
+ }
204
+ function style(el, rules) {
205
+ for (const [key, value] of Object.entries(rules)) el.style[key] = value;
206
+ }
207
+ //#endregion
208
+ export { resolveRendering as i, attachTouchControls as n, joystickVector as r, TouchControls as t };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ //#region src/vite/index.d.ts
2
+ interface HotUpdateContext {
3
+ file: string;
4
+ read(): Promise<string>;
5
+ server: {
6
+ ws: {
7
+ send(payload: unknown): void;
8
+ };
9
+ config?: {
10
+ logger?: {
11
+ error(msg: string): void;
12
+ };
13
+ };
14
+ };
15
+ modules?: unknown[];
16
+ }
17
+ interface IncantoScenesOptions {
18
+ /** Hard-fail on unregistered behaviors too (default: structure-only). */
19
+ strictBehaviors?: boolean;
20
+ }
21
+ declare function incantoScenes(opts?: IncantoScenesOptions): {
22
+ name: string;
23
+ handleHotUpdate(ctx: HotUpdateContext): Promise<undefined | never[]>;
24
+ };
25
+ //#endregion
26
+ export { IncantoScenesOptions, incantoScenes };