incanto 0.65.0 → 0.67.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/_behaviors-loader.mjs +87 -0
- package/bin/incanto-editor.mjs +39 -2
- package/bin/incanto-feel.mjs +4 -1
- package/bin/incanto-frame.mjs +52 -3
- package/bin/incanto-logs.mjs +33 -3
- package/bin/incanto-multiplay.mjs +50 -1
- package/bin/incanto-play.mjs +3 -1
- package/bin/incanto-playtest.mjs +3 -25
- package/bin/incanto-verify.mjs +15 -2
- package/dist/2d.d.ts +76 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +213 -14
- package/dist/3d.js +6 -6
- package/dist/{audio-player-BrEHwbK1.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
- package/dist/{behavior-CmMuQ6CL.d.ts → behavior-ZnfzKDo3.d.ts} +58 -1
- package/dist/{create-game-DIyh5Zy9.js → create-game-DMetd3vR.js} +14 -8
- package/dist/{create-game-W2xzTJhs.js → create-game-FAXdHta3.js} +138 -11
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +3 -2
- package/dist/{duplicate-CqSAtdrh.js → duplicate-B8QnX2-r.js} +1 -1
- package/dist/editor.js +7 -2
- package/dist/{environment-presets-BvHs6HEk.js → environment-presets-C-5g2pMh.js} +113 -9
- package/dist/{gameplay-D-H986A4.js → gameplay-CGSv-9XR.js} +295 -12
- package/dist/gameplay.d.ts +102 -6
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +50 -6
- package/dist/index.js +7 -6
- package/dist/{loader-CTB441z4.d.ts → loader-DyT30ArB.d.ts} +1 -1
- package/dist/{loader-lQDCwNag.js → loader-KZESOZmE.js} +148 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-CmnunaR2.js → physics-2d-_B6zj3zU.js} +57 -4
- package/dist/{physics-3d-DULB1lD1.js → physics-3d-i1IeAY0F.js} +74 -5
- package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-CgeR4ckF.js → register-Cs53CY7s.js} +110 -16
- package/dist/{register-DO5_Qp0Q.js → register-vIxU3Xbv.js} +38 -4
- package/dist/rendering-options-ByAdi8PF.js +184 -0
- package/dist/{replay-CEw2xAhG.js → replay-BkpDV51O.js} +4 -3
- package/dist/{replay-CThvwkcQ.d.ts → replay-D0GcEHYU.d.ts} +1 -1
- package/dist/{split-screen-DxZzC1nm.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
- package/dist/{split-screen-gb0c3xdb.js → split-screen-DS8RaUV_.js} +2 -2
- package/dist/{src-DYffzHbd.js → src-BLlsGJW9.js} +1 -1
- package/dist/{teardown-BordHL9o.js → teardown-LB9cTnXJ.js} +2 -2
- package/dist/{test-CXZuXs8k.js → test-81uZ967a.js} +127 -24
- package/dist/test.d.ts +7 -5
- package/dist/test.js +2 -2
- package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
- package/dist/vite.d.ts +0 -7
- package/dist/vite.js +13 -3
- package/editor/assets/{agent8-DO5qzIOD.js → agent8-C2jtNKbx.js} +1 -1
- package/editor/assets/debug-DoU3KeqS.js +3 -0
- package/editor/assets/{index-CqEUVaPN.js → index-CNqHojcC.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +20 -1
- package/skills/incanto-building-3d-games.md +19 -0
- package/skills/incanto-editor.md +5 -1
- package/skills/incanto-environment.md +2 -2
- package/skills/incanto-gameplay-behaviors.md +86 -7
- package/skills/incanto-hud.md +14 -1
- package/skills/incanto-multiplayer.md +14 -0
- package/skills/incanto-node-reference.md +60 -2
- package/skills/incanto-performance.md +35 -0
- package/skills/incanto-physics-and-input.md +69 -0
- package/skills/incanto-playtesting.md +15 -1
- package/skills/incanto-save-slots.md +21 -1
- package/skills/incanto-verifying-your-game.md +19 -0
- package/skills/incanto-web-integration.md +24 -3
- package/skills/incanto-your-first-game.md +4 -4
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +99 -53
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/game.scene.json +31 -17
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +14 -8
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
- package/templates-app/village-quest-3d/src/village.scene.json +10 -7
- package/editor/assets/debug-DntG9CVD.js +0 -3
- package/templates-app/beacon-isle-3d/coverage.json +0 -9
- package/templates-app/platformer-2d/coverage.json +0 -5
- package/templates-app/star-survivor/coverage.json +0 -5
- package/templates-app/tps-3d/coverage.json +0 -5
- package/templates-app/village-quest-3d/coverage.json +0 -9
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-
|
|
2
|
+
import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-KZESOZmE.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
5
|
import { l as registerNode } from "./registry-CF70EArN.js";
|
|
6
|
+
import { r as capturePointer } from "./touch-DEAmqGdf.js";
|
|
6
7
|
//#region src/core/audio/buses.ts
|
|
7
8
|
const AUDIO_LOG_LIMIT = 200;
|
|
8
9
|
var AudioBuses = class {
|
|
@@ -1339,11 +1340,29 @@ var HudLayer = class extends Node {
|
|
|
1339
1340
|
}
|
|
1340
1341
|
}
|
|
1341
1342
|
}
|
|
1342
|
-
/**
|
|
1343
|
+
/**
|
|
1344
|
+
* Focusable widgets under this layer, in tree order — **on screen only**.
|
|
1345
|
+
*
|
|
1346
|
+
* This filtered on each widget's OWN `visible` and recursed into hidden
|
|
1347
|
+
* containers anyway, so arrow-keys walked into closed panels and Enter really
|
|
1348
|
+
* pressed what it found. Measured on a title screen: 23 focus stops, 21 of
|
|
1349
|
+
* them under an ancestor with `visible = false`, and
|
|
1350
|
+
*
|
|
1351
|
+
* gold before = 150 -> Enter on the hidden Shop/Row0/Buy -> gold 120,
|
|
1352
|
+
* screen still 'title'
|
|
1353
|
+
*
|
|
1354
|
+
* The layer's own visibility was not a gate either. It is now: a HUD nobody
|
|
1355
|
+
* can see has no ring, and a subtree nobody can see is not walked.
|
|
1356
|
+
*
|
|
1357
|
+
* PUBLIC, because a game that wants to know what its ring contains could not
|
|
1358
|
+
* ask — and "why is focus on that" is exactly the question this answers.
|
|
1359
|
+
*/
|
|
1343
1360
|
focusables() {
|
|
1344
1361
|
const out = [];
|
|
1362
|
+
if (!this.visible) return out;
|
|
1345
1363
|
const walk = (node) => {
|
|
1346
|
-
if (node instanceof HudWidgetBase && node.
|
|
1364
|
+
if (node instanceof HudWidgetBase && !node.visible) return;
|
|
1365
|
+
if (node instanceof HudWidgetBase && node.focusable) out.push(node);
|
|
1347
1366
|
for (const child of node.children) walk(child);
|
|
1348
1367
|
};
|
|
1349
1368
|
walk(this);
|
|
@@ -1513,7 +1532,7 @@ var HudWidgetBase = class extends Node {
|
|
|
1513
1532
|
el.addEventListener("pointerdown", (event) => {
|
|
1514
1533
|
if (!this.draggable) return;
|
|
1515
1534
|
event.preventDefault();
|
|
1516
|
-
el
|
|
1535
|
+
capturePointer(el, event.pointerId);
|
|
1517
1536
|
dragging = this;
|
|
1518
1537
|
draggingFrom = dropTargetFromElement(el.parentElement);
|
|
1519
1538
|
el.style.opacity = "0.6";
|
|
@@ -1553,10 +1572,16 @@ var HudWidgetBase = class extends Node {
|
|
|
1553
1572
|
this._mountWidget();
|
|
1554
1573
|
if (!this._element) return;
|
|
1555
1574
|
}
|
|
1556
|
-
this._element.style.display = this.visible ? "" : "none";
|
|
1557
1575
|
this._followAnchor();
|
|
1558
1576
|
this._sync();
|
|
1577
|
+
if (this._element.style.display !== "none") this._shownDisplay = this._element.style.display;
|
|
1578
|
+
this._element.style.display = this.visible ? this._shownDisplay : "none";
|
|
1559
1579
|
}
|
|
1580
|
+
/**
|
|
1581
|
+
* The display this widget's own css asks for while shown — `grid` for a grid
|
|
1582
|
+
* panel, `flex` for a row, `''` for a widget that never says.
|
|
1583
|
+
*/
|
|
1584
|
+
_shownDisplay = "";
|
|
1560
1585
|
/** Which anchor slot this widget was mounted into. */
|
|
1561
1586
|
mountedAnchor = null;
|
|
1562
1587
|
/**
|
|
@@ -2280,14 +2305,9 @@ function qualityEnvironment(tier) {
|
|
|
2280
2305
|
};
|
|
2281
2306
|
if (tier === "medium") return {
|
|
2282
2307
|
shadows: { static: true },
|
|
2283
|
-
bloom: {},
|
|
2284
2308
|
post: null
|
|
2285
2309
|
};
|
|
2286
|
-
return {
|
|
2287
|
-
shadows: true,
|
|
2288
|
-
bloom: {},
|
|
2289
|
-
post: {}
|
|
2290
|
-
};
|
|
2310
|
+
return { shadows: true };
|
|
2291
2311
|
}
|
|
2292
2312
|
const QUALITY_CAPS = Object.freeze({
|
|
2293
2313
|
low: Object.freeze({
|
|
@@ -2393,6 +2413,33 @@ var Engine = class {
|
|
|
2393
2413
|
* and cleared on dispose; null when nothing renders (headless).
|
|
2394
2414
|
*/
|
|
2395
2415
|
picker = null;
|
|
2416
|
+
/** Frames the engine has stepped — monotonic, and the pick cache's key. */
|
|
2417
|
+
frameNumber = 0;
|
|
2418
|
+
/** @internal frame number + answer, so N Clickables cost ONE raycast. */
|
|
2419
|
+
pickCacheFrame = -1;
|
|
2420
|
+
pickCacheKey = "";
|
|
2421
|
+
pickCacheHit = null;
|
|
2422
|
+
/**
|
|
2423
|
+
* The node under (x, y), answered at most ONCE per frame per cursor position.
|
|
2424
|
+
*
|
|
2425
|
+
* `Clickable` asks every frame, and a board game has one per cell: 42 pads
|
|
2426
|
+
* measured **420 picker calls over ten frames** — one GPU raycast per pad per
|
|
2427
|
+
* frame, all answering the same question about the same pixel. At 60 fps that
|
|
2428
|
+
* is ~2500 raycasts a second to find out what the cursor is on, and a 200-tile
|
|
2429
|
+
* board was unusable.
|
|
2430
|
+
*
|
|
2431
|
+
* Cached on (frame, x, y), so a game that picks at two different points in one
|
|
2432
|
+
* frame still gets two real answers.
|
|
2433
|
+
*/
|
|
2434
|
+
pickAt(x, y) {
|
|
2435
|
+
if (!this.picker) return null;
|
|
2436
|
+
const key = `${x},${y}`;
|
|
2437
|
+
if (this.pickCacheFrame === this.frameNumber && this.pickCacheKey === key) return this.pickCacheHit;
|
|
2438
|
+
this.pickCacheFrame = this.frameNumber;
|
|
2439
|
+
this.pickCacheKey = key;
|
|
2440
|
+
this.pickCacheHit = this.picker(x, y);
|
|
2441
|
+
return this.pickCacheHit;
|
|
2442
|
+
}
|
|
2396
2443
|
/** Global volume buses — `master` × `sfx`/`music` gain + `muted`. AudioPlayer
|
|
2397
2444
|
* routes its volume through this; games set it for global volume/mute. */
|
|
2398
2445
|
audio = new AudioBuses();
|
|
@@ -2409,7 +2456,7 @@ var Engine = class {
|
|
|
2409
2456
|
* reset on reload. Reading it here costs nothing (localStorage, once) and
|
|
2410
2457
|
* `bindAudio` makes the volumes write themselves.
|
|
2411
2458
|
*/
|
|
2412
|
-
settings
|
|
2459
|
+
settings;
|
|
2413
2460
|
/**
|
|
2414
2461
|
* Translations, and the locale in force. English is the base and the default;
|
|
2415
2462
|
* anything a locale does not declare falls back to it silently.
|
|
@@ -2483,6 +2530,7 @@ var Engine = class {
|
|
|
2483
2530
|
this.scheduler = opts.scheduler ?? rafScheduler;
|
|
2484
2531
|
this.rng = new Rng(opts.seed ?? Math.random() * 4294967295 >>> 0);
|
|
2485
2532
|
this.maxFps = opts.maxFps ?? 0;
|
|
2533
|
+
this.settings = opts.settings instanceof Settings ? opts.settings : new Settings(opts.settings?.namespace ?? "settings");
|
|
2486
2534
|
this.sfx._setBuses(this.audio);
|
|
2487
2535
|
this.music.onTrackError = (src, reason) => {
|
|
2488
2536
|
diagnose(this, "error", `[incanto] music '${src}' will not play — ${reason}.`);
|
|
@@ -2609,8 +2657,9 @@ var Engine = class {
|
|
|
2609
2657
|
captureState() {
|
|
2610
2658
|
const root = this.scene?.root;
|
|
2611
2659
|
if (!root) return {};
|
|
2612
|
-
const { state, unaddressable } = captureBehaviors(root, this.scene?.source);
|
|
2660
|
+
const { state, unaddressable, unrecordable } = captureBehaviors(root, this.scene?.source);
|
|
2613
2661
|
for (const path of unaddressable) diagnose(this, "warn", `save: ${path} has state to save and no uid to key it under.`);
|
|
2662
|
+
if (unrecordable.length > 0) diagnose(this, "error", `[incanto] save: ${unrecordable.length} authored node(s) this run consumed have no uid, so nothing records that they are gone and LOADING THIS SAVE BRINGS THEM BACK: ${unrecordable.slice(0, 8).join(", ")}${unrecordable.length > 8 ? `, +${unrecordable.length - 8} more` : ""}. Give each one a "uid" from newUid().`);
|
|
2614
2663
|
return state;
|
|
2615
2664
|
}
|
|
2616
2665
|
/** Give it back, after the scene is loaded and onReady has run. */
|
|
@@ -2728,6 +2777,7 @@ var Engine = class {
|
|
|
2728
2777
|
scene.tree.fixedUpdate(dt);
|
|
2729
2778
|
this.fixedUpdated.emit(dt);
|
|
2730
2779
|
const startedUpdate = performance.now();
|
|
2780
|
+
this.frameNumber += 1;
|
|
2731
2781
|
scene.tree.update(dt);
|
|
2732
2782
|
this.music.tick(dt);
|
|
2733
2783
|
this.updated.emit(dt);
|
|
@@ -2762,6 +2812,7 @@ var Engine = class {
|
|
|
2762
2812
|
}
|
|
2763
2813
|
if (this.accumulator >= this.fixedStep) this.accumulator %= this.fixedStep;
|
|
2764
2814
|
const startedUpdate = performance.now();
|
|
2815
|
+
this.frameNumber += 1;
|
|
2765
2816
|
scene.tree.update(dt);
|
|
2766
2817
|
this.music.tick(dt);
|
|
2767
2818
|
this.updated.emit(dt);
|
|
@@ -3158,6 +3209,7 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3158
3209
|
];
|
|
3159
3210
|
static props = {
|
|
3160
3211
|
...HudWidgetBase.props,
|
|
3212
|
+
focusable: { default: true },
|
|
3161
3213
|
anchor: {
|
|
3162
3214
|
default: "bottom",
|
|
3163
3215
|
options: [
|
|
@@ -3207,6 +3259,49 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3207
3259
|
});
|
|
3208
3260
|
}
|
|
3209
3261
|
/**
|
|
3262
|
+
* A dialogue takes focus while it is up.
|
|
3263
|
+
*
|
|
3264
|
+
* It paints a focus ring and the generated reference advertises the prop, and
|
|
3265
|
+
* it was inert: choices are raw DOM buttons inside the widget, so
|
|
3266
|
+
* `HudLayer.focusables()` can never see them, and this class — unlike
|
|
3267
|
+
* `UiButton` a hundred lines above it — never overrode `_activate`. Eleven
|
|
3268
|
+
* keys and eight pad codes did nothing to a waiting choice while the same
|
|
3269
|
+
* Enter pressed a sibling button. Native Tab worked, and is closed by any
|
|
3270
|
+
* game that binds Tab; a pad has no Tab at all.
|
|
3271
|
+
*/
|
|
3272
|
+
focusable = true;
|
|
3273
|
+
/** Which choice the keyboard/pad is on. Reset by every new line. */
|
|
3274
|
+
cursor = 0;
|
|
3275
|
+
/**
|
|
3276
|
+
* Enter / A, and left / right between the options.
|
|
3277
|
+
*
|
|
3278
|
+
* `dir` is 0 for "activate" and ±1 for a nudge, the same shape `UiSelect`
|
|
3279
|
+
* uses — so one key layout drives a menu, a slider and a conversation.
|
|
3280
|
+
*/
|
|
3281
|
+
_activate(dir) {
|
|
3282
|
+
if (!this.active || !this.current) return;
|
|
3283
|
+
const choices = this.current.choices;
|
|
3284
|
+
if (!choices || choices.length === 0) {
|
|
3285
|
+
this.advance();
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
if (dir !== 0) {
|
|
3289
|
+
this.cursor = (this.cursor + dir + choices.length) % choices.length;
|
|
3290
|
+
this.paintCursor();
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
this.choose(this.cursor);
|
|
3294
|
+
}
|
|
3295
|
+
/** Show which option the keyboard is on — a ring nobody can see is not one. */
|
|
3296
|
+
paintCursor() {
|
|
3297
|
+
const buttons = this.choicesEl?.children;
|
|
3298
|
+
if (!buttons) return;
|
|
3299
|
+
for (let i = 0; i < buttons.length; i++) {
|
|
3300
|
+
const el = buttons[i];
|
|
3301
|
+
if (el) el.style.outline = i === this.cursor ? "2px solid #fff" : "none";
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3210
3305
|
* Player pressed "next": mid-typing reveals the whole line; a fully-typed
|
|
3211
3306
|
* line without choices advances (choice lines wait for `choose`).
|
|
3212
3307
|
*/
|
|
@@ -3289,12 +3384,11 @@ var UiDialogue = class extends HudWidgetBase {
|
|
|
3289
3384
|
this.revealed = Math.round(progress * this.current.text.length);
|
|
3290
3385
|
if (this.speakerEl) this.speakerEl.textContent = this.current.speaker;
|
|
3291
3386
|
}
|
|
3292
|
-
|
|
3293
|
-
if (wasTyping) this.revealed = Math.min(this.current.text.length, this.revealed + this.charsPerSecond * dt);
|
|
3387
|
+
if (this.revealed < this.current.text.length) this.revealed = Math.min(this.current.text.length, this.revealed + this.charsPerSecond * dt);
|
|
3294
3388
|
const doneTyping = this.revealed >= this.current.text.length;
|
|
3295
3389
|
if (this.textEl) this.textEl.textContent = this.showing;
|
|
3296
3390
|
if (this.hintEl) this.hintEl.style.visibility = doneTyping && !this.current.choices ? "visible" : "hidden";
|
|
3297
|
-
if (doneTyping &&
|
|
3391
|
+
if (doneTyping && this.current.choices && this.choicesEl && this.choicesEl.children.length === 0) this.current.choices.forEach((label, i) => {
|
|
3298
3392
|
const b = document.createElement("button");
|
|
3299
3393
|
b.type = "button";
|
|
3300
3394
|
b.textContent = label;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-
|
|
2
|
-
import { L as translateOn, t as registerCoreNodes } from "./register-
|
|
1
|
+
import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-KZESOZmE.js";
|
|
2
|
+
import { L as translateOn, t as registerCoreNodes } from "./register-Cs53CY7s.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
5
5
|
import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
|
|
6
6
|
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
|
|
7
|
-
import {
|
|
7
|
+
import { F as localFromWorld, I as worldPosition } from "./gameplay-CGSv-9XR.js";
|
|
8
8
|
import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
|
|
9
9
|
import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
|
|
10
10
|
//#region src/2d/physics/collider-validate.ts
|
|
@@ -223,6 +223,14 @@ var StaticBody2D = class extends PhysicsBody2D {
|
|
|
223
223
|
*/
|
|
224
224
|
var Area2D = class extends PhysicsBody2D {
|
|
225
225
|
static typeName = "Area2D";
|
|
226
|
+
/**
|
|
227
|
+
* Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
|
|
228
|
+
* See `Area3D.overlapping`. Empty in a scene with no physics world.
|
|
229
|
+
*/
|
|
230
|
+
overlapping(group) {
|
|
231
|
+
const all = this._physics?.overlapping(this) ?? [];
|
|
232
|
+
return group === void 0 ? all : all.filter((n) => n.isInGroup(group));
|
|
233
|
+
}
|
|
226
234
|
};
|
|
227
235
|
/** Dynamic simulated body. */
|
|
228
236
|
var RigidBody2D = class extends PhysicsBody2D {
|
|
@@ -334,7 +342,8 @@ var Joint2D = class extends Node2D {
|
|
|
334
342
|
targetAnchor: { default: [0, 0] },
|
|
335
343
|
length: { default: 0 },
|
|
336
344
|
stiffness: { default: 50 },
|
|
337
|
-
damping: { default: 5 }
|
|
345
|
+
damping: { default: 5 },
|
|
346
|
+
collide: { default: null }
|
|
338
347
|
};
|
|
339
348
|
type = "revolute";
|
|
340
349
|
target = "";
|
|
@@ -344,6 +353,31 @@ var Joint2D = class extends Node2D {
|
|
|
344
353
|
length = 0;
|
|
345
354
|
stiffness = 50;
|
|
346
355
|
damping = 5;
|
|
356
|
+
/**
|
|
357
|
+
* Do the two linked bodies still collide with EACH OTHER?
|
|
358
|
+
*
|
|
359
|
+
* `null` (default) decides from the joint type, because the two families want
|
|
360
|
+
* opposite answers:
|
|
361
|
+
*
|
|
362
|
+
* - **pivots** (`fixed`, `spherical`, `revolute`) → **off**. A hinge's bodies
|
|
363
|
+
* overlap at the pivot by construction, so the contact solver fights the
|
|
364
|
+
* joint and flings them. A puzzle builder's first hinge produced a bar that
|
|
365
|
+
* spun chaotically forever and threw a 40 kg box thirty metres, and they
|
|
366
|
+
* nearly filed "spherical joints inject energy":
|
|
367
|
+
*
|
|
368
|
+
* ```
|
|
369
|
+
* t= 0 Plank r=[0,0,0] Box [ 0.00, 5.50, 2.40]
|
|
370
|
+
* t=3000 Plank r=[-66,24,64] Box [-29.70, 0.40, -4.91]
|
|
371
|
+
* ```
|
|
372
|
+
*
|
|
373
|
+
* - **tethers** (`rope`, `spring`) → **on**. These anchor a thing to the world,
|
|
374
|
+
* and the thing has to rest on what it is tethered to. Defaulting these off
|
|
375
|
+
* drops a barrel roped to the floor straight through it — measured, in this
|
|
376
|
+
* repo's own conformance room.
|
|
377
|
+
*
|
|
378
|
+
* Set `true`/`false` to say it outright.
|
|
379
|
+
*/
|
|
380
|
+
collide = null;
|
|
347
381
|
onEnterTree() {
|
|
348
382
|
if (!(this.parent instanceof PhysicsBody2D)) throw new IncantoError("TREE_VIOLATION", `Joint2D '${this.name}' must be a child of a physics body (its body A); parent is '${this.parent?.name ?? "none"}'.`);
|
|
349
383
|
if (this.target === "") throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}' needs a "target" node path to the body it links to.`);
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
//#region src/core/rendering-options.ts
|
|
174
|
+
function resolveRendering(environment, fallback, devicePixelRatio, explicit) {
|
|
175
|
+
const rendering = environment?.rendering ?? {};
|
|
176
|
+
const scenePixelRatio = rendering.pixelRatio === "device" ? devicePixelRatio : rendering.pixelRatio;
|
|
177
|
+
return {
|
|
178
|
+
antialias: explicit?.antialias ?? rendering.antialias ?? fallback.antialias,
|
|
179
|
+
pixelRatio: explicit?.pixelRatio ?? scenePixelRatio ?? fallback.pixelRatio,
|
|
180
|
+
preserveDrawingBuffer: explicit?.preserveDrawingBuffer ?? rendering.preserveDrawingBuffer ?? fallback.preserveDrawingBuffer
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { parseDrive as i, logReport as n, logText as r, resolveRendering as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-
|
|
2
|
-
import { R as translationKey, k as HudWidgetBase } from "./register-
|
|
1
|
+
import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-KZESOZmE.js";
|
|
2
|
+
import { R as translationKey, k as HudWidgetBase } from "./register-Cs53CY7s.js";
|
|
3
3
|
import { o as getNodeType } from "./registry-CF70EArN.js";
|
|
4
4
|
//#region src/core/audit.ts
|
|
5
5
|
const ANIMATED_TYPES = new Set([
|
|
@@ -166,7 +166,8 @@ function auditScene(scene, opts = {}) {
|
|
|
166
166
|
if (cameraDrivers.length > 0 && followedCameras.length > 0) warnings.push(`${followedCameras.join(", ")}: a FollowCamera writes this camera's position every frame, and so does the CharacterController3D at ${cameraDrivers.join(", ")} — the controller owns the camera in every view it has. Drop one of them: use the controller's own \`view\`/\`camDistance\`, or take the controller off the camera duty.`);
|
|
167
167
|
if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
|
|
168
168
|
if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
|
|
169
|
-
|
|
169
|
+
const isFragment = cameras === 0;
|
|
170
|
+
if (scene.dimension === "3d" && !isFragment && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
|
|
170
171
|
if (damageGroups.length > 0) {
|
|
171
172
|
const hurtable = hurtableGroups(root);
|
|
172
173
|
for (const { path, group } of damageGroups) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Lt as Signal, b as Engine, jt as Node } from "./behavior-
|
|
1
|
+
import { Lt as Signal, b as Engine, jt as Node } from "./behavior-ZnfzKDo3.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
|
|
4
4
|
|
|
5
5
|
//#region src/net/types.d.ts
|
|
6
6
|
type Unsubscribe = () => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-
|
|
2
|
-
import { t as registerCoreNodes, v as Engine } from "./register-
|
|
1
|
+
import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-KZESOZmE.js";
|
|
2
|
+
import { t as registerCoreNodes, v as Engine } from "./register-Cs53CY7s.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
4
|
import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
|
|
5
5
|
import { l as registerNode } from "./registry-CF70EArN.js";
|
|
@@ -214,6 +214,6 @@ function newUid() {
|
|
|
214
214
|
//#endregion
|
|
215
215
|
//#region src/index.ts
|
|
216
216
|
/** Engine version. Kept in sync with package.json by the release pipeline. */
|
|
217
|
-
const VERSION = "0.
|
|
217
|
+
const VERSION = "0.67.0";
|
|
218
218
|
//#endregion
|
|
219
219
|
export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as AudioPlayer } from "./register-
|
|
2
|
-
import {
|
|
1
|
+
import { _ as AudioPlayer } from "./register-Cs53CY7s.js";
|
|
2
|
+
import { i as parseDrive, n as logReport } from "./rendering-options-ByAdi8PF.js";
|
|
3
3
|
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
|
|
4
4
|
//#region src/core/audio-errors.ts
|
|
5
5
|
/** Every AudioPlayer under `root` whose source could not be played. */
|