incanto 0.74.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -1,422 +1,11 @@
1
- import { J as Node, P as effectiveOrder, Y as diagnose } from "./save-slots-CEUGeLuB.js";
2
- import { M as translateOn, t as registerCoreNodes } from "./register-CjociOtt.js";
1
+ import { $ as Node, et as diagnose, i as worldPosition, n as localFromWorld } from "./pose-ByFB_J3O.js";
2
+ import { P as translateOn, t as registerCoreNodes } from "./register-B768Qedj.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-CioTZB-y.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-Bz3DHD_4.js";
6
- import { $ as localFromWorld, E as tolerateUnknownAction, O as effectRng, et as worldPosition } from "./gameplay-Bxe1sMVT.js";
4
+ import { c as getNodeSchema, p as registerNode } from "./touch-CioTZB-y.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-SQa5gIu2.js";
6
+ import { Q as tolerateUnknownAction, V as effectRng, _ as Node2D, f as Area2D, g as StaticBody2D, h as RigidBody2D, m as PhysicsBody2D, p as CharacterBody2D, vt as findHealth } from "./gameplay-Bfff_beb.js";
7
7
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
8
- import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
9
- //#region src/2d/physics/collider-validate.ts
10
- /**
11
- * Hard-validate a 2D collider prop — called at LOAD time (via the loader's
12
- * static validateJson hook) so bad shapes fail when the scene is authored,
13
- * not minutes later when physics starts. An EMPTY collider is fine: a body
14
- * without a collider is a work-in-progress node, not an error.
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
- ];
32
- function validateCollider2D(collider, nodeName) {
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
- }
41
- if (shape === void 0) return;
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)}.`);
43
- if (shape === "rect") {
44
- const size = collider.size;
45
- if (!Array.isArray(size) || size.length !== 2 || size.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': rect needs "size": [width, height].`);
46
- return;
47
- }
48
- if (shape === "circle") {
49
- if (typeof collider.radius !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': circle needs "radius".`);
50
- return;
51
- }
52
- if (shape === "capsule") {
53
- if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
54
- return;
55
- }
56
- throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'rect', 'circle', or 'capsule', got ${JSON.stringify(shape)}.`);
57
- }
58
- //#endregion
59
- //#region src/2d/nodes/node-2d.ts
60
- /**
61
- * Base of every 2D node. Convention (Phaser/Godot prior):
62
- * **1 unit = 1 px, +y DOWN, (0,0) top-left, positive rotation = clockwise.**
63
- * Internally mapped onto three's y-up space by negating y and the z-spin.
64
- */
65
- var Node2D = class extends Node {
66
- static typeName = "Node2D";
67
- static props = {
68
- position: { default: [0, 0] },
69
- rotation: { default: 0 },
70
- static: { default: false },
71
- scale: { default: [1, 1] },
72
- renderOrder: { default: 0 },
73
- orderGroup: {
74
- default: "default",
75
- options: [
76
- "background",
77
- "terrain",
78
- "default",
79
- "characters",
80
- "effects",
81
- "overlay"
82
- ]
83
- },
84
- visible: { default: true }
85
- };
86
- /** Legacy alias: old 2D scenes (and `zIndex`-trained agents) keep loading —
87
- * `zIndex` resolves to `renderOrder` at load. The schema exposes only
88
- * `renderOrder` so 2D and 3D share ONE render-order name. */
89
- static propAliases = { zIndex: "renderOrder" };
90
- /** Pixels, y-down. */
91
- position = [0, 0];
92
- /** Degrees, clockwise. */
93
- rotation = 0;
94
- /** Frozen after first sync — see Node3D.static (same semantics in 2D). */
95
- static = false;
96
- scale = [1, 1];
97
- /** Draw order among 2D drawables (higher = on top); matches 3D `renderOrder`. */
98
- renderOrder = 0;
99
- /** Named draw-order band; `renderOrder` is the fine offset inside it. */
100
- orderGroup = "default";
101
- /** What three actually sorts by: band base + renderOrder. */
102
- get effectiveRenderOrder() {
103
- return effectiveOrder(this.orderGroup, this.renderOrder, this.tree?.engine?.orderGroups);
104
- }
105
- visible = true;
106
- _object2D = null;
107
- /** @internal The backing three object (lazily created). */
108
- _ensureObject2D() {
109
- if (!this._object2D) {
110
- this._object2D = this._createObject2D();
111
- this._object2D.userData.incantoNode = this;
112
- }
113
- return this._object2D;
114
- }
115
- /** @internal Override point. */
116
- _createObject2D() {
117
- return new Object3D();
118
- }
119
- /** @internal Push JSON props onto the backing object. Called every frame. */
120
- /**
121
- * Has this node produced the thing `static: true` is about to freeze?
122
- *
123
- * `static` latches after the first sync, and a textured node's first sync
124
- * happens BEFORE its texture has decoded — `TextureLoader.load()` returns a
125
- * `Texture` with `image === undefined` and fills it asynchronously, while
126
- * `Renderer2D.render()` calls `assets.load()` and `syncTree2D()` in the same
127
- * frame. So the state frozen was `visible = false`, forever. Measured on two
128
- * identical sprites differing only in `static`:
129
- *
130
- * ```
131
- * frame 1 Backdrop(static) visible: false Control visible: false
132
- * 600 frames later Backdrop(static) visible: false width 1
133
- * Control visible: true width 64
134
- * ```
135
- *
136
- * Nothing throws: the texture loaded fine, so `assetErrors()` is empty,
137
- * `stats().errors` is 0, and `framing` reads props rather than pixels and
138
- * still calls the node on-screen. Overridden by the nodes that wait on an
139
- * image; a node with nothing to wait for is ready by definition.
140
- */
141
- _staticReady(_assets) {
142
- return true;
143
- }
144
- _syncObject2D(_assets) {
145
- const o = this._ensureObject2D();
146
- o.position.set(this.position[0] ?? 0, -(this.position[1] ?? 0), 0);
147
- o.rotation.z = -MathUtils.degToRad(this.rotation);
148
- o.scale.set(this.scale[0] ?? 1, this.scale[1] ?? 1, 1);
149
- o.visible = this.visible;
150
- }
151
- free() {
152
- super.free();
153
- this._object2D?.removeFromParent();
154
- this._object2D = null;
155
- }
156
- };
157
- //#endregion
158
- //#region src/2d/nodes/bodies-2d.ts
159
- /**
160
- * Shared base for physics-backed 2D nodes. Colliders are NODE PROPS:
161
- * `{shape:'rect', size:[w,h]}` · `{shape:'circle', radius}` ·
162
- * `{shape:'capsule', radius, height}` — never child shape nodes.
163
- */
164
- var PhysicsBody2D = class extends Node2D {
165
- static props = {
166
- collider: {
167
- default: {},
168
- variants: {
169
- tag: "shape",
170
- byTag: {
171
- rect: {
172
- shape: "rect",
173
- size: [32, 32]
174
- },
175
- circle: {
176
- shape: "circle",
177
- radius: 16
178
- },
179
- capsule: {
180
- shape: "capsule",
181
- radius: 12,
182
- height: 32
183
- }
184
- }
185
- }
186
- },
187
- /**
188
- * Whether this body's collider takes part in physics right now.
189
- *
190
- * A melee hitbox is off most of the time and live for a few frames of a
191
- * swing, and there was no way to say so: games expressed it by REPLACING
192
- * `collider` with `{}`, which tears the rigid body down and builds a new
193
- * one twice per swing, and makes the engine warn `has no collider —
194
- * physics skips it` about a scene that is correct. Keep the real collider
195
- * and turn it off.
196
- *
197
- * Off means no contacts and no `triggerEnter`/`triggerExit`. The body
198
- * itself stays, so nothing is rebuilt and re-arming is one call.
199
- */
200
- enabled: { default: true }
201
- };
202
- /** The unified collision model: every collider participant can emit these. */
203
- static signals = ["triggerEnter", "triggerExit"];
204
- _collider = {};
205
- /** @internal Physics resync flag — raised when `collider` is REPLACED. */
206
- _colliderChanged = false;
207
- _enabled = true;
208
- /** @internal Physics resync flag — raised when `enabled` is written. */
209
- _enabledChanged = false;
210
- /**
211
- * Collider shape prop. Change it by REPLACING the object
212
- * (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
213
- * body on replacement; in-place mutation of the old object is not watched.
214
- */
215
- get collider() {
216
- return this._collider;
217
- }
218
- set collider(value) {
219
- this._collider = value;
220
- this._colliderChanged = true;
221
- }
222
- /** See the `enabled` prop. Writing it arms or disarms the existing collider. */
223
- get enabled() {
224
- return this._enabled;
225
- }
226
- set enabled(value) {
227
- if (value === this._enabled) return;
228
- this._enabled = value;
229
- this._enabledChanged = true;
230
- }
231
- /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
232
- static validateJson(node) {
233
- validateCollider2D(node.collider, node.name);
234
- }
235
- /** @internal Set by Physics2D when the body is created. */
236
- _physics = null;
237
- };
238
- /**
239
- * Immovable collider (ground, walls, platforms).
240
- *
241
- * It also carries the SURFACE it is: `friction` and `restitution` used to
242
- * live on `RigidBody2D` alone, so a trampoline, an ice patch or a sticky
243
- * landing could not be written in scene JSON, and Rapier's AVERAGE rule
244
- * halved whatever a ball brought to a plain floor. A static body's authored
245
- * value WINS the pair — restitution above 0 combines by Max, friction below
246
- * the default by Min and above it by Max — so the pad decides, whatever lands
247
- * on it. Left at the defaults, nothing changes for any existing scene.
248
- */
249
- var StaticBody2D = class extends PhysicsBody2D {
250
- static typeName = "StaticBody2D";
251
- static props = {
252
- friction: { default: .5 },
253
- restitution: { default: 0 }
254
- };
255
- /** 0.5 plain · 0 ice (slides on and on) · 1+ glue. Away from 0.5 it wins the pair. */
256
- friction = .5;
257
- /** 0 dead · 1 trampoline. Above 0 it wins the pair: a dead ball still bounces. */
258
- restitution = 0;
259
- };
260
- /**
261
- * Sensor volume. Emits `triggerEnter(other)` / `triggerExit(other)` — the
262
- * unified collision model (solid bodies emit the same signals on contact).
263
- */
264
- var Area2D = class extends PhysicsBody2D {
265
- static typeName = "Area2D";
266
- /**
267
- * Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
268
- * See `Area3D.overlapping`. Empty in a scene with no physics world.
269
- */
270
- overlapping(group = void 0) {
271
- const all = this._physics?.overlapping(this) ?? [];
272
- return group === void 0 ? all : all.filter((n) => n.isInGroup(group));
273
- }
274
- };
275
- /** Dynamic simulated body. */
276
- var RigidBody2D = class extends PhysicsBody2D {
277
- static typeName = "RigidBody2D";
278
- static props = {
279
- mass: { default: 1 },
280
- gravityScale: { default: 1 },
281
- fixedRotation: { default: false },
282
- friction: { default: .5 },
283
- restitution: { default: 0 },
284
- linearVelocity: { default: [0, 0] },
285
- angularVelocity: { default: 0 },
286
- linearDamping: {
287
- default: 0,
288
- range: { min: 0 }
289
- },
290
- angularDamping: {
291
- default: 0,
292
- range: { min: 0 }
293
- }
294
- };
295
- mass = 1;
296
- gravityScale = 1;
297
- fixedRotation = false;
298
- friction = .5;
299
- restitution = 0;
300
- /** Drag, per second: velocity decays by `e^(-linearDamping·t)`. `0` = none. */
301
- linearDamping = 0;
302
- /** Rolling resistance, per second, on the spin — see `RigidBody3D.angularDamping`. */
303
- angularDamping = 0;
304
- /** px/s, y-down. Read back every step; write to launch. */
305
- linearVelocity = [0, 0];
306
- /**
307
- * rad/s about the z axis — the same sign as `rotation`, which is CLOCKWISE
308
- * in this y-down space. Read back every step; write to spin.
309
- *
310
- * `RigidBody3D` got its `angularVelocity` because "a puzzle builder could
311
- * neither launch a spinning body nor read how fast a lever was swinging —
312
- * only its angle, differenced by hand". Same sentence, same defect, other
313
- * adapter: 2D had `linearVelocity` and no counterpart. `fixedRotation` pins
314
- * it at zero.
315
- */
316
- angularVelocity = 0;
317
- /**
318
- * The other name for `linearVelocity`, because it is the name every skill,
319
- * every sibling node and every other engine uses.
320
- *
321
- * `body.velocity = [0, -400]` is what `incanto-physics-and-input.md` taught
322
- * for years. On a plain TS class that assignment SUCCEEDS — it just writes a
323
- * stray field nothing reads — so a jump that never happened produced no
324
- * error, no warning and nothing to search for. `CharacterBody2D.velocity`
325
- * next to `RigidBody2D.linearVelocity` is the kind of near-miss a builder
326
- * cannot be expected to notice.
327
- *
328
- * Scene JSON still names the prop `linearVelocity` and rejects `velocity`
329
- * loudly, with the valid keys listed — a hard error corrects itself.
330
- */
331
- get velocity() {
332
- return this.linearVelocity;
333
- }
334
- set velocity(v) {
335
- this.linearVelocity = v;
336
- }
337
- /**
338
- * Impulse in px·kg/s (y-down) — the 3D twin has had this since it shipped,
339
- * and calling it here was a TypeError.
340
- */
341
- applyImpulse(impulse) {
342
- this._physics?.applyImpulse(this, impulse);
343
- }
344
- };
345
- /**
346
- * Kinematic body on Rapier's character controller. Gravity is NOT applied
347
- * automatically (Godot semantics) — integrate `velocity` yourself, then call
348
- * `moveAndSlide()` from `fixedUpdate`.
349
- */
350
- var CharacterBody2D = class extends PhysicsBody2D {
351
- static typeName = "CharacterBody2D";
352
- static props = {
353
- velocity: { default: [0, 0] },
354
- stickToGround: { default: true },
355
- slopeLimitDeg: { default: 45 },
356
- stepHeight: {
357
- default: 35,
358
- range: { min: 0 }
359
- },
360
- collideWithCharacters: { default: true }
361
- };
362
- /**
363
- * Renamed to match the 3D body, where `snapToGround` had to give the name
364
- * back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is
365
- * ambiguous here and old scenes keep loading unchanged.
366
- */
367
- static propAliases = { snapToGround: "stickToGround" };
368
- /** px/s, y-down (up = -y). */
369
- velocity = [0, 0];
370
- /** Keep contact on slopes/steps while not moving upward. */
371
- stickToGround = true;
372
- /** Max climbable slope angle. */
373
- slopeLimitDeg = 45;
374
- /**
375
- * How high a step this body walks up without jumping, in PIXELS.
376
- *
377
- * Rapier's character controller does not autostep unless it is asked, and in
378
- * 2D nothing asked: a ledge of ANY height — measured, ONE pixel — stopped a
379
- * walking body dead, while the mirrored `CharacterBody3D` has cleared 0.35 m
380
- * by default since 0.63. The default here is that same 0.35 m at the 2D
381
- * world's own scale (100 px = 1 m, which is why gravity defaults to 980).
382
- * `0` restores the old behaviour.
383
- */
384
- stepHeight = 35;
385
- /** See CharacterBody3D.collideWithCharacters — off, a crowd ignores itself and costs nothing. */
386
- collideWithCharacters = true;
387
- /** @internal Updated by Physics2D.moveAndSlide. */
388
- _grounded = false;
389
- moveAndSlide() {
390
- if (!this._physics) throw new IncantoError("TREE_VIOLATION", this.collider.shape === void 0 ? `CharacterBody2D '${this.name}' has no collider — set a collider prop (physics skips collider-less bodies).` : `CharacterBody2D '${this.name}' has no physics world — await enablePhysics2D(engine) first.`);
391
- this._physics.moveAndSlide(this);
392
- }
393
- /** @internal Set by the physics step from the KCC's resolved collisions. */
394
- _onWall = 0;
395
- /** @internal */
396
- _onCeiling = false;
397
- /**
398
- * Touching a wall, and which side (-1 left, +1 right, 0 neither).
399
- *
400
- * Wall jump and wall slide are unbuildable without it — not awkward to build,
401
- * impossible: `isOnFloor()` was the only surface a character could report, so
402
- * even a hand-written behavior had nothing to ask.
403
- */
404
- isOnWall() {
405
- return this._onWall !== 0;
406
- }
407
- /** -1 the wall is to the left, +1 to the right, 0 no wall. */
408
- wallSide() {
409
- return this._onWall;
410
- }
411
- /** Head hit something — cut the jump short, the way every platformer does. */
412
- isOnCeiling() {
413
- return this._onCeiling;
414
- }
415
- isOnFloor() {
416
- return this._grounded;
417
- }
418
- };
419
- //#endregion
8
+ import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, PlaneGeometry, SRGBColorSpace } from "three";
420
9
  //#region src/2d/nodes/joint-2d.ts
