incanto 0.69.0 → 0.70.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 +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +125 -44
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +281 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +7 -7
- package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-DaMxqfNE.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-DZExDn9o.d.ts} +744 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-Bwvh6q8A.js} +142 -59
- package/dist/{create-game-CvJ1wSRP.js → create-game-C7ffQWW7.js} +97 -47
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1065 -863
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-D6Q5BxeE.js} +298 -45
- package/dist/{gameplay-D1KixWK8.js → gameplay-BfHkuzVb.js} +809 -211
- package/dist/gameplay.d.ts +187 -7
- package/dist/gameplay.js +1 -1
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +156 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-Cff09LMm.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-CE0Qvy3V.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CpH-2gn5.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-CQJ_PJKh.js} +103 -12
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-mmfugpaQ.js → register-6DYnKZcy.js} +546 -817
- package/dist/{register-DbVArsns.js → register-Bkk0wSDB.js} +332 -23
- package/dist/{replay-BEHLDcQV.js → replay-DjAkAzMq.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-BXVg148r.js} +4570 -2322
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-PL78oVXP.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-Cxfiv1Hg.js} +2 -17
- package/dist/test-iHYVUcDK.js +4036 -0
- package/dist/test.d.ts +531 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +176 -6
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-D0MS174y.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-BnXkKuYu.js} +2 -2
- package/editor/assets/index-CIu3uc3l.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +30 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +83 -6
- package/skills/incanto-building-2d-games.md +77 -8
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +109 -19
- package/skills/incanto-hud.md +128 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +222 -58
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +78 -2
- package/skills/incanto-save-slots.md +161 -6
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +196 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1255 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +128 -600
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +41 -195
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +78 -217
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +199 -838
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.js +0 -10958
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { K as Node, M as effectiveOrder, q as diagnose } from "./save-slots-BXVg148r.js";
|
|
2
|
+
import { M as translateOn, t as registerCoreNodes } from "./register-6DYnKZcy.js";
|
|
3
3
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
4
|
-
import {
|
|
5
|
-
import { i as
|
|
6
|
-
import {
|
|
7
|
-
import { F as localFromWorld, I as worldPosition } from "./gameplay-D1KixWK8.js";
|
|
4
|
+
import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
|
|
5
|
+
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CqR2o3SA.js";
|
|
6
|
+
import { E as effectRng, X as localFromWorld, Z as worldPosition, w as tolerateUnknownAction } from "./gameplay-BfHkuzVb.js";
|
|
8
7
|
import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
|
|
9
8
|
import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
|
|
10
9
|
//#region src/2d/physics/collider-validate.ts
|
|
@@ -14,8 +13,31 @@ import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color
|
|
|
14
13
|
* not minutes later when physics starts. An EMPTY collider is fine: a body
|
|
15
14
|
* without a collider is a work-in-progress node, not an error.
|
|
16
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Every key a 2D collider may carry.
|
|
18
|
+
*
|
|
19
|
+
* The shape was checked thoroughly and everything else let through, so
|
|
20
|
+
* `{"shape": "circle", "radius": 8, "offest": [0, -24]}` loaded clean with its
|
|
21
|
+
* hitbox at the body's origin — the exact defect `verify:playable` was written
|
|
22
|
+
* for after it found hitboxes 155 px behind their own bodies in a starter.
|
|
23
|
+
*/
|
|
24
|
+
const COLLIDER_KEYS_2D = [
|
|
25
|
+
"shape",
|
|
26
|
+
"size",
|
|
27
|
+
"radius",
|
|
28
|
+
"height",
|
|
29
|
+
"offset",
|
|
30
|
+
"oneWay"
|
|
31
|
+
];
|
|
17
32
|
function validateCollider2D(collider, nodeName) {
|
|
18
33
|
const shape = collider.shape;
|
|
34
|
+
for (const key of Object.keys(collider)) {
|
|
35
|
+
if (COLLIDER_KEYS_2D.includes(key)) continue;
|
|
36
|
+
throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}' has an unknown key "${key}".` + didYouMean(key, COLLIDER_KEYS_2D) + ` A collider is [${COLLIDER_KEYS_2D.join(", ")}] — friction, restitution and mass are props of the BODY, not of its collider.`, {
|
|
37
|
+
prop: key,
|
|
38
|
+
validOptions: [...COLLIDER_KEYS_2D]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
19
41
|
if (shape === void 0) return;
|
|
20
42
|
if (collider.oneWay !== void 0 && typeof collider.oneWay !== "boolean") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "oneWay" must be a boolean (platforms you jump up through), got ${JSON.stringify(collider.oneWay)}.`);
|
|
21
43
|
if (shape === "rect") {
|
|
@@ -227,7 +249,7 @@ var Area2D = class extends PhysicsBody2D {
|
|
|
227
249
|
* Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
|
|
228
250
|
* See `Area3D.overlapping`. Empty in a scene with no physics world.
|
|
229
251
|
*/
|
|
230
|
-
overlapping(group) {
|
|
252
|
+
overlapping(group = void 0) {
|
|
231
253
|
const all = this._physics?.overlapping(this) ?? [];
|
|
232
254
|
return group === void 0 ? all : all.filter((n) => n.isInGroup(group));
|
|
233
255
|
}
|
|
@@ -241,7 +263,8 @@ var RigidBody2D = class extends PhysicsBody2D {
|
|
|
241
263
|
fixedRotation: { default: false },
|
|
242
264
|
friction: { default: .5 },
|
|
243
265
|
restitution: { default: 0 },
|
|
244
|
-
linearVelocity: { default: [0, 0] }
|
|
266
|
+
linearVelocity: { default: [0, 0] },
|
|
267
|
+
angularVelocity: { default: 0 }
|
|
245
268
|
};
|
|
246
269
|
mass = 1;
|
|
247
270
|
gravityScale = 1;
|
|
@@ -250,6 +273,44 @@ var RigidBody2D = class extends PhysicsBody2D {
|
|
|
250
273
|
restitution = 0;
|
|
251
274
|
/** px/s, y-down. Read back every step; write to launch. */
|
|
252
275
|
linearVelocity = [0, 0];
|
|
276
|
+
/**
|
|
277
|
+
* rad/s about the z axis — the same sign as `rotation`, which is CLOCKWISE
|
|
278
|
+
* in this y-down space. Read back every step; write to spin.
|
|
279
|
+
*
|
|
280
|
+
* `RigidBody3D` got its `angularVelocity` because "a puzzle builder could
|
|
281
|
+
* neither launch a spinning body nor read how fast a lever was swinging —
|
|
282
|
+
* only its angle, differenced by hand". Same sentence, same defect, other
|
|
283
|
+
* adapter: 2D had `linearVelocity` and no counterpart. `fixedRotation` pins
|
|
284
|
+
* it at zero.
|
|
285
|
+
*/
|
|
286
|
+
angularVelocity = 0;
|
|
287
|
+
/**
|
|
288
|
+
* The other name for `linearVelocity`, because it is the name every skill,
|
|
289
|
+
* every sibling node and every other engine uses.
|
|
290
|
+
*
|
|
291
|
+
* `body.velocity = [0, -400]` is what `incanto-physics-and-input.md` taught
|
|
292
|
+
* for years. On a plain TS class that assignment SUCCEEDS — it just writes a
|
|
293
|
+
* stray field nothing reads — so a jump that never happened produced no
|
|
294
|
+
* error, no warning and nothing to search for. `CharacterBody2D.velocity`
|
|
295
|
+
* next to `RigidBody2D.linearVelocity` is the kind of near-miss a builder
|
|
296
|
+
* cannot be expected to notice.
|
|
297
|
+
*
|
|
298
|
+
* Scene JSON still names the prop `linearVelocity` and rejects `velocity`
|
|
299
|
+
* loudly, with the valid keys listed — a hard error corrects itself.
|
|
300
|
+
*/
|
|
301
|
+
get velocity() {
|
|
302
|
+
return this.linearVelocity;
|
|
303
|
+
}
|
|
304
|
+
set velocity(v) {
|
|
305
|
+
this.linearVelocity = v;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Impulse in px·kg/s (y-down) — the 3D twin has had this since it shipped,
|
|
309
|
+
* and calling it here was a TypeError.
|
|
310
|
+
*/
|
|
311
|
+
applyImpulse(impulse) {
|
|
312
|
+
this._physics?.applyImpulse(this, impulse);
|
|
313
|
+
}
|
|
253
314
|
};
|
|
254
315
|
/**
|
|
255
316
|
* Kinematic body on Rapier's character controller. Gravity is NOT applied
|
|
@@ -261,7 +322,11 @@ var CharacterBody2D = class extends PhysicsBody2D {
|
|
|
261
322
|
static props = {
|
|
262
323
|
velocity: { default: [0, 0] },
|
|
263
324
|
stickToGround: { default: true },
|
|
264
|
-
slopeLimitDeg: { default: 45 }
|
|
325
|
+
slopeLimitDeg: { default: 45 },
|
|
326
|
+
stepHeight: {
|
|
327
|
+
default: 35,
|
|
328
|
+
range: { min: 0 }
|
|
329
|
+
}
|
|
265
330
|
};
|
|
266
331
|
/**
|
|
267
332
|
* Renamed to match the 3D body, where `snapToGround` had to give the name
|
|
@@ -275,6 +340,17 @@ var CharacterBody2D = class extends PhysicsBody2D {
|
|
|
275
340
|
stickToGround = true;
|
|
276
341
|
/** Max climbable slope angle. */
|
|
277
342
|
slopeLimitDeg = 45;
|
|
343
|
+
/**
|
|
344
|
+
* How high a step this body walks up without jumping, in PIXELS.
|
|
345
|
+
*
|
|
346
|
+
* Rapier's character controller does not autostep unless it is asked, and in
|
|
347
|
+
* 2D nothing asked: a ledge of ANY height — measured, ONE pixel — stopped a
|
|
348
|
+
* walking body dead, while the mirrored `CharacterBody3D` has cleared 0.35 m
|
|
349
|
+
* by default since 0.63. The default here is that same 0.35 m at the 2D
|
|
350
|
+
* world's own scale (100 px = 1 m, which is why gravity defaults to 980).
|
|
351
|
+
* `0` restores the old behaviour.
|
|
352
|
+
*/
|
|
353
|
+
stepHeight = 35;
|
|
278
354
|
/** @internal Updated by Physics2D.moveAndSlide. */
|
|
279
355
|
_grounded = false;
|
|
280
356
|
moveAndSlide() {
|
|
@@ -387,6 +463,10 @@ var Joint2D = class extends Node2D {
|
|
|
387
463
|
const a = this.parent;
|
|
388
464
|
const b = this.getNodeOrNull(this.target);
|
|
389
465
|
if (!(b instanceof PhysicsBody2D)) throw new IncantoError("NODE_NOT_FOUND", `Joint2D '${this.name}': target '${this.target}' is not a physics body.`);
|
|
466
|
+
if (a === b) throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}': target '${this.target}' is the SAME body this joint hangs off, so it constrains nothing. A joint is a child of ONE body pointing at the OTHER — name the other body (e.g. "%Crate").`, {
|
|
467
|
+
prop: "target",
|
|
468
|
+
path: this.getPath()
|
|
469
|
+
});
|
|
390
470
|
return {
|
|
391
471
|
a,
|
|
392
472
|
b
|
|
@@ -415,7 +495,13 @@ var Sprite2D = class extends Node2D {
|
|
|
415
495
|
flipX: { default: false },
|
|
416
496
|
flipY: { default: false },
|
|
417
497
|
tint: { default: "#ffffff" },
|
|
418
|
-
opacity: {
|
|
498
|
+
opacity: {
|
|
499
|
+
default: 1,
|
|
500
|
+
range: {
|
|
501
|
+
min: 0,
|
|
502
|
+
max: 1
|
|
503
|
+
}
|
|
504
|
+
}
|
|
419
505
|
};
|
|
420
506
|
/** `'$assetKey'` texture reference. Empty = hidden. */
|
|
421
507
|
texture = "";
|
|
@@ -479,6 +565,22 @@ var Sprite2D = class extends Node2D {
|
|
|
479
565
|
mat.color.set(this.tint);
|
|
480
566
|
mat.opacity = this.opacity;
|
|
481
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* The material is PER NODE and nothing freed it.
|
|
570
|
+
*
|
|
571
|
+
* The geometry is shared for the process (`UNIT_PLANE`, on purpose), but
|
|
572
|
+
* every sprite makes its own `MeshBasicMaterial` — and a 2D game that spawns
|
|
573
|
+
* and frees sprites, which is every 2D game, leaked one per node forever.
|
|
574
|
+
* The TEXTURE is not touched: it belongs to the asset store and is shared by
|
|
575
|
+
* every node that names the same key.
|
|
576
|
+
*
|
|
577
|
+
* `AnimatedSprite2D` extends this, so it is covered by the same override.
|
|
578
|
+
*/
|
|
579
|
+
free() {
|
|
580
|
+
(this.quadMesh?.material)?.dispose();
|
|
581
|
+
this.quadMesh = null;
|
|
582
|
+
super.free();
|
|
583
|
+
}
|
|
482
584
|
};
|
|
483
585
|
//#endregion
|
|
484
586
|
//#region src/2d/nodes/animated-sprite-2d.ts
|
|
@@ -534,7 +636,7 @@ var AnimatedSprite2D = class extends Sprite2D {
|
|
|
534
636
|
this.playing = false;
|
|
535
637
|
}
|
|
536
638
|
onReady() {
|
|
537
|
-
|
|
639
|
+
validateAnimations(this.animations, this.name);
|
|
538
640
|
if (this.autoplay) this.play(this.autoplay);
|
|
539
641
|
}
|
|
540
642
|
update(dt) {
|
|
@@ -635,10 +737,19 @@ var Camera2D = class extends Node2D {
|
|
|
635
737
|
get effectiveZoom() {
|
|
636
738
|
return Math.max(.01, this.zoom);
|
|
637
739
|
}
|
|
740
|
+
/** The `follow` path already reported as untrackable — cleared by a new one. */
|
|
741
|
+
warnedFollow = "";
|
|
638
742
|
update(dt) {
|
|
639
743
|
if (!this.follow) return;
|
|
640
744
|
const target = this.getNodeOrNull(this.follow);
|
|
641
|
-
if (!(target instanceof Node2D))
|
|
745
|
+
if (!(target instanceof Node2D)) {
|
|
746
|
+
if (this.warnedFollow !== this.follow) {
|
|
747
|
+
this.warnedFollow = this.follow;
|
|
748
|
+
const what = target ? `a ${target.constructor.typeName ?? "Node"}` : "nothing";
|
|
749
|
+
diagnose(this.tree?.engine ?? null, "warn", `[incanto] ${this.getPath()}: follow '${this.follow}' is ${what}, which has no position for the camera to track — the view will not move. Point it at a Node2D.`);
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
642
753
|
const [tx = 0, ty = 0] = worldPosition(target);
|
|
643
754
|
const [px = 0, py = 0] = worldPosition(this);
|
|
644
755
|
const s = Math.min(Math.max(this.smoothing, 0), .99);
|
|
@@ -709,6 +820,12 @@ function clampCentered(v, lo, hi) {
|
|
|
709
820
|
* Every one defaults to OFF (0 / 1 / false), so an existing scene behaves
|
|
710
821
|
* exactly as before until it asks for something.
|
|
711
822
|
*/
|
|
823
|
+
/** What each action prop is called when nobody names it. */
|
|
824
|
+
const DEFAULT_ACTIONS = {
|
|
825
|
+
moveAction: "move",
|
|
826
|
+
jumpAction: "jump",
|
|
827
|
+
dashAction: "dash"
|
|
828
|
+
};
|
|
712
829
|
var CharacterController2D = class extends Node {
|
|
713
830
|
static typeName = "CharacterController2D";
|
|
714
831
|
static props = {
|
|
@@ -771,7 +888,10 @@ var CharacterController2D = class extends Node {
|
|
|
771
888
|
const body = this.parent;
|
|
772
889
|
if (!(body instanceof CharacterBody2D)) return;
|
|
773
890
|
const input = engine.input;
|
|
774
|
-
const dir = input.getVector(this.moveAction)
|
|
891
|
+
const dir = this.readAction("moveAction", () => input.getVector(this.moveAction), {
|
|
892
|
+
x: 0,
|
|
893
|
+
y: 0
|
|
894
|
+
}, "the character will not move");
|
|
775
895
|
if (this.mode === "topDown") {
|
|
776
896
|
body.velocity = [dir.x * this.maxSpeed, dir.y * this.maxSpeed];
|
|
777
897
|
body.moveAndSlide();
|
|
@@ -788,7 +908,7 @@ var CharacterController2D = class extends Node {
|
|
|
788
908
|
this.setState("dash");
|
|
789
909
|
return;
|
|
790
910
|
}
|
|
791
|
-
if (this.dashSpeed > 0 && input.justPressed(this.dashAction)) {
|
|
911
|
+
if (this.dashSpeed > 0 && this.readAction("dashAction", () => input.justPressed(this.dashAction), false, "the character will never dash")) {
|
|
792
912
|
this.dashLeft = this.dashSeconds;
|
|
793
913
|
this.dashDir = dir.x !== 0 ? Math.sign(dir.x) : this.dashDir;
|
|
794
914
|
body.velocity[0] = this.dashDir * this.dashSpeed;
|
|
@@ -805,9 +925,10 @@ var CharacterController2D = class extends Node {
|
|
|
805
925
|
this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
|
|
806
926
|
if (this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
|
|
807
927
|
}
|
|
808
|
-
|
|
928
|
+
const jumpEdge = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "the character will never jump");
|
|
929
|
+
if (jumpEdge) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
|
|
809
930
|
else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
|
|
810
|
-
const wantsJump = this.bufferLeft > 0 ||
|
|
931
|
+
const wantsJump = this.bufferLeft > 0 || jumpEdge;
|
|
811
932
|
const onWall = this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0) ? body.isOnWall() : false;
|
|
812
933
|
if (wantsJump && onWall && !grounded && this.wallJumpImpulse.some((v) => v !== 0)) {
|
|
813
934
|
const away = -body.wallSide();
|
|
@@ -827,7 +948,8 @@ var CharacterController2D = class extends Node {
|
|
|
827
948
|
this.rising = false;
|
|
828
949
|
} else {
|
|
829
950
|
body.velocity[1] = (body.velocity[1] ?? 0) + gravityY * dt;
|
|
830
|
-
|
|
951
|
+
const jumpHeld = this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "the character will never jump");
|
|
952
|
+
if (this.rising && this.jumpCutMultiplier < 1 && !jumpHeld) {
|
|
831
953
|
body.velocity[1] = (body.velocity[1] ?? 0) * this.jumpCutMultiplier;
|
|
832
954
|
this.rising = false;
|
|
833
955
|
}
|
|
@@ -854,6 +976,38 @@ var CharacterController2D = class extends Node {
|
|
|
854
976
|
this.state = next;
|
|
855
977
|
this.emit("movementStateChanged", next);
|
|
856
978
|
}
|
|
979
|
+
/** Actions already reported missing, so a per-frame read says it once. */
|
|
980
|
+
reportedActions = /* @__PURE__ */ new Set();
|
|
981
|
+
/**
|
|
982
|
+
* Read an input action this controller is allowed to be missing.
|
|
983
|
+
*
|
|
984
|
+
* The 3D controller has done this since `optional-action.ts` was written —
|
|
985
|
+
* whose own docstring says "`CharacterController3D` and `GameFlow` wrap every
|
|
986
|
+
* input read" — and the 2D one did not. So a top-down 2D game, which is the
|
|
987
|
+
* first thing anyone builds with this node and has no jump key, declared
|
|
988
|
+
* `move` and got:
|
|
989
|
+
*
|
|
990
|
+
* ```
|
|
991
|
+
* [incanto] /Game/Player/Ctl (CharacterController2D) threw in fixedUpdate —
|
|
992
|
+
* this node is now SKIPPED. IncantoError: Unknown input action 'jump'.
|
|
993
|
+
* Declared actions: [move].
|
|
994
|
+
* ```
|
|
995
|
+
*
|
|
996
|
+
* SKIPPED: the character could not move either, and every tool reported the
|
|
997
|
+
* game as `✗ error in 1/1` with the reason only in the log. Tolerated because
|
|
998
|
+
* a game with no jump key is the ordinary case; reported because otherwise a
|
|
999
|
+
* TYPO in `"jumpAction"` looks exactly like a deliberate omission.
|
|
1000
|
+
*/
|
|
1001
|
+
readAction(prop, read, fallback, consequence) {
|
|
1002
|
+
const action = this[prop];
|
|
1003
|
+
return tolerateUnknownAction({
|
|
1004
|
+
engine: this.tree?.engine,
|
|
1005
|
+
who: this.getPath(),
|
|
1006
|
+
reported: this.reportedActions,
|
|
1007
|
+
consequence,
|
|
1008
|
+
authored: action !== DEFAULT_ACTIONS[prop]
|
|
1009
|
+
}, action, read, fallback);
|
|
1010
|
+
}
|
|
857
1011
|
};
|
|
858
1012
|
//#endregion
|
|
859
1013
|
//#region src/2d/nodes/color-rect-2d.ts
|
|
@@ -869,7 +1023,13 @@ var ColorRect2D = class extends Node2D {
|
|
|
869
1023
|
static props = {
|
|
870
1024
|
size: { default: [100, 100] },
|
|
871
1025
|
color: { default: "#ffffff" },
|
|
872
|
-
opacity: {
|
|
1026
|
+
opacity: {
|
|
1027
|
+
default: 1,
|
|
1028
|
+
range: {
|
|
1029
|
+
min: 0,
|
|
1030
|
+
max: 1
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
873
1033
|
anchor: { default: [.5, .5] }
|
|
874
1034
|
};
|
|
875
1035
|
/** [width, height] in world px. */
|
|
@@ -905,6 +1065,24 @@ var ColorRect2D = class extends Node2D {
|
|
|
905
1065
|
mat.color.set(this.color);
|
|
906
1066
|
mat.opacity = this.opacity;
|
|
907
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The material is PER NODE and nothing freed it.
|
|
1070
|
+
*
|
|
1071
|
+
* The geometry is shared for the process (`UNIT_PLANE`, on purpose — a
|
|
1072
|
+
* `THREE.Sprite` cannot be instanced and this is the discipline that replaces
|
|
1073
|
+
* it), but every rect makes its own `MeshBasicMaterial`, and a 2D game that
|
|
1074
|
+
* spawns and frees — bullets, coins, damage flashes, a level swap — leaks one
|
|
1075
|
+
* GPU program's worth of state per node, forever.
|
|
1076
|
+
*
|
|
1077
|
+
* Four 3D nodes had exactly this and were fixed. These two 2D siblings make a
|
|
1078
|
+
* material on the same line of the same kind of `_quad()` helper, and were
|
|
1079
|
+
* not on that list.
|
|
1080
|
+
*/
|
|
1081
|
+
free() {
|
|
1082
|
+
(this.quadMesh?.material)?.dispose();
|
|
1083
|
+
this.quadMesh = null;
|
|
1084
|
+
super.free();
|
|
1085
|
+
}
|
|
908
1086
|
};
|
|
909
1087
|
//#endregion
|
|
910
1088
|
//#region src/2d/nodes/label.ts
|
|
@@ -1074,8 +1252,20 @@ var Particles2D = class Particles2D extends Node2D {
|
|
|
1074
1252
|
colorStart: { default: "#ffffff" },
|
|
1075
1253
|
colorEnd: { default: "#ffffff" },
|
|
1076
1254
|
paletteColors: { default: [] },
|
|
1077
|
-
alphaStart: {
|
|
1078
|
-
|
|
1255
|
+
alphaStart: {
|
|
1256
|
+
default: 1,
|
|
1257
|
+
range: {
|
|
1258
|
+
min: 0,
|
|
1259
|
+
max: 1
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
alphaEnd: {
|
|
1263
|
+
default: 0,
|
|
1264
|
+
range: {
|
|
1265
|
+
min: 0,
|
|
1266
|
+
max: 1
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1079
1269
|
shimmer: { default: 0 },
|
|
1080
1270
|
blend: {
|
|
1081
1271
|
default: "add",
|
|
@@ -1138,7 +1328,7 @@ var Particles2D = class Particles2D extends Node2D {
|
|
|
1138
1328
|
_ensureSim() {
|
|
1139
1329
|
if (!this.sim) {
|
|
1140
1330
|
applyParticlePreset(this, this.preset, getNodeSchema(Particles2D.typeName));
|
|
1141
|
-
const rng = this
|
|
1331
|
+
const rng = effectRng(this);
|
|
1142
1332
|
this.sim = new ParticleSim({
|
|
1143
1333
|
rate: this.emitting ? this.rate : 0,
|
|
1144
1334
|
lifetime: [this.lifetime[0] ?? 1, this.lifetime[1] ?? 1],
|
|
@@ -1244,6 +1434,7 @@ var Particles2D = class Particles2D extends Node2D {
|
|
|
1244
1434
|
kind,
|
|
1245
1435
|
name: this.preset,
|
|
1246
1436
|
from: this.getPath(),
|
|
1437
|
+
wiredAs: this.wiringPath(),
|
|
1247
1438
|
amount,
|
|
1248
1439
|
at: engine.time
|
|
1249
1440
|
});
|
|
@@ -1343,6 +1534,37 @@ const colorScratchC = new Color();
|
|
|
1343
1534
|
* - number rows: passed through (`-1` = empty).
|
|
1344
1535
|
* Ragged rows are right-padded with empty.
|
|
1345
1536
|
*/
|
|
1537
|
+
/**
|
|
1538
|
+
* A legend entry is a tile INDEX, and anything else is a level with no floor.
|
|
1539
|
+
*
|
|
1540
|
+
* `legend` maps a char to a number; solidity is the SEPARATE `solid` prop, a
|
|
1541
|
+
* list of indices. Every other tilemap format in the world puts them together,
|
|
1542
|
+
* so the natural guess is
|
|
1543
|
+
*
|
|
1544
|
+
* ```jsonc
|
|
1545
|
+
* "legend": { "#": { "solid": true, "color": "#4a5568" } } // WRONG, and silent
|
|
1546
|
+
* ```
|
|
1547
|
+
*
|
|
1548
|
+
* and it loaded clean: `legend[ch]` was not `undefined`, so the object went
|
|
1549
|
+
* straight into a `number[]`. `solid.includes(thatObject)` is false, so the
|
|
1550
|
+
* ground had no colliders — measured on a scene authored exactly that way, two
|
|
1551
|
+
* crates fell from y=120 to y=1859 through a map that `incanto-check` had just
|
|
1552
|
+
* called valid.
|
|
1553
|
+
*
|
|
1554
|
+
* An open dictionary beside a closed one: the loader validates the PROP names
|
|
1555
|
+
* hard and never looked inside this one.
|
|
1556
|
+
*/
|
|
1557
|
+
function tileIndex(ch, value) {
|
|
1558
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= -1) return value;
|
|
1559
|
+
throw new IncantoError("PROP_TYPE_MISMATCH", `TileMap2D: legend entry '${ch}' must be a tile INDEX (a whole number, -1 for empty), got ${describeLegendValue(value)}. Solidity is the separate "solid" prop — a list of tile indices: "legend": { "${ch}": 0 }, "solid": [0].`, { prop: "legend" });
|
|
1560
|
+
}
|
|
1561
|
+
function describeLegendValue(value) {
|
|
1562
|
+
if (value === null) return "null";
|
|
1563
|
+
if (Array.isArray(value)) return `an array of ${value.length}`;
|
|
1564
|
+
if (typeof value === "object") return `an object (keys: ${Object.keys(value).join(", ")})`;
|
|
1565
|
+
if (typeof value === "number") return `${value}`;
|
|
1566
|
+
return `a ${typeof value}`;
|
|
1567
|
+
}
|
|
1346
1568
|
function parseCells(cells, legend) {
|
|
1347
1569
|
const rows = [];
|
|
1348
1570
|
let width = 0;
|
|
@@ -1350,7 +1572,7 @@ function parseCells(cells, legend) {
|
|
|
1350
1572
|
const out = [];
|
|
1351
1573
|
if (typeof row === "string") for (const ch of row) {
|
|
1352
1574
|
const mapped = legend[ch];
|
|
1353
|
-
if (mapped !== void 0) out.push(mapped);
|
|
1575
|
+
if (mapped !== void 0) out.push(tileIndex(ch, mapped));
|
|
1354
1576
|
else if (ch === "." || ch === " ") out.push(-1);
|
|
1355
1577
|
else if (ch >= "0" && ch <= "9") out.push(ch.charCodeAt(0) - 48);
|
|
1356
1578
|
else throw new IncantoError("BAD_FORMAT", `TileMap2D: unknown tile char '${ch}' — add it to "legend" (char → tile index), or use '.'/' ' for empty and digits 0-9.`);
|
|
@@ -1433,7 +1655,13 @@ var TileMap2D = class extends Node2D {
|
|
|
1433
1655
|
legend: { default: {} },
|
|
1434
1656
|
/** Tile indices that get merged static colliders. */
|
|
1435
1657
|
solid: { default: [] },
|
|
1436
|
-
opacity: {
|
|
1658
|
+
opacity: {
|
|
1659
|
+
default: 1,
|
|
1660
|
+
range: {
|
|
1661
|
+
min: 0,
|
|
1662
|
+
max: 1
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1437
1665
|
};
|
|
1438
1666
|
texture = "";
|
|
1439
1667
|
tileSize = 32;
|
|
@@ -1485,6 +1713,87 @@ var TileMap2D = class extends Node2D {
|
|
|
1485
1713
|
grid() {
|
|
1486
1714
|
return parseCells(this._cells, this._legend);
|
|
1487
1715
|
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Which CELL is under this world point — `null` outside the grid.
|
|
1718
|
+
*
|
|
1719
|
+
* The one question a tile game asks constantly and this node could not
|
|
1720
|
+
* answer. `VoxelGrid3D`, the 3D grid, has had `tileAt` since it was written;
|
|
1721
|
+
* the 2D one shipped `grid()` and nothing else, so every mouse-driven tile
|
|
1722
|
+
* game hand-rolled the same arithmetic with the same two traps in it: cell
|
|
1723
|
+
* (0,0) hangs its TOP-LEFT on the origin (so a cell's CENTRE is half a tile
|
|
1724
|
+
* further on), and the origin is the node's WORLD position, not its `position`
|
|
1725
|
+
* prop.
|
|
1726
|
+
*
|
|
1727
|
+
* Composes ancestor POSITIONS, like every other world query in the engine
|
|
1728
|
+
* (`Terrain3D.heightAt`, `Water3D`); `auditScene` reports an ancestor that
|
|
1729
|
+
* scales or rotates one of these, because that moves the picture and not the
|
|
1730
|
+
* answer.
|
|
1731
|
+
*/
|
|
1732
|
+
cellAt(worldX, worldY) {
|
|
1733
|
+
const at = worldPosition(this);
|
|
1734
|
+
const [tw, th] = this.scaledTile();
|
|
1735
|
+
if (!(tw > 0) || !(th > 0)) return null;
|
|
1736
|
+
const cx = Math.floor((worldX - (at[0] ?? 0)) / tw);
|
|
1737
|
+
const cy = Math.floor((worldY - (at[1] ?? 0)) / th);
|
|
1738
|
+
const row = this.grid()[cy];
|
|
1739
|
+
if (!row || cx < 0 || cx >= row.length) return null;
|
|
1740
|
+
return [cx, cy];
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* One cell's size in WORLD pixels — `tileSize` times this node's own scale.
|
|
1744
|
+
*
|
|
1745
|
+
* `scale` is an ordinary `Node2D` prop, the renderer scales the mesh by it,
|
|
1746
|
+
* and `rebuildColliders` has always rebuilt when it changes — so at
|
|
1747
|
+
* `scale: [2, 2]` a tile is 64 px wide on screen AND in physics. These
|
|
1748
|
+
* queries divided by 32 and answered the wrong cell, in silence, which is the
|
|
1749
|
+
* picture and the collider agreeing and the decider disagreeing with both.
|
|
1750
|
+
*
|
|
1751
|
+
* Ancestor scale is not composed here, for the same reason `worldPosition`
|
|
1752
|
+
* does not: every world query in the engine reads its own props and composes
|
|
1753
|
+
* ancestor POSITIONS, and `auditScene` reports an ancestor that scales one.
|
|
1754
|
+
*/
|
|
1755
|
+
scaledTile() {
|
|
1756
|
+
const [sx = 1, sy = 1] = this.scale;
|
|
1757
|
+
return [this.tileSize * Math.abs(sx), this.tileSize * Math.abs(sy)];
|
|
1758
|
+
}
|
|
1759
|
+
/** The atlas index at a cell — `-1` for an empty cell or one off the grid. */
|
|
1760
|
+
tileAt(cx, cy) {
|
|
1761
|
+
return this.grid()[cy]?.[cx] ?? -1;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* The CENTRE of a cell, in world pixels — for putting a highlight, a marker
|
|
1765
|
+
* or a spawned enemy on a tile.
|
|
1766
|
+
*
|
|
1767
|
+
* The centre rather than the corner, because that is where a thing goes and
|
|
1768
|
+
* because the corner is the half-tile every hand-rolled version forgets.
|
|
1769
|
+
*/
|
|
1770
|
+
worldAt(cx, cy) {
|
|
1771
|
+
const at = worldPosition(this);
|
|
1772
|
+
const [tw, th] = this.scaledTile();
|
|
1773
|
+
return [(at[0] ?? 0) + cx * tw + tw / 2, (at[1] ?? 0) + cy * th + th / 2];
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Write ONE cell, by the character the row holds — dig a tile out, drop a
|
|
1777
|
+
* wall in, flip a switch.
|
|
1778
|
+
*
|
|
1779
|
+
* `cells` are strings and a mutation is not watched, so the documented way to
|
|
1780
|
+
* change a map was to rebuild the whole row by hand
|
|
1781
|
+
* (`row.slice(0, x) + c + row.slice(x + 1)`) and reassign the array. This is
|
|
1782
|
+
* that, once, correctly, and it rebuilds the geometry and the colliders.
|
|
1783
|
+
*
|
|
1784
|
+
* A CHARACTER, not an index: `legend` may map several characters to one tile,
|
|
1785
|
+
* so an index cannot say which of them you meant. `'.'` empties the cell.
|
|
1786
|
+
* Returns false for a cell off the grid, or a numeric row (those hold indices
|
|
1787
|
+
* — assign the row).
|
|
1788
|
+
*/
|
|
1789
|
+
setTile(cx, cy, char) {
|
|
1790
|
+
const row = this._cells[cy];
|
|
1791
|
+
if (typeof row !== "string" || cx < 0 || cx >= row.length || char.length !== 1) return false;
|
|
1792
|
+
const next = this._cells.slice();
|
|
1793
|
+
next[cy] = `${row.slice(0, cx)}${char}${row.slice(cx + 1)}`;
|
|
1794
|
+
this.cells = next;
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1488
1797
|
onReady() {
|
|
1489
1798
|
this.rebuildColliders();
|
|
1490
1799
|
}
|