incanto 0.68.0 → 0.69.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/assets/catalog.json +9 -5
- package/bin/incanto-assets.mjs +14 -1
- package/bin/incanto-check.mjs +53 -3
- package/bin/incanto-new.mjs +29 -1
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-verify.mjs +40 -0
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +7 -5
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +4 -4
- package/dist/3d.js +7 -7
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-C0j2k-yB.d.ts} +32 -13
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-rm8vVzu_.d.ts} +67 -0
- package/dist/{create-game-DbWtVTxD.js → create-game-CvJ1wSRP.js} +13 -9
- package/dist/{create-game-IZIydDwI.js → create-game-CwxEDlCm.js} +13 -9
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-IWIqk0HJ.js → duplicate-Dj4L-5AA.js} +1 -1
- package/dist/editor.js +1 -1
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-BO-7XoFV.js} +4 -4
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-DM1eu_cV.js → gameplay-D1KixWK8.js} +157 -151
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/{loader-BC4PNtJX.js → loader-DZE7B07H.js} +22 -6
- package/dist/{loader-CcB533FR.d.ts → loader-DadV2QCT.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CF5yyjbj.js} +2 -2
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-DbYaEcZy.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-ibCjm-wH.js → register-DbVArsns.js} +19 -10
- package/dist/{register-3ta-2Xig.js → register-mmfugpaQ.js} +107 -15
- package/dist/{replay-CYvhVHHN.js → replay-BEHLDcQV.js} +2 -2
- package/dist/{replay-Dvn8aeBd.d.ts → replay-BQfkX4x7.d.ts} +1 -1
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-CYwDkbLF.js → split-screen-CauMwu6y.js} +2 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen-Cg76MCV5.d.ts} +2 -2
- package/dist/{src-D7RIqXYF.js → src-CYEu-_S9.js} +1 -1
- package/dist/{teardown-D2NEmxPB.js → teardown-CMqFVxO9.js} +5 -4
- package/dist/{test-8hoHeRmo.js → test-D6ISmuFE.js} +60 -17
- package/dist/test.d.ts +15 -4
- package/dist/test.js +2 -2
- package/dist/vite.d.ts +70 -2
- package/dist/vite.js +176 -14
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-Z5_g8x9H.js} +12 -1
- package/editor/assets/{agent8-BrrHOjMJ.js → agent8-BDvaZMQJ.js} +1 -1
- package/editor/assets/{debug-BBhKuBNV.js → debug-CMULinBW.js} +1 -1
- package/editor/assets/{index-eVd0BToA.js → index-CZIRr5EL.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +4 -3
- package/skills/incanto-assets.md +16 -2
- package/skills/incanto-audio.md +19 -3
- package/skills/incanto-building-2d-games.md +29 -0
- package/skills/incanto-building-3d-games.md +60 -2
- package/skills/incanto-gameplay-behaviors.md +18 -1
- package/skills/incanto-save-slots.md +27 -0
- package/skills/incanto-verifying-your-game.md +96 -2
- package/skills/incanto-your-first-game.md +2 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/main.ts +13 -3
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +17 -27
- package/templates-app/platformer-2d/src/main.ts +24 -12
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/main.ts +17 -6
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/main.ts +26 -15
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/main.ts +13 -3
|
@@ -1,11 +1,11 @@
|
|
|
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-DZE7B07H.js";
|
|
2
|
+
import { L as translateOn, t as registerCoreNodes } from "./register-mmfugpaQ.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 { F as localFromWorld, I as worldPosition } from "./gameplay-
|
|
8
|
-
import { t as checkSheetGrid } from "./sheet-grid-
|
|
7
|
+
import { F as localFromWorld, I as worldPosition } from "./gameplay-D1KixWK8.js";
|
|
8
|
+
import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.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
|
|
11
11
|
/**
|
|
@@ -1443,9 +1443,11 @@ var TileMap2D = class extends Node2D {
|
|
|
1443
1443
|
_solid = [];
|
|
1444
1444
|
/** Rebuild flags — raised when the map-shaping props are REPLACED. */
|
|
1445
1445
|
geometryDirty = true;
|
|
1446
|
-
/**
|
|
1446
|
+
/** `texture|highestTile` already reported, so a bad grid says so once. */
|
|
1447
1447
|
gridChecked = "";
|
|
1448
1448
|
collidersDirty = true;
|
|
1449
|
+
/** The `scale` the colliders were last built for — see `rebuildColliders`. */
|
|
1450
|
+
builtScale = [1, 1];
|
|
1449
1451
|
/** Replace the whole array to change the map (mutations are not watched). */
|
|
1450
1452
|
get cells() {
|
|
1451
1453
|
return this._cells;
|
|
@@ -1487,19 +1489,23 @@ var TileMap2D = class extends Node2D {
|
|
|
1487
1489
|
this.rebuildColliders();
|
|
1488
1490
|
}
|
|
1489
1491
|
update(_dt) {
|
|
1492
|
+
const [sx = 1, sy = 1] = this.scale;
|
|
1493
|
+
if (sx !== this.builtScale[0] || sy !== this.builtScale[1]) this.collidersDirty = true;
|
|
1490
1494
|
if (this.collidersDirty) this.rebuildColliders();
|
|
1491
1495
|
}
|
|
1492
1496
|
rebuildColliders() {
|
|
1493
1497
|
this.collidersDirty = false;
|
|
1494
1498
|
for (const child of [...this.children]) if (child.name.startsWith("__tileSolid")) child.free();
|
|
1499
|
+
this.builtScale = [this.scale[0] ?? 1, this.scale[1] ?? 1];
|
|
1495
1500
|
if (this._solid.length === 0) return;
|
|
1496
1501
|
const ts = this.tileSize;
|
|
1502
|
+
const [sx, sy] = this.builtScale;
|
|
1497
1503
|
mergeSolidRects(this.grid(), new Set(this._solid)).forEach((r, i) => {
|
|
1498
1504
|
const body = new StaticBody2D(`__tileSolid${i}`);
|
|
1499
|
-
body.position = [(r.x + r.w / 2) * ts, (r.y + r.h / 2) * ts];
|
|
1505
|
+
body.position = [(r.x + r.w / 2) * ts * sx, (r.y + r.h / 2) * ts * sy];
|
|
1500
1506
|
body.collider = {
|
|
1501
1507
|
shape: "rect",
|
|
1502
|
-
size: [r.w * ts, r.h * ts]
|
|
1508
|
+
size: [r.w * ts * sx, r.h * ts * sy]
|
|
1503
1509
|
};
|
|
1504
1510
|
this.addChild(body);
|
|
1505
1511
|
});
|
|
@@ -1543,9 +1549,12 @@ var TileMap2D = class extends Node2D {
|
|
|
1543
1549
|
mat.needsUpdate = true;
|
|
1544
1550
|
this.geometryDirty = true;
|
|
1545
1551
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1552
|
+
let highestAsked = -1;
|
|
1553
|
+
for (const row of this.grid()) for (const tile of row) if (tile > highestAsked) highestAsked = tile;
|
|
1554
|
+
const checkKey = `${this.texture}|${highestAsked}`;
|
|
1555
|
+
if (this.gridChecked !== checkKey) {
|
|
1556
|
+
this.gridChecked = checkKey;
|
|
1557
|
+
const problem = checkSheetGrid(this.texture, this.tileSize, this.tileSize, image.width, image.height, highestAsked >= 0 ? highestAsked : void 0, "tile");
|
|
1549
1558
|
if (problem) diagnose(this.tree?.engine ?? null, "error", problem);
|
|
1550
1559
|
}
|
|
1551
1560
|
const columns = this.columns > 0 ? this.columns : Math.max(1, Math.floor(image.width / this.tileSize));
|
|
@@ -1,5 +1,5 @@
|
|
|
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-DZE7B07H.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";
|
|
@@ -2031,6 +2031,25 @@ var FrameStatsRing = class {
|
|
|
2031
2031
|
const span = this.spanMs();
|
|
2032
2032
|
return span > 0 ? span / (this.count - 1) : 0;
|
|
2033
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* How long ago the newest sample arrived, in ms. `null` with no samples.
|
|
2036
|
+
*
|
|
2037
|
+
* `fps()` is a rate over a WINDOW, so it keeps answering after the frames
|
|
2038
|
+
* stop: a hidden tab, a descheduled loop, a page the browser froze. Measured
|
|
2039
|
+
* on an engine that ticked 40 times and then stopped being ticked:
|
|
2040
|
+
*
|
|
2041
|
+
* while ticking : fps 59.9
|
|
2042
|
+
* after 0 more ticks : fps 59.9 <- the last sample is 668 ms old
|
|
2043
|
+
*
|
|
2044
|
+
* Both numbers are true about the window they describe. What was missing was
|
|
2045
|
+
* any way to ask WHEN that window was, so "60 fps" read the same whether it
|
|
2046
|
+
* was measured 8 ms ago or four seconds ago.
|
|
2047
|
+
*/
|
|
2048
|
+
ageMs(nowMs) {
|
|
2049
|
+
if (this.count === 0) return null;
|
|
2050
|
+
const newest = this.times[(this.head + this.count - 1) % this.capacity];
|
|
2051
|
+
return Math.max(0, nowMs - newest);
|
|
2052
|
+
}
|
|
2034
2053
|
/** Forget all samples (engine.stop() — a paused game has no frame rate). */
|
|
2035
2054
|
clear() {
|
|
2036
2055
|
this.head = 0;
|
|
@@ -2433,6 +2452,49 @@ var Engine = class {
|
|
|
2433
2452
|
* learns about Rapier or three. From a behavior, prefer `this.physics`.
|
|
2434
2453
|
*/
|
|
2435
2454
|
physics = null;
|
|
2455
|
+
/**
|
|
2456
|
+
* How to build a physics world for a scene that needs one.
|
|
2457
|
+
*
|
|
2458
|
+
* `createGame2D`/`createGame3D` install this; core never learns what Rapier
|
|
2459
|
+
* is. It exists because `physics: 'auto'` can only look at the scene the game
|
|
2460
|
+
* BOOTED with, and the most universal act of finishing a game is putting a
|
|
2461
|
+
* title screen in front of it:
|
|
2462
|
+
*
|
|
2463
|
+
* ```
|
|
2464
|
+
* after boot on the title : engine.physics = null
|
|
2465
|
+
* after goToScene into game: engine.physics = null
|
|
2466
|
+
* player y after 2 s : 0 -> 0
|
|
2467
|
+
* ```
|
|
2468
|
+
*
|
|
2469
|
+
* The game rendered perfectly and was inert — nothing fell, nothing collided,
|
|
2470
|
+
* nothing could be collected or hurt. One scene swap turned a working game
|
|
2471
|
+
* into a diorama, and nothing was logged.
|
|
2472
|
+
*/
|
|
2473
|
+
physicsProvider = null;
|
|
2474
|
+
ensuring = null;
|
|
2475
|
+
/**
|
|
2476
|
+
* Give the current scene a physics world if it needs one and has none.
|
|
2477
|
+
*
|
|
2478
|
+
* Idempotent and safe to call on every swap: the provider decides whether the
|
|
2479
|
+
* incoming tree actually wants physics, and an explicit `physics: false` at
|
|
2480
|
+
* boot installs no provider at all, so opting out stays opted out.
|
|
2481
|
+
*
|
|
2482
|
+
* Asynchronous because the Rapier WASM has to load. The first frames after a
|
|
2483
|
+
* swap therefore run unsimulated — which every AI behaviour already tolerates
|
|
2484
|
+
* (`moveBody` falls back to a direct write when `_physics` is null) and which
|
|
2485
|
+
* is a hitch, where the alternative was a game that never simulated at all.
|
|
2486
|
+
*/
|
|
2487
|
+
ensurePhysics() {
|
|
2488
|
+
if (this.physics || !this.physicsProvider || !this._scene) return Promise.resolve();
|
|
2489
|
+
if (this.ensuring) return this.ensuring;
|
|
2490
|
+
const root = this._scene.root;
|
|
2491
|
+
this.ensuring = this.physicsProvider(root).catch((e) => {
|
|
2492
|
+
this.log.error(`[incanto] could not enable physics for this scene: ${String(e?.message ?? e)}`);
|
|
2493
|
+
}).finally(() => {
|
|
2494
|
+
this.ensuring = null;
|
|
2495
|
+
});
|
|
2496
|
+
return this.ensuring;
|
|
2497
|
+
}
|
|
2436
2498
|
/** Seeded randomness for game logic (deterministic when `seed` is set). */
|
|
2437
2499
|
rng;
|
|
2438
2500
|
/** The engine log channel (debug overlay + headless harness tail this). */
|
|
@@ -2756,6 +2818,7 @@ var Engine = class {
|
|
|
2756
2818
|
return {
|
|
2757
2819
|
fps: this.frameStats.fps(),
|
|
2758
2820
|
frameMs: this.frameStats.frameMs(),
|
|
2821
|
+
lastFrameAgeMs: this.frameStats.ageMs(performance.now()),
|
|
2759
2822
|
nodes: this._scene?.tree.root ? countNodes(this._scene.tree.root) : 0,
|
|
2760
2823
|
running: this.disposeScheduler !== null,
|
|
2761
2824
|
errors: this.errorCount,
|
|
@@ -2808,6 +2871,7 @@ var Engine = class {
|
|
|
2808
2871
|
this._time += dt;
|
|
2809
2872
|
this._unscaledTime += this.fixedStep;
|
|
2810
2873
|
const startedFixed = performance.now();
|
|
2874
|
+
this.input.beginFrame();
|
|
2811
2875
|
scene.tree.fixedUpdate(dt);
|
|
2812
2876
|
this.fixedUpdated.emit(dt);
|
|
2813
2877
|
const startedUpdate = performance.now();
|
|
@@ -2835,6 +2899,7 @@ var Engine = class {
|
|
|
2835
2899
|
this.lastMs = nowMs;
|
|
2836
2900
|
const scene = this._scene;
|
|
2837
2901
|
if (!scene) return;
|
|
2902
|
+
this.input.beginFrame();
|
|
2838
2903
|
this.accumulator += dt;
|
|
2839
2904
|
let steps = 0;
|
|
2840
2905
|
const startedFixed = performance.now();
|
|
@@ -3007,7 +3072,8 @@ var AudioPlayer = class extends Node {
|
|
|
3007
3072
|
name,
|
|
3008
3073
|
from: this.getPath(),
|
|
3009
3074
|
bus: this.bus === "music" ? "music" : "sfx",
|
|
3010
|
-
at: engine.time
|
|
3075
|
+
at: engine.time,
|
|
3076
|
+
...this.loop ? { loop: true } : {}
|
|
3011
3077
|
});
|
|
3012
3078
|
}
|
|
3013
3079
|
/** Final gain = engine buses × this volume (1 when not in a tree). */
|
|
@@ -3040,31 +3106,57 @@ var AudioPlayer = class extends Node {
|
|
|
3040
3106
|
};
|
|
3041
3107
|
}
|
|
3042
3108
|
/**
|
|
3043
|
-
* Start the sound
|
|
3109
|
+
* Start the sound, now.
|
|
3110
|
+
*
|
|
3111
|
+
* **Takes no arguments, on purpose.** This is the method scenes wire signals
|
|
3112
|
+
* to — `incanto-audio.md` teaches
|
|
3113
|
+
* `{"signal": "collected", "from": "Player/Collector", "to": "Coin",
|
|
3114
|
+
* "handler": "play"}` — and a signal hands its handler whatever it carries.
|
|
3115
|
+
* `collected` leads with a NUMBER. When this briefly took an optional
|
|
3116
|
+
* scheduling time, that wiring became `play(10)`, which scheduled the pickup
|
|
3117
|
+
* sound at ABSOLUTE audio-clock second 10: inaudible for the first ten
|
|
3118
|
+
* seconds of the game and fine thereafter. The same optional parameter also
|
|
3119
|
+
* made `Function.length` 1, so the engine's own arity checker cried wolf on
|
|
3120
|
+
* the shipped `platformer-2d` template's own harness.
|
|
3121
|
+
*
|
|
3122
|
+
* Scheduling has its own name: {@link playAt}.
|
|
3123
|
+
*/
|
|
3124
|
+
/**
|
|
3125
|
+
* Start the sound at `when` on the AUDIO clock (`engine.sfx.now` + a lead).
|
|
3044
3126
|
*
|
|
3045
3127
|
* A frame is a 16.67 ms grid at 60 Hz and 33.33 ms at 30, so a sound fired
|
|
3046
3128
|
* from `update()` cannot land closer than one frame to where a chart wants
|
|
3047
|
-
* it — measured 0 of 64 notes
|
|
3048
|
-
*
|
|
3049
|
-
* ms:
|
|
3129
|
+
* it — measured 0 of 64 notes given a scheduled start. Queue them a lead
|
|
3130
|
+
* ahead and the same notes land at |mean| 0.000000 ms:
|
|
3050
3131
|
*
|
|
3051
3132
|
* ```ts
|
|
3052
|
-
* const
|
|
3053
|
-
* for (const note of dueSoon(engine.sfx.now +
|
|
3054
|
-
* hit.
|
|
3133
|
+
* const LEAD = 0.08;
|
|
3134
|
+
* for (const note of dueSoon(engine.sfx.now + LEAD)) {
|
|
3135
|
+
* hit.playAt(startedAt + note.atSec);
|
|
3055
3136
|
* }
|
|
3056
3137
|
* ```
|
|
3057
3138
|
*
|
|
3058
3139
|
* PRESETS only — a `src` clip goes through an `<audio>` element, which has no
|
|
3059
|
-
* scheduling clock,
|
|
3060
|
-
* approximated.
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
3140
|
+
* scheduling clock, so `playAt` on one plays immediately rather than being
|
|
3141
|
+
* silently approximated. A time already past plays now (Web Audio's own
|
|
3142
|
+
* rule), so a late scheduler is late, not silent. The bus gain, the
|
|
3143
|
+
* `engine.audio.recent()` entry and `muted` all still apply.
|
|
3144
|
+
*
|
|
3145
|
+
* Separate from {@link play} because `play` is what scenes wire signals to,
|
|
3146
|
+
* and a signal hands its handler whatever it carries.
|
|
3063
3147
|
*/
|
|
3064
|
-
|
|
3148
|
+
playAt(when) {
|
|
3149
|
+
if (this.preset !== "custom") {
|
|
3150
|
+
this.note("preset", this.preset);
|
|
3151
|
+
this.playPreset(Number.isFinite(when) ? when : void 0);
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
this.play();
|
|
3155
|
+
}
|
|
3156
|
+
play() {
|
|
3065
3157
|
if (this.preset !== "custom") {
|
|
3066
3158
|
this.note("preset", this.preset);
|
|
3067
|
-
this.playPreset(
|
|
3159
|
+
this.playPreset();
|
|
3068
3160
|
return;
|
|
3069
3161
|
}
|
|
3070
3162
|
if (!this.src) return;
|
|
@@ -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-DZE7B07H.js";
|
|
2
|
+
import { R as translationKey, k as HudWidgetBase } from "./register-mmfugpaQ.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([
|
|
@@ -35,7 +35,7 @@ function divisors(total) {
|
|
|
35
35
|
* known — an animation range that runs off the end is the other half of the
|
|
36
36
|
* same mistake.
|
|
37
37
|
*/
|
|
38
|
-
function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame) {
|
|
38
|
+
function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame, unit = "frame") {
|
|
39
39
|
if (!imageWidth || !imageHeight || !frameWidth || !frameHeight) return null;
|
|
40
40
|
if (imageWidth % frameWidth !== 0 || imageHeight % frameHeight !== 0) {
|
|
41
41
|
const parts = [];
|
|
@@ -51,7 +51,11 @@ function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, h
|
|
|
51
51
|
}
|
|
52
52
|
if (highestFrame !== void 0) {
|
|
53
53
|
const frames = imageWidth / frameWidth * (imageHeight / frameHeight);
|
|
54
|
-
if (highestFrame >= frames)
|
|
54
|
+
if (highestFrame >= frames) {
|
|
55
|
+
const sheet = unit === "tile" ? "tile atlas" : "spritesheet";
|
|
56
|
+
const asks = unit === "tile" ? "a cell asks for tile" : "an animation asks for frame";
|
|
57
|
+
return `[incanto] ${sheet} '${key}' has ${frames} ${unit}s (0–${frames - 1}), and ${asks} ${highestFrame}. Those ${unit}s draw whatever is at the wrong end of the sheet.`;
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
return null;
|
|
57
61
|
}
|
|
@@ -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-DZE7B07H.js";
|
|
2
|
+
import { t as registerCoreNodes, v as Engine } from "./register-mmfugpaQ.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";
|
|
@@ -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-rm8vVzu_.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
|
|
4
4
|
|
|
5
5
|
//#region src/net/types.d.ts
|
|
6
6
|
type Unsubscribe = () => void;
|
|
@@ -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.69.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,6 +1,6 @@
|
|
|
1
|
-
import { _ as AudioPlayer } from "./register-
|
|
2
|
-
import { i as logReport, o as parseDrive } from "./webgl-unavailable-
|
|
3
|
-
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-
|
|
1
|
+
import { _ as AudioPlayer } from "./register-mmfugpaQ.js";
|
|
2
|
+
import { i as logReport, o as parseDrive } from "./webgl-unavailable-Z5_g8x9H.js";
|
|
3
|
+
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-Dlq13Gyj.js";
|
|
4
4
|
//#region src/core/audio-errors.ts
|
|
5
5
|
/** Every AudioPlayer under `root` whose source could not be played. */
|
|
6
6
|
function audioErrors(root) {
|
|
@@ -35,7 +35,7 @@ function claimCanvasGestures(canvas) {
|
|
|
35
35
|
* empty list outside a dev server.
|
|
36
36
|
*/
|
|
37
37
|
function wireDevChannel(opts) {
|
|
38
|
-
const hot = import.meta.hot;
|
|
38
|
+
const hot = import.meta.hot ?? globalThis.__incantoInspect;
|
|
39
39
|
const cleanups = [];
|
|
40
40
|
if (!hot) return cleanups;
|
|
41
41
|
const onFrameRequest = (data) => {
|
|
@@ -59,6 +59,7 @@ function wireDevChannel(opts) {
|
|
|
59
59
|
report: {
|
|
60
60
|
...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
|
|
61
61
|
signature: frameSignature(shot.pixels, shot.width, shot.height),
|
|
62
|
+
hidden: typeof document !== "undefined" && document.hidden,
|
|
62
63
|
...droveFrames === null ? {} : { droveFrames },
|
|
63
64
|
...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
|
|
64
65
|
width: w,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-
|
|
2
|
-
import { v as Engine } from "./register-
|
|
1
|
+
import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-DZE7B07H.js";
|
|
2
|
+
import { v as Engine } from "./register-mmfugpaQ.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 { i as getNodeSchema, s as mergeStaticProps } from "./registry-CF70EArN.js";
|
|
6
|
-
import { n as startRecording } from "./replay-
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { t as registerNodes2D } from "./register-
|
|
9
|
-
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-
|
|
10
|
-
import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-
|
|
6
|
+
import { n as startRecording } from "./replay-BEHLDcQV.js";
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-D1KixWK8.js";
|
|
8
|
+
import { t as registerNodes2D } from "./register-DbVArsns.js";
|
|
9
|
+
import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BO-7XoFV.js";
|
|
10
|
+
import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-CauMwu6y.js";
|
|
11
11
|
import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
12
12
|
//#region src/test/framing.ts
|
|
13
13
|
/**
|
|
@@ -252,7 +252,7 @@ function describeFraming2D(scene, placed, opts) {
|
|
|
252
252
|
round(cy),
|
|
253
253
|
0
|
|
254
254
|
],
|
|
255
|
-
view: [vw, vh],
|
|
255
|
+
view: [round(vw / zoom), round(vh / zoom)],
|
|
256
256
|
zoom
|
|
257
257
|
};
|
|
258
258
|
}
|
|
@@ -630,6 +630,23 @@ var Driver = class {
|
|
|
630
630
|
headingLeft = 0;
|
|
631
631
|
visits = /* @__PURE__ */ new Map();
|
|
632
632
|
buttons;
|
|
633
|
+
/**
|
|
634
|
+
* Buttons the bot pressed and will never press again, because pressing them
|
|
635
|
+
* STOPPED THE CLOCK.
|
|
636
|
+
*
|
|
637
|
+
* It drove every declared button action, and `incanto-your-first-game.md`
|
|
638
|
+
* tells you to declare `pause`. Measured on one losable scene: 0/10 losses
|
|
639
|
+
* with `pause` declared, 10/10 without it, same seed, same everything else.
|
|
640
|
+
* A frozen game cannot hurt you, so the run also reported the game as having
|
|
641
|
+
* no way to lose.
|
|
642
|
+
*
|
|
643
|
+
* This is OBSERVED, not guessed from the name — a game is free to call it
|
|
644
|
+
* `menu`, `escape`, `halt` or nothing at all, and the bot notices the same
|
|
645
|
+
* way a player would: the world stopped moving.
|
|
646
|
+
*/
|
|
647
|
+
frozeTheGame = /* @__PURE__ */ new Set();
|
|
648
|
+
/** What was pressed most recently, so a freeze can be attributed to it. */
|
|
649
|
+
lastPressed = null;
|
|
633
650
|
moveAction;
|
|
634
651
|
held = /* @__PURE__ */ new Map();
|
|
635
652
|
/**
|
|
@@ -680,7 +697,20 @@ var Driver = class {
|
|
|
680
697
|
cell(p) {
|
|
681
698
|
return `${Math.round(p[0] / 4)},${Math.round(p[2] / 4)}`;
|
|
682
699
|
}
|
|
700
|
+
/** Actions this run avoided after they stopped the clock. */
|
|
701
|
+
paused() {
|
|
702
|
+
return [...this.frozeTheGame];
|
|
703
|
+
}
|
|
683
704
|
step(dt, at) {
|
|
705
|
+
if (this.engine.timeScale === 0) {
|
|
706
|
+
if (this.lastPressed) this.frozeTheGame.add(this.lastPressed);
|
|
707
|
+
for (const button of this.buttons) {
|
|
708
|
+
this.engine.input.releaseAction(button);
|
|
709
|
+
this.held.set(button, -1);
|
|
710
|
+
}
|
|
711
|
+
this.engine.timeScale = 1;
|
|
712
|
+
this.lastPressed = null;
|
|
713
|
+
}
|
|
684
714
|
this.visits.set(this.cell(at), (this.visits.get(this.cell(at)) ?? 0) + 1);
|
|
685
715
|
this.headingLeft -= dt;
|
|
686
716
|
if (this.headingLeft <= 0) {
|
|
@@ -689,6 +719,7 @@ var Driver = class {
|
|
|
689
719
|
}
|
|
690
720
|
if (this.moveAction) this.engine.input.setActionVector(this.moveAction, Math.cos(this.heading), Math.sin(this.heading));
|
|
691
721
|
for (const button of this.buttons) {
|
|
722
|
+
if (this.frozeTheGame.has(button)) continue;
|
|
692
723
|
const left = (this.held.get(button) ?? 0) - dt;
|
|
693
724
|
if (left > 0) {
|
|
694
725
|
this.held.set(button, left);
|
|
@@ -702,6 +733,7 @@ var Driver = class {
|
|
|
702
733
|
const chance = button === "jump" ? .06 : .02;
|
|
703
734
|
if (this.rand() < chance) {
|
|
704
735
|
this.engine.input.pressAction(button);
|
|
736
|
+
this.lastPressed = button;
|
|
705
737
|
this.held.set(button, .08 + this.rand() * .12);
|
|
706
738
|
}
|
|
707
739
|
}
|
|
@@ -820,9 +852,10 @@ async function runOnce(json, seed, opts) {
|
|
|
820
852
|
});
|
|
821
853
|
let at = start;
|
|
822
854
|
let wentAsFarAs = 0;
|
|
855
|
+
const travelled = (a, b) => scene.dimension === "2d" ? Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[1] ?? 0) - (b[1] ?? 0)) : Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[2] ?? 0) - (b[2] ?? 0));
|
|
823
856
|
while (session.timeMs < deadline) {
|
|
824
857
|
at = player ? worldOf(player) : start;
|
|
825
|
-
wentAsFarAs = Math.max(wentAsFarAs,
|
|
858
|
+
wentAsFarAs = Math.max(wentAsFarAs, travelled(at, start));
|
|
826
859
|
driver.step(stepMs / 1e3, at);
|
|
827
860
|
session.step(stepMs);
|
|
828
861
|
if (player) for (const [path, node] of targetNodes) {
|
|
@@ -878,7 +911,8 @@ async function runOnce(json, seed, opts) {
|
|
|
878
911
|
signals,
|
|
879
912
|
declaresEnd: oracle.declaresEnd(),
|
|
880
913
|
hazards,
|
|
881
|
-
hasPlayer: player !== null
|
|
914
|
+
hasPlayer: player !== null,
|
|
915
|
+
froze: driver.paused()
|
|
882
916
|
};
|
|
883
917
|
}
|
|
884
918
|
/**
|
|
@@ -908,6 +942,7 @@ async function playtest(json, opts = {}) {
|
|
|
908
942
|
let declaresWin = false;
|
|
909
943
|
let hasPlayer = false;
|
|
910
944
|
let hazards = 0;
|
|
945
|
+
const froze = /* @__PURE__ */ new Set();
|
|
911
946
|
for (let i = 0; i < count; i++) {
|
|
912
947
|
const one = await runOnce(json, base + i, opts);
|
|
913
948
|
runs.push(one.run);
|
|
@@ -916,6 +951,7 @@ async function playtest(json, opts = {}) {
|
|
|
916
951
|
declaresWin = declaresWin || one.declaresEnd;
|
|
917
952
|
hasPlayer = hasPlayer || one.hasPlayer;
|
|
918
953
|
hazards = one.hazards;
|
|
954
|
+
for (const action of one.froze) froze.add(action);
|
|
919
955
|
}
|
|
920
956
|
const probe = await createPlaySession(json, {
|
|
921
957
|
seed: base,
|
|
@@ -946,6 +982,7 @@ async function playtest(json, opts = {}) {
|
|
|
946
982
|
declaresWin,
|
|
947
983
|
hasPlayer,
|
|
948
984
|
hazards,
|
|
985
|
+
froze: [...froze],
|
|
949
986
|
seconds: opts.seconds ?? 60,
|
|
950
987
|
feedback: {
|
|
951
988
|
declaredAudio: feedbackNodes.audio,
|
|
@@ -1016,8 +1053,14 @@ function playtestText(report) {
|
|
|
1016
1053
|
if (silent.length > 12) lines.push(` …and ${silent.length - 12} more`);
|
|
1017
1054
|
}
|
|
1018
1055
|
const damage = runs.reduce((n, r) => n + r.damageTaken, 0);
|
|
1056
|
+
const lost = runs.filter((r) => r.outcome === "lost").length;
|
|
1057
|
+
if (report.froze.length > 0) {
|
|
1058
|
+
lines.push("");
|
|
1059
|
+
lines.push(` · the bot stopped pressing ${report.froze.map((a) => `\`${a}\``).join(", ")} — pressing it stopped the clock, and a run spent inside a pause menu measures nothing. Your game is fine; this is the bot getting out of its own way.`);
|
|
1060
|
+
}
|
|
1019
1061
|
lines.push("");
|
|
1020
1062
|
if (damage > 0) lines.push(` danger: ${damage} hits across ${total} runs (${(damage / total).toFixed(1)} per run)`);
|
|
1063
|
+
else if (hazards === 0 && lost > 0) lines.push(` danger: nothing DEALT DAMAGE — no DamageOnContact, no hazard group — and yet ${lost} of ${total} runs were lost. The loss comes from something else: a fall, a timer, a trigger, or running out of lives.`);
|
|
1021
1064
|
else if (hazards === 0) lines.push(" danger: nothing in this scene can hurt the player — no DamageOnContact, no hazard group. A game with no way to lose is a walkabout.");
|
|
1022
1065
|
else lines.push(` danger: ${hazards} hazard(s) are wired and none of them landed a hit in ${total} runs. They may be unreachable — or the bot may simply have outrun them, which a real player would not. Judge it with a scripted run.`);
|
|
1023
1066
|
return lines.join("\n");
|
|
@@ -1812,8 +1855,8 @@ async function playMultiplayer(opts) {
|
|
|
1812
1855
|
*/
|
|
1813
1856
|
async function joinLate(server, first, opts) {
|
|
1814
1857
|
try {
|
|
1815
|
-
const { Engine } = await import("./register-
|
|
1816
|
-
const { loadScene } = await import("./loader-
|
|
1858
|
+
const { Engine } = await import("./register-mmfugpaQ.js").then((n) => n.y);
|
|
1859
|
+
const { loadScene } = await import("./loader-DZE7B07H.js").then((n) => n.r);
|
|
1817
1860
|
const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
|
|
1818
1861
|
const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
|
|
1819
1862
|
engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
|
|
@@ -2175,10 +2218,10 @@ async function runScript(json, opts) {
|
|
|
2175
2218
|
if (opts.locale) engine.locale.locale = opts.locale;
|
|
2176
2219
|
const physics = opts.physics ?? "auto";
|
|
2177
2220
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
2178
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
2221
|
+
const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
|
|
2179
2222
|
await enablePhysics2D(engine);
|
|
2180
2223
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
2181
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
2224
|
+
const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
|
|
2182
2225
|
await enablePhysics3D(engine);
|
|
2183
2226
|
}
|
|
2184
2227
|
const failures = [];
|
|
@@ -2294,10 +2337,10 @@ async function createPlaySession(json, opts = {}) {
|
|
|
2294
2337
|
engine.setScene(scene);
|
|
2295
2338
|
const physics = opts.physics ?? "auto";
|
|
2296
2339
|
if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
|
|
2297
|
-
const { enablePhysics2D } = await import("./physics-2d-
|
|
2340
|
+
const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
|
|
2298
2341
|
await enablePhysics2D(engine);
|
|
2299
2342
|
} else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
|
|
2300
|
-
const { enablePhysics3D } = await import("./physics-3d-
|
|
2343
|
+
const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
|
|
2301
2344
|
await enablePhysics3D(engine);
|
|
2302
2345
|
}
|
|
2303
2346
|
const stepMs = 1e3 / (opts.fixedHz ?? 60);
|
|
@@ -2317,7 +2360,7 @@ async function createPlaySession(json, opts = {}) {
|
|
|
2317
2360
|
timeMs += ticks * stepMs;
|
|
2318
2361
|
},
|
|
2319
2362
|
capture() {
|
|
2320
|
-
return scene.toJSON();
|
|
2363
|
+
return (engine.scene ?? scene).toJSON();
|
|
2321
2364
|
},
|
|
2322
2365
|
describe() {
|
|
2323
2366
|
return describeCapture(captureScene(engine.scene ?? scene));
|
package/dist/test.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-
|
|
1
|
+
import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-rm8vVzu_.js";
|
|
2
2
|
import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
-
import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-
|
|
5
|
-
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-
|
|
3
|
+
import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
|
|
4
|
+
import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-BQfkX4x7.js";
|
|
5
|
+
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-Cg76MCV5.js";
|
|
6
6
|
|
|
7
7
|
//#region src/test/facing.d.ts
|
|
8
8
|
interface FacingOptions {
|
|
@@ -361,6 +361,17 @@ interface PlaytestReport {
|
|
|
361
361
|
/** How many nodes are wired to hurt the player — see the `danger:` line. */
|
|
362
362
|
hazards: number;
|
|
363
363
|
/**
|
|
364
|
+
* Actions the bot stopped pressing because they STOPPED THE CLOCK.
|
|
365
|
+
*
|
|
366
|
+
* It drove every declared button, and `incanto-your-first-game.md` tells you
|
|
367
|
+
* to declare `pause`. Measured on one losable scene: 0/10 losses with `pause`
|
|
368
|
+
* declared, 10/10 without, everything else identical — a frozen game cannot
|
|
369
|
+
* hurt you, so the same run also reported the game as having no way to lose.
|
|
370
|
+
* Named here rather than guessed from the word "pause": a game may call it
|
|
371
|
+
* `menu` or `escape`, and the bot notices the way a player would.
|
|
372
|
+
*/
|
|
373
|
+
froze: string[];
|
|
374
|
+
/**
|
|
364
375
|
* Did `findPlayer` find anyone to drive?
|
|
365
376
|
*
|
|
366
377
|
* `findPlayer`'s own comment says a scene it cannot drive "says so rather
|
package/dist/test.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as instancedScenes, r as auditScene } from "./replay-
|
|
2
|
-
import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-
|
|
1
|
+
import { i as instancedScenes, r as auditScene } from "./replay-BEHLDcQV.js";
|
|
2
|
+
import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-D6ISmuFE.js";
|
|
3
3
|
export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
|