421
10
  /**
422
11
  * A physics joint linking its PARENT body to `target` (a node path to the
@@ -438,6 +27,7 @@ var Joint2D = class extends Node2D {
438
27
  options: [
439
28
  "fixed",
440
29
  "revolute",
30
+ "prismatic",
441
31
  "rope",
442
32
  "spring"
443
33
  ]
@@ -449,6 +39,17 @@ var Joint2D = class extends Node2D {
449
39
  },
450
40
  anchor: { default: [0, 0] },
451
41
  targetAnchor: { default: [0, 0] },
42
+ /** The line of a `prismatic` slide, in this body's local frame (y-down: `[0, -1]` is up). */
43
+ axis: { default: [0, 1] },
44
+ /** The stops: `[minDeg, maxDeg]` for a `revolute`, `[min, max]` px for a `prismatic`; empty = free. */
45
+ limits: { default: [] },
46
+ /** The motor's target speed — degrees a second (`revolute`) or px a second (`prismatic`); 0 = no motor. */
47
+ motorSpeed: { default: 0 },
48
+ /** How hard the motor holds its speed (acceleration per rad/s of error). */
49
+ motorStrength: {
50
+ default: 20,
51
+ range: { min: 0 }
52
+ },
452
53
  length: { default: 0 },
453
54
  stiffness: { default: 50 },
