incanto 0.30.0 → 0.32.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.
- package/bin/incanto-play.mjs +7 -2
- package/dist/2d.d.ts +12 -5
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +67 -6
- package/dist/3d.js +4 -4
- package/dist/{audit-C6rMyict.js → audit-C4kmDK0o.js} +16 -0
- package/dist/{behavior-BAc0erXF.d.ts → behavior-D_jMpFh8.d.ts} +117 -55
- package/dist/{create-game-5z_QVtLx.js → create-game-bKHgHcsZ.js} +19 -10
- package/dist/{create-game-DuBTv2zI.js → create-game-gXI7PYl0.js} +61 -46
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +7 -2
- package/dist/{duplicate-BgtFrFo4.js → duplicate-KRPtUtzl.js} +1 -1
- package/dist/{editor-switch-B0wB_DSr.d.ts → editor-switch-BJb-CWfA.d.ts} +12 -1
- package/dist/{register-DJ0SByQg.js → environment-presets--DigHNg4.js} +42 -11
- package/dist/{gameplay-Cfr6aFZ1.js → gameplay-BftxM_It.js} +2 -2
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.js +7 -7
- package/dist/{loader-B242FF6N.d.ts → loader-BYBrqTxP.d.ts} +1 -1
- package/dist/{loader-BZqOKfI2.js → loader-BlaRQGaA.js} +482 -6
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-BwD974Ss.d.ts → pathfinding-BwhqPD3i.d.ts} +1 -1
- package/dist/{physics-2d-3kOQCtgd.js → physics-2d-D9wquBvK.js} +4 -3
- package/dist/{physics-3d-CeRH-Ff_.js → physics-3d-CnPygVGo.js} +5 -4
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-MelqEdza.js → register-CUY284Is.js} +3 -3
- package/dist/{register-BTg0EM7s.js → register-Dzkd6-os.js} +56 -380
- package/dist/{register-DWcWq4QG.js → register-tkR_8tWg.js} +4 -4
- package/dist/{registry-BVJ2HbCn.js → registry-CJdGpT2V.js} +6 -2
- package/dist/test-WwRIlXsK.js +642 -0
- package/dist/test.d.ts +55 -8
- package/dist/test.js +3 -364
- package/dist/{touch-031PxtCR.js → touch-BoNg_MnF.js} +2 -1
- package/dist/vite.js +1 -1
- package/editor/assets/{agent8-CAp0i5qn.js → agent8-C5k1nTCH.js} +1 -1
- package/editor/assets/{debug-BoEYfbqK.js → debug-BT_0mjk-.js} +3 -2
- package/editor/assets/{index-BO6WU8by.js → index-gfyrByWw.js} +90 -90
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/README.md +14 -2
- package/skills/incanto-building-2d-games.md +16 -0
- package/skills/incanto-building-3d-games.md +16 -0
- package/skills/incanto-verifying-your-game.md +118 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
2
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
3
|
-
import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-
|
|
3
|
+
import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-CJdGpT2V.js";
|
|
4
4
|
//#region src/core/signal.ts
|
|
5
5
|
var Signal = class {
|
|
6
6
|
connections = [];
|
|
@@ -118,6 +118,402 @@ function createBehavior(name, props) {
|
|
|
118
118
|
return behavior;
|
|
119
119
|
}
|
|
120
120
|
//#endregion
|
|
121
|
+
//#region src/core/input.ts
|
|
122
|
+
/** Keys typed into editable elements belong to the page UI, not the game. */
|
|
123
|
+
function isEditableTarget(target) {
|
|
124
|
+
const el = target;
|
|
125
|
+
return !!el && (el.isContentEditable === true || el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
|
|
129
|
+
* via `handleKey`, or wire a browser with `attachKeyboard(window)`).
|
|
130
|
+
*
|
|
131
|
+
* Convention: vector2 follows the 2D y-down space (up = -y). `justPressed` /
|
|
132
|
+
* `justReleased` are one-frame edges, settled by the Engine each tick.
|
|
133
|
+
*/
|
|
134
|
+
var InputMap = class {
|
|
135
|
+
actions = /* @__PURE__ */ new Map();
|
|
136
|
+
down = /* @__PURE__ */ new Set();
|
|
137
|
+
pressedEdge = /* @__PURE__ */ new Set();
|
|
138
|
+
releasedEdge = /* @__PURE__ */ new Set();
|
|
139
|
+
detach = null;
|
|
140
|
+
/** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
|
|
141
|
+
declare(decls) {
|
|
142
|
+
for (const [name, action] of parseInputDecls(decls)) this.actions.set(name, action);
|
|
143
|
+
}
|
|
144
|
+
/** Feed a key state change (code = KeyboardEvent.code, e.g. 'Space', 'KeyW'). */
|
|
145
|
+
handleKey(code, isDown) {
|
|
146
|
+
if (isDown) {
|
|
147
|
+
if (!this.down.has(code)) {
|
|
148
|
+
this.down.add(code);
|
|
149
|
+
this.pressedEdge.add(code);
|
|
150
|
+
}
|
|
151
|
+
} else if (this.down.has(code)) {
|
|
152
|
+
this.down.delete(code);
|
|
153
|
+
this.releasedEdge.add(code);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
injectedDown = /* @__PURE__ */ new Set();
|
|
157
|
+
injectedPressed = /* @__PURE__ */ new Set();
|
|
158
|
+
injectedReleased = /* @__PURE__ */ new Set();
|
|
159
|
+
injectedVectors = /* @__PURE__ */ new Map();
|
|
160
|
+
/**
|
|
161
|
+
* Press a button ACTION directly — no key codes involved. This is how
|
|
162
|
+
* scripted gameplay tests and touch buttons drive the game by intent
|
|
163
|
+
* (`press('jump')`) instead of reverse-engineering keybinds.
|
|
164
|
+
*/
|
|
165
|
+
pressAction(action) {
|
|
166
|
+
this.button(action);
|
|
167
|
+
if (!this.injectedDown.has(action)) {
|
|
168
|
+
this.injectedDown.add(action);
|
|
169
|
+
this.injectedPressed.add(action);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Release an injected button action (yields one justReleased frame). */
|
|
173
|
+
releaseAction(action) {
|
|
174
|
+
this.button(action);
|
|
175
|
+
if (this.injectedDown.delete(action)) this.injectedReleased.add(action);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Feed an analog direction for a vector2 ACTION — virtual joysticks and
|
|
179
|
+
* scripted runs. Persists until replaced; (0, 0) clears the injection.
|
|
180
|
+
* Combines with key state in getVector (clamped to unit length).
|
|
181
|
+
*/
|
|
182
|
+
setActionVector(action, x, y) {
|
|
183
|
+
if (this.get(action).type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use pressAction/releaseAction, not setActionVector.`);
|
|
184
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new IncantoError("BAD_FORMAT", `setActionVector('${action}') needs finite numbers, got (${x}, ${y}).`);
|
|
185
|
+
if (x === 0 && y === 0) this.injectedVectors.delete(action);
|
|
186
|
+
else this.injectedVectors.set(action, {
|
|
187
|
+
x,
|
|
188
|
+
y
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Actions the scene wants on-screen touch controls for, in declaration
|
|
193
|
+
* order (`"touch": "joystick"` on vector2, `"touch": "button"` on buttons).
|
|
194
|
+
* The touch overlay (core/touch.ts) renders these.
|
|
195
|
+
*/
|
|
196
|
+
touchControls() {
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const [name, def] of this.actions) if (def.touch) out.push({
|
|
199
|
+
action: name,
|
|
200
|
+
kind: def.touch
|
|
201
|
+
});
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
/** Drop all injected action state (held buttons, vectors, pending edges). */
|
|
205
|
+
clearInjected() {
|
|
206
|
+
this.injectedDown.clear();
|
|
207
|
+
this.injectedPressed.clear();
|
|
208
|
+
this.injectedReleased.clear();
|
|
209
|
+
this.injectedVectors.clear();
|
|
210
|
+
}
|
|
211
|
+
dx = 0;
|
|
212
|
+
dy = 0;
|
|
213
|
+
wheel = 0;
|
|
214
|
+
/** Mouse buttons feed the same code space as keys: Mouse0/Mouse1/Mouse2. */
|
|
215
|
+
handleMouseButton(button, isDown) {
|
|
216
|
+
this.handleKey(`Mouse${button}`, isDown);
|
|
217
|
+
}
|
|
218
|
+
/** Accumulate look deltas (movementX/Y under pointer lock, else move deltas). */
|
|
219
|
+
handlePointerMove(dx, dy) {
|
|
220
|
+
this.dx += dx;
|
|
221
|
+
this.dy += dy;
|
|
222
|
+
}
|
|
223
|
+
handleWheel(deltaY) {
|
|
224
|
+
this.wheel += deltaY;
|
|
225
|
+
}
|
|
226
|
+
/** Drain the accumulated pointer delta (read once per frame). */
|
|
227
|
+
pointerDelta() {
|
|
228
|
+
const d = {
|
|
229
|
+
x: this.dx,
|
|
230
|
+
y: this.dy
|
|
231
|
+
};
|
|
232
|
+
this.dx = 0;
|
|
233
|
+
this.dy = 0;
|
|
234
|
+
return d;
|
|
235
|
+
}
|
|
236
|
+
/** Drain the accumulated wheel delta. */
|
|
237
|
+
wheelDelta() {
|
|
238
|
+
const w = this.wheel;
|
|
239
|
+
this.wheel = 0;
|
|
240
|
+
return w;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Wire browser pointer events on a canvas: buttons → Mouse0/1/2 codes,
|
|
244
|
+
* movement → pointerDelta (movementX/Y so pointer lock just works),
|
|
245
|
+
* wheel → wheelDelta. `lockOnClick` requests pointer lock on mousedown
|
|
246
|
+
* (the FPS pattern).
|
|
247
|
+
*/
|
|
248
|
+
padAxesState = {
|
|
249
|
+
lx: 0,
|
|
250
|
+
ly: 0,
|
|
251
|
+
rx: 0,
|
|
252
|
+
ry: 0
|
|
253
|
+
};
|
|
254
|
+
padButtonsDown = 0;
|
|
255
|
+
/**
|
|
256
|
+
* Feed one polled gamepad snapshot (standard mapping). Buttons become
|
|
257
|
+
* codes `Pad0`..`Pad16` in the same space as keys — declare them in input
|
|
258
|
+
* actions like any key ("jump": ["Space", "Pad0"]). Axes land in
|
|
259
|
+
* `padAxes()` with a deadzone. Pass null when no pad is connected.
|
|
260
|
+
*/
|
|
261
|
+
pollGamepad(pad) {
|
|
262
|
+
const dead = .15;
|
|
263
|
+
const shape = (v) => Math.abs(v) < dead ? 0 : v;
|
|
264
|
+
if (!pad) {
|
|
265
|
+
for (let i = 0; i < 17; i++) if (this.padButtonsDown & 1 << i) this.handleKey(`Pad${i}`, false);
|
|
266
|
+
this.padButtonsDown = 0;
|
|
267
|
+
this.padAxesState.lx = 0;
|
|
268
|
+
this.padAxesState.ly = 0;
|
|
269
|
+
this.padAxesState.rx = 0;
|
|
270
|
+
this.padAxesState.ry = 0;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
for (let i = 0; i < Math.min(17, pad.buttons.length); i++) {
|
|
274
|
+
const down = pad.buttons[i]?.pressed === true;
|
|
275
|
+
if (down !== ((this.padButtonsDown & 1 << i) !== 0)) {
|
|
276
|
+
this.handleKey(`Pad${i}`, down);
|
|
277
|
+
this.padButtonsDown = down ? this.padButtonsDown | 1 << i : this.padButtonsDown & ~(1 << i);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
this.padAxesState.lx = shape(pad.axes[0] ?? 0);
|
|
281
|
+
this.padAxesState.ly = shape(pad.axes[1] ?? 0);
|
|
282
|
+
this.padAxesState.rx = shape(pad.axes[2] ?? 0);
|
|
283
|
+
this.padAxesState.ry = shape(pad.axes[3] ?? 0);
|
|
284
|
+
}
|
|
285
|
+
/** Deadzoned analog sticks: 0 = left {x,y}, 1 = right. */
|
|
286
|
+
padAxes(stick = 0) {
|
|
287
|
+
return stick === 0 ? {
|
|
288
|
+
x: this.padAxesState.lx,
|
|
289
|
+
y: this.padAxesState.ly
|
|
290
|
+
} : {
|
|
291
|
+
x: this.padAxesState.rx,
|
|
292
|
+
y: this.padAxesState.ry
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Poll the browser Gamepad API every frame (the first connected pad).
|
|
297
|
+
* Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
|
|
298
|
+
*/
|
|
299
|
+
attachGamepad(engine) {
|
|
300
|
+
if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") return () => {};
|
|
301
|
+
const disconnect = engine.updated.connect(() => {
|
|
302
|
+
const pads = navigator.getGamepads();
|
|
303
|
+
let pad = null;
|
|
304
|
+
for (const p of pads) if (p?.connected) {
|
|
305
|
+
pad = p;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
this.pollGamepad(pad);
|
|
309
|
+
});
|
|
310
|
+
const prev = this.detach;
|
|
311
|
+
this.detach = () => {
|
|
312
|
+
prev?.();
|
|
313
|
+
disconnect();
|
|
314
|
+
};
|
|
315
|
+
return disconnect;
|
|
316
|
+
}
|
|
317
|
+
attachPointer(target, opts) {
|
|
318
|
+
let buttonsHeld = 0;
|
|
319
|
+
const onDown = (e) => {
|
|
320
|
+
buttonsHeld++;
|
|
321
|
+
this.handleMouseButton(e.button, true);
|
|
322
|
+
if (opts?.lockOnClick && document.pointerLockElement !== target) target.requestPointerLock?.();
|
|
323
|
+
};
|
|
324
|
+
const onUp = (e) => {
|
|
325
|
+
buttonsHeld = Math.max(0, buttonsHeld - 1);
|
|
326
|
+
this.handleMouseButton(e.button, false);
|
|
327
|
+
};
|
|
328
|
+
const onMove = (e) => {
|
|
329
|
+
if (document.pointerLockElement === target || buttonsHeld > 0) this.handlePointerMove(e.movementX, e.movementY);
|
|
330
|
+
};
|
|
331
|
+
const onWheel = (e) => {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
this.handleWheel(e.deltaY);
|
|
334
|
+
};
|
|
335
|
+
const onContext = (e) => e.preventDefault();
|
|
336
|
+
target.addEventListener("mousedown", onDown);
|
|
337
|
+
window.addEventListener("mouseup", onUp);
|
|
338
|
+
window.addEventListener("mousemove", onMove);
|
|
339
|
+
target.addEventListener("wheel", onWheel, { passive: false });
|
|
340
|
+
target.addEventListener("contextmenu", onContext);
|
|
341
|
+
const detach = () => {
|
|
342
|
+
target.removeEventListener("mousedown", onDown);
|
|
343
|
+
window.removeEventListener("mouseup", onUp);
|
|
344
|
+
window.removeEventListener("mousemove", onMove);
|
|
345
|
+
target.removeEventListener("wheel", onWheel);
|
|
346
|
+
target.removeEventListener("contextmenu", onContext);
|
|
347
|
+
};
|
|
348
|
+
const prev = this.detach;
|
|
349
|
+
this.detach = () => {
|
|
350
|
+
prev?.();
|
|
351
|
+
detach();
|
|
352
|
+
};
|
|
353
|
+
return detach;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Wire browser keyboard events. Returns (and chains into dispose()) a detach
|
|
357
|
+
* function.
|
|
358
|
+
*
|
|
359
|
+
* By default the browser default is prevented for keys BOUND to a declared
|
|
360
|
+
* action — an embedded game must not scroll its host page on Space/arrows.
|
|
361
|
+
* Keys typed into editable elements (inputs, textareas, contenteditable)
|
|
362
|
+
* are ignored entirely so DOM UI overlays keep working.
|
|
363
|
+
*/
|
|
364
|
+
attachKeyboard(target, opts) {
|
|
365
|
+
const mode = opts?.preventDefault ?? "bound";
|
|
366
|
+
const onDown = (e) => {
|
|
367
|
+
if (isEditableTarget(e.target)) return;
|
|
368
|
+
if ((e.ctrlKey || e.metaKey) && !e.code.startsWith("Control") && !e.code.startsWith("Meta")) return;
|
|
369
|
+
if (mode === "bound" && this.codeIsBound(e.code)) e.preventDefault?.();
|
|
370
|
+
if (!e.repeat) this.handleKey(e.code, true);
|
|
371
|
+
};
|
|
372
|
+
const onUp = (e) => {
|
|
373
|
+
if (isEditableTarget(e.target)) return;
|
|
374
|
+
this.handleKey(e.code, false);
|
|
375
|
+
};
|
|
376
|
+
target.addEventListener("keydown", onDown);
|
|
377
|
+
target.addEventListener("keyup", onUp);
|
|
378
|
+
const detach = () => {
|
|
379
|
+
target.removeEventListener("keydown", onDown);
|
|
380
|
+
target.removeEventListener("keyup", onUp);
|
|
381
|
+
};
|
|
382
|
+
const prev = this.detach;
|
|
383
|
+
this.detach = () => {
|
|
384
|
+
prev?.();
|
|
385
|
+
detach();
|
|
386
|
+
};
|
|
387
|
+
return detach;
|
|
388
|
+
}
|
|
389
|
+
/** Whether any declared action binds this key code. */
|
|
390
|
+
codeIsBound(code) {
|
|
391
|
+
for (const action of this.actions.values()) if (action.type === "button") {
|
|
392
|
+
if (action.keys.includes(code)) return true;
|
|
393
|
+
} else {
|
|
394
|
+
const k = action.keys;
|
|
395
|
+
if (k.up.includes(code) || k.down.includes(code) || k.left.includes(code) || k.right.includes(code)) return true;
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
dispose() {
|
|
400
|
+
this.detach?.();
|
|
401
|
+
this.detach = null;
|
|
402
|
+
}
|
|
403
|
+
isPressed(action) {
|
|
404
|
+
return this.button(action).keys.some((k) => this.down.has(k)) || this.injectedDown.has(action);
|
|
405
|
+
}
|
|
406
|
+
justPressed(action) {
|
|
407
|
+
return this.button(action).keys.some((k) => this.pressedEdge.has(k)) || this.injectedPressed.has(action);
|
|
408
|
+
}
|
|
409
|
+
justReleased(action) {
|
|
410
|
+
return this.button(action).keys.some((k) => this.releasedEdge.has(k)) || this.injectedReleased.has(action);
|
|
411
|
+
}
|
|
412
|
+
/** Normalized direction for a vector2 action (y-down: up = -y). */
|
|
413
|
+
getVector(action) {
|
|
414
|
+
const def = this.get(action);
|
|
415
|
+
if (def.type !== "vector2") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a button — use isPressed/justPressed, not getVector.`);
|
|
416
|
+
const active = (codes) => codes.some((k) => this.down.has(k)) ? 1 : 0;
|
|
417
|
+
let x = active(def.keys.right) - active(def.keys.left);
|
|
418
|
+
let y = active(def.keys.down) - active(def.keys.up);
|
|
419
|
+
const injected = this.injectedVectors.get(action);
|
|
420
|
+
if (injected) {
|
|
421
|
+
x += injected.x;
|
|
422
|
+
y += injected.y;
|
|
423
|
+
}
|
|
424
|
+
const len = Math.hypot(x, y);
|
|
425
|
+
if (len > 1) {
|
|
426
|
+
x /= len;
|
|
427
|
+
y /= len;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
x,
|
|
431
|
+
y
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/** Consume one-frame edges. The Engine calls this at the end of every tick. */
|
|
435
|
+
endFrame() {
|
|
436
|
+
this.pressedEdge.clear();
|
|
437
|
+
this.releasedEdge.clear();
|
|
438
|
+
this.injectedPressed.clear();
|
|
439
|
+
this.injectedReleased.clear();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Drop all action declarations and injected action state (physical key
|
|
443
|
+
* state is kept). The Engine calls this on setScene so keybinds never
|
|
444
|
+
* bleed between scenes.
|
|
445
|
+
*/
|
|
446
|
+
clear() {
|
|
447
|
+
this.actions.clear();
|
|
448
|
+
this.pressedEdge.clear();
|
|
449
|
+
this.releasedEdge.clear();
|
|
450
|
+
this.clearInjected();
|
|
451
|
+
}
|
|
452
|
+
get(action) {
|
|
453
|
+
const def = this.actions.get(action);
|
|
454
|
+
if (!def) throw new IncantoError("BAD_FORMAT", `Unknown input action '${action}'. Declared actions: [${[...this.actions.keys()].join(", ")}].`);
|
|
455
|
+
return def;
|
|
456
|
+
}
|
|
457
|
+
button(action) {
|
|
458
|
+
const def = this.get(action);
|
|
459
|
+
if (def.type !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${action}' is a vector2 — use getVector, not button queries.`);
|
|
460
|
+
return def;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* Parse and HARD-VALIDATE a scene's `input{}` block.
|
|
465
|
+
*
|
|
466
|
+
* Standalone because validating it used to require an Engine: the only caller
|
|
467
|
+
* was `setScene`, so `incanto-check` — the gate an agent runs after every edit —
|
|
468
|
+
* never looked at `input` at all. A typo there ("keys": "KeyW" instead of
|
|
469
|
+
* ["KeyW"]) passed the check and threw in the browser at boot.
|
|
470
|
+
*/
|
|
471
|
+
function parseInputDecls(decls) {
|
|
472
|
+
const out = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const [name, raw] of Object.entries(decls)) {
|
|
474
|
+
const decl = raw;
|
|
475
|
+
if (decl.type === "button") {
|
|
476
|
+
const keys = decl.keys;
|
|
477
|
+
if (!Array.isArray(keys) || keys.some((k) => typeof k !== "string")) throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "keys" must be an array of KeyboardEvent.code strings.`);
|
|
478
|
+
if (decl.touch !== void 0 && decl.touch !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "touch" must be 'button', got ${JSON.stringify(decl.touch)}.`);
|
|
479
|
+
out.set(name, {
|
|
480
|
+
type: "button",
|
|
481
|
+
keys: [...keys],
|
|
482
|
+
...decl.touch === "button" ? { touch: "button" } : {}
|
|
483
|
+
});
|
|
484
|
+
} else if (decl.type === "vector2") {
|
|
485
|
+
const dirs = decl.keys;
|
|
486
|
+
for (const dir of [
|
|
487
|
+
"up",
|
|
488
|
+
"down",
|
|
489
|
+
"left",
|
|
490
|
+
"right"
|
|
491
|
+
]) if (!Array.isArray(dirs?.[dir])) throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "keys" needs arrays for up/down/left/right.`);
|
|
492
|
+
if (decl.touch !== void 0 && decl.touch !== "joystick") throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "touch" must be 'joystick', got ${JSON.stringify(decl.touch)}.`);
|
|
493
|
+
const d = dirs;
|
|
494
|
+
out.set(name, {
|
|
495
|
+
type: "vector2",
|
|
496
|
+
keys: {
|
|
497
|
+
up: [...d.up],
|
|
498
|
+
down: [...d.down],
|
|
499
|
+
left: [...d.left],
|
|
500
|
+
right: [...d.right]
|
|
501
|
+
},
|
|
502
|
+
...decl.touch === "joystick" ? { touch: "joystick" } : {}
|
|
503
|
+
});
|
|
504
|
+
} else throw new IncantoError("BAD_FORMAT", `Input action '${name}': "type" must be 'button' or 'vector2', got ${JSON.stringify(decl.type)}.`);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/core/diagnostics.ts
|
|
510
|
+
function diagnose(engine, level, ...parts) {
|
|
511
|
+
engine?.log[level](...parts);
|
|
512
|
+
if (level === "error") console.error(...parts);
|
|
513
|
+
else if (level === "warn") console.warn(...parts);
|
|
514
|
+
else console.info(...parts);
|
|
515
|
+
}
|
|
516
|
+
//#endregion
|
|
121
517
|
//#region src/core/node-path.ts
|
|
122
518
|
function parseNodePath(path) {
|
|
123
519
|
if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
|
|
@@ -432,18 +828,86 @@ var Node = class {
|
|
|
432
828
|
}
|
|
433
829
|
/** @internal */
|
|
434
830
|
_propagateUpdate(dt) {
|
|
435
|
-
this.
|
|
436
|
-
|
|
831
|
+
if (!this.errored) try {
|
|
832
|
+
this.update(dt);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
this._quarantine("update", error, "node");
|
|
835
|
+
}
|
|
836
|
+
if (this.behavior && !this.behaviorErrored) try {
|
|
837
|
+
this.behavior.update?.(dt);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
this._quarantine("update", error, "behavior");
|
|
840
|
+
}
|
|
437
841
|
const kids = this._children;
|
|
438
842
|
for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateUpdate(dt);
|
|
439
843
|
}
|
|
440
844
|
/** @internal */
|
|
441
845
|
_propagateFixedUpdate(dt) {
|
|
442
|
-
this.
|
|
443
|
-
|
|
846
|
+
if (!this.errored) try {
|
|
847
|
+
this.fixedUpdate(dt);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
this._quarantine("fixedUpdate", error, "node");
|
|
850
|
+
}
|
|
851
|
+
if (this.behavior && !this.behaviorErrored) try {
|
|
852
|
+
this.behavior.fixedUpdate?.(dt);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this._quarantine("fixedUpdate", error, "behavior");
|
|
855
|
+
}
|
|
444
856
|
const kids = this._children;
|
|
445
857
|
for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateFixedUpdate(dt);
|
|
446
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Report an engine-level problem about THIS node.
|
|
861
|
+
*
|
|
862
|
+
* Goes to `engine.log` (what the debug overlay, `runScript()` and any agent
|
|
863
|
+
* tool read) as well as the console. A `console.warn` alone is invisible to
|
|
864
|
+
* every channel except a human with devtools open.
|
|
865
|
+
*/
|
|
866
|
+
diagnose(level, ...parts) {
|
|
867
|
+
diagnose(this._tree?.engine ?? null, level, ...parts);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* This node threw and is being skipped. Its CHILDREN still run.
|
|
871
|
+
*
|
|
872
|
+
* The alternative is what used to happen: the exception escaped into the rAF
|
|
873
|
+
* callback, the loop was never rescheduled, and the game froze — permanently,
|
|
874
|
+
* silently, with `stats().running` still reporting true. In a vibe-coded
|
|
875
|
+
* project the least-tested code in the repo is a behavior's `update`, so this
|
|
876
|
+
* is not an edge case; it is Tuesday.
|
|
877
|
+
*
|
|
878
|
+
* Set it back to false to try the node again (the debug overlay's "resume").
|
|
879
|
+
*/
|
|
880
|
+
errored = false;
|
|
881
|
+
/**
|
|
882
|
+
* This node's SCRIPT threw and is being skipped — the node itself still runs.
|
|
883
|
+
*
|
|
884
|
+
* A behavior is your code; the node is the engine's. A typo in the first must
|
|
885
|
+
* not switch off the second, or breaking a sword script stops the character
|
|
886
|
+
* from walking.
|
|
887
|
+
*/
|
|
888
|
+
behaviorErrored = false;
|
|
889
|
+
/**
|
|
890
|
+
* Report ONCE and stop running this node's own update.
|
|
891
|
+
*
|
|
892
|
+
* Once, because a behavior that throws throws every frame: sixty identical
|
|
893
|
+
* stack traces a second buries the one line that matters and costs more than
|
|
894
|
+
* the game did.
|
|
895
|
+
*/
|
|
896
|
+
_quarantine(phase, error, what) {
|
|
897
|
+
const where = `${this.getPath()} (${this.constructor.typeName ?? "Node"})`;
|
|
898
|
+
let message;
|
|
899
|
+
if (what === "behavior") {
|
|
900
|
+
this.behaviorErrored = true;
|
|
901
|
+
message = `[incanto] behavior '${this.behavior?.constructor.name ?? "?"}' on ${where} threw in ${phase} — THE SCRIPT is now skipped; the node itself and the rest of the scene keep running.`;
|
|
902
|
+
} else {
|
|
903
|
+
this.errored = true;
|
|
904
|
+
message = `[incanto] ${where} threw in ${phase} — this node is now SKIPPED. Its children and the rest of the scene keep running.`;
|
|
905
|
+
}
|
|
906
|
+
const engine = this._tree?.engine;
|
|
907
|
+
engine?.log.error(message, error);
|
|
908
|
+
engine?._nodeErrored(this);
|
|
909
|
+
console.error(message, error);
|
|
910
|
+
}
|
|
447
911
|
onEnterTree() {}
|
|
448
912
|
onReady() {}
|
|
449
913
|
onExitTree() {}
|
|
@@ -841,6 +1305,18 @@ function validateHeader(json) {
|
|
|
841
1305
|
if (json.dimension !== void 0 && json.dimension !== "2d" && json.dimension !== "3d") throw new IncantoError("BAD_FORMAT", `Scene "dimension" must be "2d" or "3d", got ${JSON.stringify(json.dimension)}.`);
|
|
842
1306
|
if (typeof json.root !== "object" || json.root === null) throw new IncantoError("BAD_FORMAT", "Scene \"root\" must be a node object.");
|
|
843
1307
|
resolveViewport(json.viewport);
|
|
1308
|
+
if (json.input !== void 0) {
|
|
1309
|
+
if (typeof json.input !== "object" || json.input === null || Array.isArray(json.input)) throw new IncantoError("BAD_FORMAT", "Scene \"input\" must be an object of action declarations.");
|
|
1310
|
+
parseInputDecls(json.input);
|
|
1311
|
+
}
|
|
1312
|
+
if (json.assets !== void 0) {
|
|
1313
|
+
if (typeof json.assets !== "object" || json.assets === null || Array.isArray(json.assets)) throw new IncantoError("BAD_FORMAT", "Scene \"assets\" must be an object of asset declarations.");
|
|
1314
|
+
for (const [key, decl] of Object.entries(json.assets)) {
|
|
1315
|
+
if (typeof decl !== "object" || decl === null || Array.isArray(decl)) throw new IncantoError("BAD_FORMAT", `Asset '${key}' must be an object, got ${JSON.stringify(decl)}.`);
|
|
1316
|
+
const url = decl.url;
|
|
1317
|
+
if (typeof url !== "string" || url === "") throw new IncantoError("BAD_FORMAT", `Asset '${key}' needs a non-empty "url" string.`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
844
1320
|
}
|
|
845
1321
|
/**
|
|
846
1322
|
* Re-raise a node-build error with the node's absolute scene path attached —
|
|
@@ -978,4 +1454,4 @@ function validateUniqueUids(root) {
|
|
|
978
1454
|
walk(root);
|
|
979
1455
|
}
|
|
980
1456
|
//#endregion
|
|
981
|
-
export {
|
|
1457
|
+
export { clearBehaviors as _, SCENE_FORMAT as a, registeredBehaviors as b, SceneTree as c, resolveConstants as d, Node as f, Behavior as g, InputMap as h, serializeNode as i, CONST_REF_KEY as l, diagnose as m, loadScene as n, computeViewport as o, parseNodePath as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, getBehavior as v, Signal as x, registerBehavior as y };
|
package/dist/net.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { X as Node, l as PropSchema, nt as Signal, v as Engine } from "./behavior-D_jMpFh8.js";
|
|
1
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
|
|
2
|
-
import { $ as Node, l as PropSchema, tt as Signal, v as Engine } from "./behavior-BAc0erXF.js";
|
|
3
3
|
|
|
4
4
|
//#region src/net/types.d.ts
|
|
5
5
|
type Unsubscribe = () => void;
|
package/dist/net.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { n as loadScene } from "./loader-
|
|
2
|
-
import { u as Engine } from "./register-
|
|
1
|
+
import { n as loadScene } from "./loader-BlaRQGaA.js";
|
|
2
|
+
import { u as Engine } from "./register-Dzkd6-os.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
5
5
|
import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
|
|
6
|
-
import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-
|
|
6
|
+
import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-CUY284Is.js";
|
|
7
7
|
//#region src/net/loopback.ts
|
|
8
8
|
/**
|
|
9
9
|
* In-memory multiplayer hub implementing the SAME kernel contract as
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { m as diagnose } from "./loader-BlaRQGaA.js";
|
|
2
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
-
import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-
|
|
4
|
+
import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-tkR_8tWg.js";
|
|
4
5
|
import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
|
|
5
6
|
//#region src/2d/physics/physics-2d.ts
|
|
6
7
|
var physics_2d_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -113,7 +114,7 @@ var Physics2D = class {
|
|
|
113
114
|
a.emit(sig, b);
|
|
114
115
|
b.emit(sig, a);
|
|
115
116
|
} catch (error) {
|
|
116
|
-
|
|
117
|
+
diagnose(this.engine, "error", `incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
|
|
117
118
|
}
|
|
118
119
|
});
|
|
119
120
|
}
|
|
@@ -346,7 +347,7 @@ var Physics2D = class {
|
|
|
346
347
|
}
|
|
347
348
|
if (!this.warnedNoCollider.has(node)) {
|
|
348
349
|
this.warnedNoCollider.add(node);
|
|
349
|
-
|
|
350
|
+
diagnose(this.engine, "warn", `[incanto] '${node.name}' has no collider — physics skips it.`);
|
|
350
351
|
}
|
|
351
352
|
continue;
|
|
352
353
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { m as diagnose } from "./loader-BlaRQGaA.js";
|
|
2
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
-
import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-
|
|
4
|
+
import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-BftxM_It.js";
|
|
4
5
|
import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
|
|
5
|
-
import {
|
|
6
|
+
import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets--DigHNg4.js";
|
|
6
7
|
import { Euler, Matrix4, Quaternion, Vector3 } from "three";
|
|
7
8
|
//#region src/3d/physics/collider-lines.ts
|
|
8
9
|
/**
|
|
@@ -336,7 +337,7 @@ var Physics3D = class {
|
|
|
336
337
|
a.emit(sig, b);
|
|
337
338
|
b.emit(sig, a);
|
|
338
339
|
} catch (error) {
|
|
339
|
-
|
|
340
|
+
diagnose(this.engine, "error", `incanto: error in a '${sig}' handler (${a.name} ↔ ${b.name}):`, error);
|
|
340
341
|
}
|
|
341
342
|
});
|
|
342
343
|
}
|
|
@@ -519,7 +520,7 @@ var Physics3D = class {
|
|
|
519
520
|
}
|
|
520
521
|
if (!this.warnedNoCollider.has(node)) {
|
|
521
522
|
this.warnedNoCollider.add(node);
|
|
522
|
-
|
|
523
|
+
diagnose(this.engine, "warn", `[incanto] '${node.name}' has no collider — physics skips it.`);
|
|
523
524
|
}
|
|
524
525
|
continue;
|
|
525
526
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-D_jMpFh8.js";
|
|
1
2
|
import { c as JsonValue } from "./schema-CcoWb32N.js";
|
|
2
|
-
import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
|
|
3
3
|
import { CSSProperties, ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react/index.d.ts
|
package/dist/react.js
CHANGED
|
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
|
|
|
156
156
|
pointer: latest.pointer,
|
|
157
157
|
...keyboard !== void 0 ? { keyboard } : {}
|
|
158
158
|
};
|
|
159
|
-
const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-
|
|
159
|
+
const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-gXI7PYl0.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-bKHgHcsZ.js").then((n) => n.n)).createGame2D(o)))(opts);
|
|
160
160
|
if (disposed) {
|
|
161
161
|
next.dispose();
|
|
162
162
|
return;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { f as Node, t as buildNodeJson,
|
|
2
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { f as Node, t as buildNodeJson, x as Signal } from "./loader-BlaRQGaA.js";
|
|
2
|
+
import { t as registerCoreNodes } from "./register-Dzkd6-os.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
5
|
-
import { l as registerNode } from "./registry-
|
|
5
|
+
import { l as registerNode } from "./registry-CJdGpT2V.js";
|
|
6
6
|
//#region src/net/network-manager.ts
|
|
7
7
|
const managers = /* @__PURE__ */ new WeakMap();
|
|
8
8
|
/**
|