454
55
  damping: { default: 5 },
@@ -461,6 +62,27 @@ var Joint2D = class extends Node2D {
461
62
  target = "";
462
63
  anchor = [0, 0];
463
64
  targetAnchor = [0, 0];
65
+ /**
66
+ * `[min, max]` in degrees for a `revolute` swing; `[]` = no stops. Measured
67
+ * as `angle` is: this body turned relative to the target, `rotation`'s sign.
68
+ */
69
+ limits = [];
70
+ /** Degrees a second the motor drives the pin at; `0` = no motor (free). */
71
+ motorSpeed = 0;
72
+ /** The motor's gain — how quickly it reaches and holds `motorSpeed`. */
73
+ motorStrength = 20;
74
+ /**
75
+ * The pin's current angle in degrees — this body's rotation relative to the
76
+ * target, written by physics every step. `0` unless `revolute` and simulated.
77
+ */
78
+ angle = 0;
79
+ /** The line of a `prismatic` slide (local to this body). */
80
+ axis = [0, 1];
81
+ /**
82
+ * A `prismatic` slide's current travel in px — this body's anchor along
83
+ * `axis` relative to the target's; `0` where the anchors coincide.
84
+ */
85
+ travel = 0;
464
86
  /** rope/spring rest length in px; 0 = measure body distance at creation. */
465
87
  length = 0;
466
88
  stiffness = 50;
@@ -493,6 +115,8 @@ var Joint2D = class extends Node2D {
493
115
  onEnterTree() {
494
116
  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"}'.`);
495
117
  if (this.target === "") throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}' needs a "target" node path to the body it links to.`);
118
+ if (this.type === "prismatic" && !(Math.hypot(this.axis[0] ?? 0, this.axis[1] ?? 0) > 1e-6)) throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}': a prismatic joint needs a non-zero "axis" (got ${JSON.stringify(this.axis)}).`);
119
+ if ((this.type === "revolute" || this.type === "prismatic") && this.limits.length !== 0 && this.limits.length !== 2) throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}': "limits" is [minDeg, maxDeg] or [] (got ${JSON.stringify(this.limits)}).`);
496
120
  }
497
121
  /** @internal The two bodies, resolved (throws on a bad target at step time). */
498
122
  _resolveBodies() {
@@ -749,7 +373,7 @@ var AnimatedSprite2D = class extends Sprite2D {
749
373
  * with exponential smoothing; `limits [minX,minY,maxX,maxY]` clamp the view
750
374
  * rect inside a world region (applied by the renderer via `clampedCenter`).
751
375
  */
752
- var Camera2D = class extends Node2D {
376
+ var Camera2D = class Camera2D extends Node2D {
753
377
  static typeName = "Camera2D";
754
378
  static props = {
755
379
  follow: {
@@ -759,7 +383,15 @@ var Camera2D = class extends Node2D {
759
383
  smoothing: { default: 0 },
760
384
  zoom: { default: 1 },
761
385
  limits: { default: [] },
762
- current: { default: false }
386
+ current: { default: false },
387
+ /**
388
+ * Seconds the view FLIES from the last drawn centre and zoom to this camera
389
+ * when it becomes current (0 = a cut) — the 2D twin of `Camera3D.blendSeconds`.
390
+ */
391
+ blendSeconds: {
392
+ default: 0,
393
+ range: { min: 0 }
394
+ }
763
395
  };
764
396
  /** NodePath of a Node2D to track. */
765
397
  follow = "";
@@ -769,6 +401,25 @@ var Camera2D = class extends Node2D {
769
401
  /** [minX, minY, maxX, maxY] world px; empty = unlimited. */
770
402
  limits = [];
771
403
  current = false;
404
+ /**
405
+ * Make THIS the scene's camera and no other: the renderer takes the first
406
+ * camera whose `current` is true in tree order, so setting your own without
407
+ * clearing the gameplay camera's changed nothing visible. A handler a
408
+ * connection can name — `triggerEnter → DoorCam.makeCurrent`.
409
+ */
410
+ makeCurrent(blendSeconds = void 0) {
411
+ const root = this.tree?.root ?? this.getRoot();
412
+ const walk = (n) => {
413
+ if (n instanceof Camera2D) n.current = n === this;
414
+ for (const c of n.children) walk(c);
415
+ };
416
+ walk(root);
417
+ this.current = true;
418
+ this._blendRequest = blendSeconds;
419
+ }
420
+ blendSeconds = 0;
421
+ /** @internal `makeCurrent(seconds)` — read once by the renderer. */
422
+ _blendRequest = void 0;
772
423
  /** `zoom` clamped to a positive floor — 0/negative zoom must never produce NaN views. */
773
424
  get effectiveZoom() {
774
425
  return Math.max(.01, this.zoom);
@@ -860,7 +511,8 @@ function clampCentered(v, lo, hi) {
860
511
  const DEFAULT_ACTIONS = {
861
512
  moveAction: "move",
862
513
  jumpAction: "jump",
863
- dashAction: "dash"
514
+ dashAction: "dash",
515
+ crouchAction: "crouch"
864
516
  };
865
517
  var CharacterController2D = class extends Node {
866
518
  static typeName = "CharacterController2D";
@@ -888,7 +540,25 @@ var CharacterController2D = class extends Node {
888
540
  /** 0 = no wall slide. Terminal fall speed while against a wall (px/s). */
889
541
  wallSlideSpeed: { default: 0 },
890
542
  /** 0 = no wall jump. `[away, up]` impulse in px/s. */
891
- wallJumpImpulse: { default: [0, 0] }
543
+ wallJumpImpulse: { default: [0, 0] },
544
+ /**
545
+ * The collider's height (px) while the crouch key is held — a rect's
546
+ * `size[1]`, a capsule's `height`; 0 = no crouch. The body drops by half
547
+ * the difference so the feet stay put, a gap lower than the standing body
548
+ * is ducked through, and a ceiling that will not let you stand keeps you
549
+ * down until you are out from under it. Platformer mode only.
550
+ */
551
+ crouchHeight: {
552
+ default: 0,
553
+ range: { min: 0 }
554
+ },
555
+ /** Pace while crouched, as a fraction of the standing pace. */
556
+ crouchSpeedMultiplier: {
557
+ default: .5,
558
+ range: { min: 0 }
559
+ },
560
+ /** Held = crouch. Missing from the input map = the character never crouches. */
561
+ crouchAction: { default: "crouch" }
892
562
  };
893
563
  static signals = ["movementStateChanged"];
894
564
  mode = "platformer";
@@ -904,6 +574,13 @@ var CharacterController2D = class extends Node {
904
574
  dashSpeed = 0;
905
575
  dashSeconds = .15;
906
576
  dashAction = "dash";
577
+ crouchHeight = 0;
578
+ crouchSpeedMultiplier = .5;
579
+ crouchAction = "crouch";
580
+ /** Read-only: shorter right now — by the key, or by a ceiling. */
581
+ crouching = false;
582
+ /** The collider height to stand back up to. */
583
+ standingHeight = 0;
907
584
  wallSlideSpeed = 0;
908
585
  wallJumpImpulse = [0, 0];
909
586
  /** What the character is doing — drive `AnimatedSprite2D.play(state)` off it. */
@@ -928,13 +605,20 @@ var CharacterController2D = class extends Node {
928
605
  x: 0,
929
606
  y: 0
930
607
  }, "the character will not move");
608
+ const health = findHealth(body)?.health ?? null;
609
+ if (health?.staggered ?? false) {
610
+ dir.x = 0;
611
+ dir.y = 0;
612
+ }
613
+ const kick = health?.knocked ? health.kickVelocity : [];
614
+ const lift = health?.knocked ? health.takeLift() : 0;
931
615
  if (this.mode === "topDown") {
932
- body.velocity = [dir.x * this.maxSpeed, dir.y * this.maxSpeed];
616
+ body.velocity = kick.length > 0 ? [kick[0] ?? 0, kick[1] ?? 0] : [dir.x * this.maxSpeed, dir.y * this.maxSpeed];
933
617
  body.moveAndSlide();
934
618
  return;
935
619
  }
936
620
  const gravityY = (engine.scene.physics?.gravity)?.[1] ?? 980;
937
- const grounded = body.isOnFloor();
621
+ const grounded = body.isOnFloor() && !(this.rising && (body.velocity[1] ?? 0) < 0);
938
622
  const jumpImpulse = -Math.sqrt(2 * gravityY * this.jumpHeight);
939
623
  if (this.dashLeft > 0) {
940
624
  this.dashLeft -= dt;
@@ -953,7 +637,24 @@ var CharacterController2D = class extends Node {
953
637
  this.setState("dash");
954
638
  return;
955
639
  }
956
- body.velocity[0] = dir.x * this.maxSpeed;
640
+ body.velocity[0] = kick.length > 0 ? kick[0] ?? 0 : dir.x * this.maxSpeed;
641
+ const wantCrouch = this.crouchHeight > 0 && this.readAction("crouchAction", () => input.isPressed(this.crouchAction), false, "the character will never crouch");
642
+ if (!this.crouching) {
643
+ const standing = colliderHeight(body.collider);
644
+ if (wantCrouch && grounded && this.crouchHeight < standing) {
645
+ this.standingHeight = standing;
646
+ this.crouching = true;
647
+ setColliderHeight(body, this.crouchHeight, standing);
648
+ }
649
+ } else if (!wantCrouch) {
650
+ const rise = this.standingHeight - this.crouchHeight;
651
+ const top = (body.position[1] ?? 0) - halfExtent(body.collider) + 1;
652
+ if (!body._physics?.castRay([body.position[0] ?? 0, top], [0, -1], rise + 2, body)) {
653
+ this.crouching = false;
654
+ setColliderHeight(body, this.standingHeight, this.crouchHeight);
655
+ }
656
+ }
657
+ if (this.crouching) body.velocity[0] = (body.velocity[0] ?? 0) * this.crouchSpeedMultiplier;
957
658
  if (grounded) {
958
659
  this.coyoteLeft = this.coyoteSeconds;
959
660
  this.jumpsUsed = 0;
@@ -964,7 +665,11 @@ var CharacterController2D = class extends Node {
964
665
  const jumpEdge = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "the character will never jump");
965
666
  if (jumpEdge) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
966
667
  else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
967
- const wantsJump = this.bufferLeft > 0 || jumpEdge;
668
+ let wantsJump = this.bufferLeft > 0 || jumpEdge;
669
+ if (this.crouching) {
670
+ wantsJump = false;
671
+ this.bufferLeft = 0;
672
+ }
968
673
  const onWall = this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0) ? body.isOnWall() : false;
969
674
  if (wantsJump && onWall && !grounded && this.wallJumpImpulse.some((v) => v !== 0)) {
970
675
  const away = -body.wallSide();
@@ -992,6 +697,10 @@ var CharacterController2D = class extends Node {
992
697
  if ((body.velocity[1] ?? 0) >= 0) this.rising = false;
993
698
  if (this.wallSlideSpeed > 0 && onWall && (body.velocity[1] ?? 0) > this.wallSlideSpeed) body.velocity[1] = this.wallSlideSpeed;
994
699
  }
700
+ if (lift !== 0) {
701
+ body.velocity[1] = lift;
702
+ if (lift < 0) this.rising = true;
703
+ }
995
704
  if (body.isOnCeiling() && (body.velocity[1] ?? 0) < 0) {
996
705
  body.velocity[1] = 0;
997
706
  this.rising = false;
@@ -999,8 +708,10 @@ var CharacterController2D = class extends Node {
999
708
  body.moveAndSlide();
1000
709
  const vy = body.velocity[1] ?? 0;
1001
710
  if (this.wallSlideSpeed > 0 && !body.isOnFloor() && body.isOnWall() && vy > 0) this.setState("wallSlide");
1002
- else if (body.isOnFloor()) this.setState(Math.abs(dir.x) > .01 ? "run" : "idle");
1003
- else this.setState(vy < 0 ? "jump" : "fall");
711
+ else if (body.isOnFloor()) {
712
+ const moving = Math.abs(dir.x) > .01;
713
+ this.setState(this.crouching ? moving ? "sneak" : "crouch" : moving ? "run" : "idle");
714
+ } else this.setState(vy < 0 ? "jump" : "fall");
1004
715
  }
1005
716
  /**
1006
717
  * Emits `movementStateChanged(state)` on a change — the 3D sibling has had
@@ -1045,6 +756,40 @@ var CharacterController2D = class extends Node {
1045
756
  }, action, read, fallback);
1046
757
  }
1047
758
  };
759
+ /** The collider's height along y: a rect's size[1], a capsule's straight part; a circle cannot crouch (0). */
760
+ function colliderHeight(c) {
761
+ if (c.shape === "rect") return c.size?.[1] ?? 0;
762
+ if (c.shape === "capsule") return c.height ?? 0;
763
+ return 0;
764
+ }
765
+ /** Half the collider's full extent along y — where its top and bottom edges are. */
766
+ function halfExtent(c) {
767
+ if (c.shape === "rect") return (c.size?.[1] ?? 0) / 2;
768
+ if (c.shape === "capsule") return (c.height ?? 0) / 2 + (c.radius ?? 0);
769
+ return c.radius ?? 0;
770
+ }
771
+ /**
772
+ * Swap the collider's height and keep the feet where they are: y-down, so a
773
+ * shorter body's centre moves DOWN by half the difference (and back up on
774
+ * standing). The adapter rebuilds the body at the new place next step.
775
+ */
776
+ function setColliderHeight(body, next, from) {
777
+ const c = body.collider;
778
+ if (c.shape === "rect") {
779
+ const size = c.size ?? [32, from];
780
+ body.collider = {
781
+ ...c,
782
+ size: [size[0] ?? 32, next]
783
+ };
784
+ } else if (c.shape === "capsule") body.collider = {
785
+ ...c,
786
+ height: next
787
+ };
788
+ else return;
789
+ const d = (from - next) / 2;
790
+ const p = body.position;
791
+ body.position = [p[0] ?? 0, (p[1] ?? 0) + d];
792
+ }
1048
793
  //#endregion
1049
794
  //#region src/2d/nodes/color-rect-2d.ts
1050
795
  /** Shared unit quad (same discipline as sprites — never THREE.Sprite). */
@@ -1307,7 +1052,11 @@ var Particles2D = class Particles2D extends Node2D {
1307
1052
  default: "add",
1308
1053
  options: ["add", "normal"]
1309
1054
  },
1310
- maxParticles: { default: 256 }
1055
+ maxParticles: { default: 256 },
1056
+ /** Half-extents (px) of a box the particles are born in, around the node; `[0,0]` = a point. */
1057
+ emitBox: { default: [0, 0] },
1058
+ /** A steady velocity (px/s, y+ = down) every particle is born with — wind on smoke. */
1059
+ drift: { default: [0, 0] }
1311
1060
  };
1312
1061
  preset = "custom";
1313
1062
  emitting = true;
@@ -1345,6 +1094,8 @@ var Particles2D = class Particles2D extends Node2D {
1345
1094
  shimmer = 0;
1346
1095
  blend = "add";
1347
1096
  maxParticles = 256;
1097
+ emitBox = [0, 0];
1098
+ drift = [0, 0];
1348
1099
  /** Loader hook: unknown presets and bad blends fail at LOAD. */
1349
1100
  static validateJson(node) {
1350
1101
  const p = node;
@@ -1377,7 +1128,17 @@ var Particles2D = class Particles2D extends Node2D {
1377
1128
  0
1378
1129
  ],
1379
1130
  drag: this.drag,
1380
- maxParticles: this.maxParticles
1131
+ maxParticles: this.maxParticles,
1132
+ emitBox: [
1133
+ this.emitBox[0] ?? 0,
1134
+ this.emitBox[1] ?? 0,
1135
+ 0
1136
+ ],
1137
+ drift: [
1138
+ this.drift[0] ?? 0,
1139
+ this.drift[1] ?? 0,
1140
+ 0
1141
+ ]
1381
1142
  }, rng);
1382
1143
  }
1383
1144
  return this.sim;
@@ -2125,4 +1886,4 @@ function registerNodes2D() {
2125
1886
  registerNode(TileMap2D);
2126
1887
  }
2127
1888
  //#endregion
2128
- export { RigidBody2D as _, parseCells as a, validateCollider2D as b, ColorRect2D as c, AnimatedSprite2D as d, Sprite2D as f, PhysicsBody2D as g, CharacterBody2D as h, mergeSolidRects as i, CharacterController2D as l, Area2D as m, UILayer as n, Particles2D as o, Joint2D as p, TileMap2D as r, Label as s, registerNodes2D as t, Camera2D as u, StaticBody2D as v, Node2D as y };
1889
+ export { parseCells as a, ColorRect2D as c, AnimatedSprite2D as d, Sprite2D as f, mergeSolidRects as i, CharacterController2D as l, UILayer as n, Particles2D as o, Joint2D as p, TileMap2D as r, Label as s, registerNodes2D as t, Camera2D as u };