incanto 0.7.0 → 0.8.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 (48) hide show
  1. package/bin/incanto-check.mjs +6 -4
  2. package/dist/2d.d.ts +93 -3
  3. package/dist/2d.js +4 -4
  4. package/dist/3d.d.ts +45 -4
  5. package/dist/3d.js +4 -4
  6. package/dist/{audio-player-VSjk_vG8.d.ts → audio-player-Dyjg5k92.d.ts} +1 -1
  7. package/dist/audit-C6rMyict.js +58 -0
  8. package/dist/{behavior-e3kAmPGC.d.ts → behavior-Bod1AyJS.d.ts} +211 -174
  9. package/dist/{create-game-CEFoFN8d.js → create-game-66c4iYJE.js} +76 -6
  10. package/dist/{create-game-X5iRu6pf.js → create-game-BNDSbhy-.js} +88 -6
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +3 -0
  13. package/dist/{errors-BMFaY68Q.d.ts → errors-1dXlIwoR.d.ts} +7 -1
  14. package/dist/{gameplay-CDFgSG6z.js → gameplay-DPMgZk9W.js} +108 -1
  15. package/dist/gameplay.d.ts +44 -3
  16. package/dist/gameplay.js +2 -2
  17. package/dist/index.d.ts +151 -9
  18. package/dist/index.js +189 -4
  19. package/dist/{loader-y_X4zYO0.d.ts → loader-BcUDfNHn.d.ts} +1 -1
  20. package/dist/net.d.ts +46 -2
  21. package/dist/net.js +54 -2
  22. package/dist/particle-sim-CyUU7HVU.js +281 -0
  23. package/dist/{physics-2d-DmQ540uR.js → physics-2d-q2N362ph.js} +45 -4
  24. package/dist/{physics-3d-CDjuhtt_.js → physics-3d-CvHr31d4.js} +1 -1
  25. package/dist/react.d.ts +1 -1
  26. package/dist/react.js +1 -1
  27. package/dist/{register-CscQqB7V.js → register-Cbs7QxoV.js} +139 -4
  28. package/dist/{register-ClKnoILk.js → register-Cqjxrfq0.js} +254 -4
  29. package/dist/{register-C35HDZpm.js → register-CxmuI9FL.js} +1 -1
  30. package/dist/{particle-sim-CrTE7c02.js → register-DRPIjrKG.js} +1185 -278
  31. package/dist/test.d.ts +4 -10
  32. package/dist/test.js +11 -16
  33. package/editor/assets/{agent8-1WJU-yXr.js → agent8-B8QnWCeP.js} +1 -1
  34. package/editor/assets/{index-DzYFgZbl.js → index-CVhnNkb3.js} +81 -81
  35. package/editor/index.html +1 -1
  36. package/package.json +1 -1
  37. package/schemas/scene.schema.json +427 -0
  38. package/skills/incanto-3d-character.md +14 -0
  39. package/skills/incanto-audio.md +23 -0
  40. package/skills/incanto-building-2d-games.md +31 -0
  41. package/skills/incanto-building-3d-games.md +21 -0
  42. package/skills/incanto-gameplay-behaviors.md +28 -0
  43. package/skills/incanto-hud.md +24 -0
  44. package/skills/incanto-multiplayer.md +27 -0
  45. package/skills/incanto-node-reference.md +73 -0
  46. package/skills/incanto-physics-and-input.md +9 -0
  47. package/skills/incanto-verifying-your-game.md +40 -0
  48. package/dist/register-Dl_ixIJe.js +0 -834
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { _ as validateCollider2D, d as Area2D, f as CharacterBody2D, g as Node2D, m as RigidBody2D, p as PhysicsBody2D, u as Joint2D } from "./register-ClKnoILk.js";
3
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-Cqjxrfq0.js";
4
4
  import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
5
5
  //#region src/2d/physics/physics-2d.ts
6
6
  var physics_2d_exports = /* @__PURE__ */ __exportAll({
@@ -110,11 +110,27 @@ var Physics2D = class {
110
110
  if (node.snapToGround && vy >= 0) this.kcc.enableSnapToGround(.05);
111
111
  else this.kcc.disableSnapToGround();
112
112
  this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
113
+ let carryX = 0;
114
+ let carryY = 0;
115
+ if (e.ground && e.ground._physics === this) {
116
+ const [gx, gy] = worldPosition2D(e.ground);
117
+ carryX = gx - (e.groundX ?? gx);
118
+ carryY = gy - (e.groundY ?? gy);
119
+ } else e.ground = null;
113
120
  const desired = {
114
- x: (node.velocity[0] ?? 0) * this.lastDt / SCALE,
115
- y: vy * this.lastDt / SCALE
121
+ x: ((node.velocity[0] ?? 0) * this.lastDt + carryX) / SCALE * 1,
122
+ y: (vy * this.lastDt + carryY) / SCALE * 1
116
123
  };
117
- this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS);
124
+ const charBottomPx = e.body.translation().y * SCALE + charHalfHeight(node);
125
+ const predicate = (collider) => {
126
+ const other = this.byColliderHandle.get(collider.handle);
127
+ if (other && other !== node && other.collider.oneWay === true) {
128
+ if (vy < 0) return false;
129
+ if (charBottomPx > platformTopPx(other) + ONE_WAY_TOLERANCE_PX) return false;
130
+ }
131
+ return true;
132
+ };
133
+ this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, predicate);
118
134
  const corrected = this.kcc.computedMovement();
119
135
  const t = e.body.translation();
120
136
  e.body.setNextKinematicTranslation({
@@ -122,6 +138,17 @@ var Physics2D = class {
122
138
  y: t.y + corrected.y
123
139
  });
124
140
  node._grounded = this.kcc.computedGrounded();
141
+ if (node._grounded) {
142
+ const footX = (t.x + corrected.x) * SCALE;
143
+ const footY = (t.y + corrected.y) * SCALE + charHalfHeight(node) - 2;
144
+ const hit = this.castRay([footX, footY], [0, 1], 8, node);
145
+ if (hit?.node && !(hit.node instanceof CharacterBody2D)) {
146
+ e.ground = hit.node;
147
+ const [gx, gy] = worldPosition2D(hit.node);
148
+ e.groundX = gx;
149
+ e.groundY = gy;
150
+ } else e.ground = null;
151
+ } else e.ground = null;
125
152
  }
126
153
  /** Rapier debug segments, scaled back to pixels. Null while debugDraw is off. */
127
154
  debugLines() {
@@ -327,6 +354,20 @@ function collectBodies(node, out) {
327
354
  * Composed world position (translation only — ancestor rotation/scale are NOT
328
355
  * supported for physics bodies; keep body ancestors untransformed or offset-only).
329
356
  */
357
+ const ONE_WAY_TOLERANCE_PX = 4;
358
+ /** Distance from a body's center to its lowest point, px. */
359
+ function charHalfHeight(node) {
360
+ const c = node.collider;
361
+ if (c.shape === "rect") return (c.size?.[1] ?? 32) / 2;
362
+ if (c.shape === "capsule") return (c.height ?? 32) / 2 + (c.radius ?? 8);
363
+ return c.radius ?? 16;
364
+ }
365
+ /** The stand-on surface of a platform (y-down: its SMALLEST y), px. */
366
+ function platformTopPx(node) {
367
+ const [, wy] = worldPosition2D(node);
368
+ const c = node.collider;
369
+ return wy - (c.shape === "rect" ? (c.size?.[1] ?? 32) / 2 : c.shape === "capsule" ? (c.height ?? 32) / 2 + (c.radius ?? 8) : c.radius ?? 16);
370
+ }
330
371
  function worldPosition2D(node) {
331
372
  let x = 0;
332
373
  let y = 0;
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
4
- import { D as CharacterBody3D, E as Area3D, M as validateCollider3D, O as PhysicsBody3D, T as Joint3D, j as Node3D, k as RigidBody3D, x as Terrain3D } from "./register-CscQqB7V.js";
4
+ import { A as RigidBody3D, D as Area3D, E as Joint3D, M as Node3D, N as validateCollider3D, O as CharacterBody3D, S as Terrain3D, k as PhysicsBody3D } from "./register-Cbs7QxoV.js";
5
5
  import { Euler, Quaternion } from "three";
6
6
  //#region src/3d/physics/physics-3d.ts
7
7
  var physics_3d_exports = /* @__PURE__ */ __exportAll({
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as JsonValue } from "./schema-CcoWb32N.js";
2
- import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-e3kAmPGC.js";
2
+ import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-Bod1AyJS.js";
3
3
  import { CSSProperties, ReactNode } from "react";
4
4
 
5
5
  //#region src/react/index.d.ts
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-CEFoFN8d.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-X5iRu6pf.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-66c4iYJE.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-BNDSbhy-.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,10 +1,10 @@
1
1
  import { f as Node } from "./loader-B4OEXDZ8.js";
2
- import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CrTE7c02.js";
2
+ import { t as registerCoreNodes } from "./register-DRPIjrKG.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { t as registerCoreNodes } from "./register-Dl_ixIJe.js";
6
5
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
7
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
+ import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CyUU7HVU.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CroQPEER.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CubeCamera, CylinderGeometry, DataTexture, DepthTexture, DirectionalLight, DoubleSide, Euler, FloatType, Frustum, Group, HalfFloatType, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, MathUtils, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, Object3D, PerspectiveCamera, Plane, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, Sphere, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector2, Vector3, Vector4, VectorKeyframeTrack, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -1996,7 +1996,8 @@ var CharacterController3D = class extends Node3D {
1996
1996
  jumpAction: { default: "jump" },
1997
1997
  sprintAction: { default: "sprint" },
1998
1998
  skinPath: { default: "../Skin" },
1999
- skinYawOffset: { default: 0 }
1999
+ skinYawOffset: { default: 0 },
2000
+ animations: { default: {} }
2000
2001
  };
2001
2002
  view = "free";
2002
2003
  camDistance = 4;
@@ -2027,6 +2028,13 @@ var CharacterController3D = class extends Node3D {
2027
2028
  * character facing away from the camera until it first moves).
2028
2029
  */
2029
2030
  skinYawOffset = 0;
2031
+ /**
2032
+ * Movement-state → animation asset map, applied to the `skinPath` model
2033
+ * directly — no CharacterAnimator behavior needed:
2034
+ * "animations": { "idle": "$idle", "run": "$run", "fastRun": "$sprint",
2035
+ * "walk": "$walk", "airborne": "$jump" }
2036
+ */
2037
+ animations = {};
2030
2038
  /** Camera yaw/pitch in radians (mouse look mutates these). */
2031
2039
  yaw = 0;
2032
2040
  pitch = 0;
@@ -2147,6 +2155,11 @@ var CharacterController3D = class extends Node3D {
2147
2155
  const next = movementState(this.grounded, moving ? intensity : 0);
2148
2156
  if (next !== this.state) {
2149
2157
  this.state = next;
2158
+ const clip = this.animations[next];
2159
+ if (clip && this.skinPath !== "") {
2160
+ const skin = this.getNodeOrNull(this.skinPath);
2161
+ if (skin && "animation" in skin) skin.animation = clip;
2162
+ }
2150
2163
  this.emit("movementStateChanged", next);
2151
2164
  }
2152
2165
  if (moving && this.skinPath !== "") {
@@ -6108,6 +6121,127 @@ function softSprite() {
6108
6121
  return tex;
6109
6122
  }
6110
6123
  //#endregion
6124
+ //#region src/3d/nodes/trail-3d.ts
6125
+ const MAX_POINTS = 128;
6126
+ /**
6127
+ * A fading ribbon behind whatever this node is parented to — wingtip trails,
6128
+ * sword arcs, tyre streaks. Records the node's world position over `seconds`
6129
+ * and renders a camera-facing strip that tapers and fades to the tail.
6130
+ *
6131
+ * { "name": "WingTrail", "type": "Trail3D",
6132
+ * "props": { "width": 0.4, "color": "#dddddd", "seconds": 0.8 } }
6133
+ *
6134
+ * `emitting: false` stops laying new ribbon (the old tail still fades out).
6135
+ */
6136
+ var Trail3D = class extends Node3D {
6137
+ static typeName = "Trail3D";
6138
+ static props = {
6139
+ width: { default: .3 },
6140
+ color: { default: "#ffffff" },
6141
+ opacity: { default: .8 },
6142
+ seconds: { default: .6 },
6143
+ emitting: { default: true },
6144
+ additive: { default: false },
6145
+ /** Min world distance between recorded points (jitter filter). */
6146
+ minDistance: { default: .05 }
6147
+ };
6148
+ width = .3;
6149
+ color = "#ffffff";
6150
+ opacity = .8;
6151
+ seconds = .6;
6152
+ emitting = true;
6153
+ additive = false;
6154
+ minDistance = .05;
6155
+ points = [];
6156
+ clock = 0;
6157
+ mesh = null;
6158
+ lastColor = "";
6159
+ _createObject3D() {
6160
+ const group = new Group();
6161
+ const geometry = new BufferGeometry();
6162
+ geometry.setAttribute("position", new BufferAttribute(new Float32Array(MAX_POINTS * 2 * 3), 3));
6163
+ const material = new MeshBasicMaterial({
6164
+ color: this.color,
6165
+ transparent: true,
6166
+ opacity: this.opacity,
6167
+ side: DoubleSide,
6168
+ depthWrite: false,
6169
+ blending: this.additive ? AdditiveBlending : NormalBlending
6170
+ });
6171
+ this.mesh = new Mesh(geometry, material);
6172
+ this.mesh.frustumCulled = false;
6173
+ this.mesh.matrixAutoUpdate = false;
6174
+ group.add(this.mesh);
6175
+ return group;
6176
+ }
6177
+ update(dt) {
6178
+ this.clock += dt;
6179
+ while (this.points.length > 0 && this.clock - this.points[0].t > this.seconds) this.points.shift();
6180
+ }
6181
+ _onRender3D(ctx) {
6182
+ const obj = this._ensureObject3D();
6183
+ if (this.emitting) {
6184
+ obj.getWorldPosition(worldScratch);
6185
+ const last = this.points[this.points.length - 1];
6186
+ if (!last || (last.x - worldScratch.x) ** 2 + (last.y - worldScratch.y) ** 2 + (last.z - worldScratch.z) ** 2 > this.minDistance * this.minDistance) {
6187
+ this.points.push({
6188
+ x: worldScratch.x,
6189
+ y: worldScratch.y,
6190
+ z: worldScratch.z,
6191
+ t: this.clock
6192
+ });
6193
+ if (this.points.length > MAX_POINTS) this.points.shift();
6194
+ }
6195
+ }
6196
+ const mesh = this.mesh;
6197
+ if (!mesh) return;
6198
+ mesh.matrixWorld.identity();
6199
+ const mat = mesh.material;
6200
+ if (this.color !== this.lastColor) {
6201
+ this.lastColor = this.color;
6202
+ mat.color = new Color(this.color);
6203
+ }
6204
+ mat.opacity = this.opacity;
6205
+ const n = this.points.length;
6206
+ const geometry = mesh.geometry;
6207
+ const pos = geometry.getAttribute("position");
6208
+ if (n < 2) {
6209
+ geometry.setDrawRange(0, 0);
6210
+ return;
6211
+ }
6212
+ for (let i = 0; i < n; i++) {
6213
+ const p = this.points[i];
6214
+ const q = this.points[Math.min(i + 1, n - 1)] ?? p;
6215
+ dirScratch.set(q.x - p.x, q.y - p.y, q.z - p.z);
6216
+ if (dirScratch.lengthSq() < 1e-8) dirScratch.set(0, 1, 0);
6217
+ camScratch.set(ctx.camera.position.x - p.x, ctx.camera.position.y - p.y, ctx.camera.position.z - p.z);
6218
+ sideScratch.crossVectors(dirScratch, camScratch).normalize();
6219
+ const age = (this.clock - p.t) / this.seconds;
6220
+ const half = this.width / 2 * Math.max(0, 1 - age);
6221
+ pos.setXYZ(i * 2, p.x + sideScratch.x * half, p.y + sideScratch.y * half, p.z + sideScratch.z * half);
6222
+ pos.setXYZ(i * 2 + 1, p.x - sideScratch.x * half, p.y - sideScratch.y * half, p.z - sideScratch.z * half);
6223
+ }
6224
+ pos.needsUpdate = true;
6225
+ if (!geometry.index || geometry.index.count < (MAX_POINTS - 1) * 6) {
6226
+ const indices = [];
6227
+ for (let i = 0; i < MAX_POINTS - 1; i++) {
6228
+ const a = i * 2;
6229
+ indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
6230
+ }
6231
+ geometry.setIndex(indices);
6232
+ }
6233
+ geometry.setDrawRange(0, (n - 1) * 6);
6234
+ }
6235
+ /** Test hook: recorded point count. */
6236
+ get pointCount() {
6237
+ return this.points.length;
6238
+ }
6239
+ };
6240
+ const worldScratch = new Vector3();
6241
+ const dirScratch = new Vector3();
6242
+ const camScratch = new Vector3();
6243
+ const sideScratch = new Vector3();
6244
+ //#endregion
6111
6245
  //#region src/3d/vegetation/eztree/options.ts
6112
6246
  /**
6113
6247
  * Predict the EXACT triangle count `buildEzTreeGeometry` will emit for these
@@ -11481,6 +11615,7 @@ function registerNodes3D() {
11481
11615
  registerNode(CharacterController3D);
11482
11616
  registerNode(VoxelGrid3D);
11483
11617
  registerNode(Joint3D);
11618
+ registerNode(Trail3D);
11484
11619
  registerNode(Terrain3D);
11485
11620
  registerNode(Water3D);
11486
11621
  registerNode(Foliage3D);
@@ -11496,4 +11631,4 @@ function registerNodes3D() {
11496
11631
  registerNode(CharacterBody3D);
11497
11632
  }
11498
11633
  //#endregion
11499
- export { StaticBody3D as A, TERRAIN_THEMES as C, CharacterBody3D as D, Area3D as E, keyboardIntensity as F, movementState as I, rigPose as L, validateCollider3D as M, QUARTER_PITCH as N, PhysicsBody3D as O, cameraRelative as P, DEFAULT_TERRAIN_TEXTURE_BASE as S, Joint3D as T, FLOWER_VARIETIES as _, VoxelGrid3D as a, Billboard3D as b, ModelInstance3D as c, DirectionalLight3D as d, OmniLight3D as f, resolveFlowerDensity as g, Flowers3D as h, VOXEL_PALETTE as i, Node3D as j, RigidBody3D as k, LoftMesh3D as l, DENSITY_PRESETS as m, Water3D as n, Tree3D as o, Foliage3D as p, WATER_MAX_RIPPLES as r, Particles3D as s, registerNodes3D as t, MeshInstance3D as u, CharacterController3D as v, terrainThemeLayers as w, Terrain3D as x, Camera3D as y };
11634
+ export { RigidBody3D as A, DEFAULT_TERRAIN_TEXTURE_BASE as C, Area3D as D, Joint3D as E, cameraRelative as F, keyboardIntensity as I, movementState as L, Node3D as M, validateCollider3D as N, CharacterBody3D as O, QUARTER_PITCH as P, rigPose as R, Terrain3D as S, terrainThemeLayers as T, resolveFlowerDensity as _, VoxelGrid3D as a, Camera3D as b, Particles3D as c, MeshInstance3D as d, DirectionalLight3D as f, Flowers3D as g, DENSITY_PRESETS as h, VOXEL_PALETTE as i, StaticBody3D as j, PhysicsBody3D as k, ModelInstance3D as l, Foliage3D as m, Water3D as n, Tree3D as o, OmniLight3D as p, WATER_MAX_RIPPLES as r, Trail3D as s, registerNodes3D as t, LoftMesh3D as u, FLOWER_VARIETIES as v, TERRAIN_THEMES as w, Billboard3D as x, CharacterController3D as y };
@@ -1,10 +1,10 @@
1
1
  import { f as Node } from "./loader-B4OEXDZ8.js";
2
- import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CrTE7c02.js";
2
+ import { t as registerCoreNodes } from "./register-DRPIjrKG.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { t as registerCoreNodes } from "./register-Dl_ixIJe.js";
6
5
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
7
- import { AdditiveBlending, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NormalBlending, Object3D, PlaneGeometry } from "three";
6
+ import { i as applyParticlePreset, o as effectiveOrder, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-CyUU7HVU.js";
7
+ import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry } from "three";
8
8
  //#region src/2d/physics/collider-validate.ts
9
9
  /**
10
10
  * Hard-validate a 2D collider prop — called at LOAD time (via the loader's
@@ -15,6 +15,7 @@ import { AdditiveBlending, CanvasTexture, Color, DynamicDrawUsage, Group, Instan
15
15
  function validateCollider2D(collider, nodeName) {
16
16
  const shape = collider.shape;
17
17
  if (shape === void 0) return;
18
+ 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)}.`);
18
19
  if (shape === "rect") {
19
20
  const size = collider.size;
20
21
  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].`);
@@ -874,6 +875,254 @@ const colorScratchA = new Color();
874
875
  const colorScratchB = new Color();
875
876
  const colorScratchC = new Color();
876
877
  //#endregion
878
+ //#region src/2d/tilemap-grid.ts
879
+ /**
880
+ * Normalize the `cells` prop into an index grid. Two author formats:
881
+ * - string rows: `"." `/`" "` = empty, digits `0-9` = tile index, anything
882
+ * else must be in `legend` (char → index). Unknown chars HARD-fail.
883
+ * - number rows: passed through (`-1` = empty).
884
+ * Ragged rows are right-padded with empty.
885
+ */
886
+ function parseCells(cells, legend) {
887
+ const rows = [];
888
+ let width = 0;
889
+ for (const row of cells) {
890
+ const out = [];
891
+ if (typeof row === "string") for (const ch of row) {
892
+ const mapped = legend[ch];
893
+ if (mapped !== void 0) out.push(mapped);
894
+ else if (ch === "." || ch === " ") out.push(-1);
895
+ else if (ch >= "0" && ch <= "9") out.push(ch.charCodeAt(0) - 48);
896
+ 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.`);
897
+ }
898
+ else for (const v of row) out.push(v);
899
+ width = Math.max(width, out.length);
900
+ rows.push(out);
901
+ }
902
+ for (const row of rows) while (row.length < width) row.push(-1);
903
+ return rows;
904
+ }
905
+ /**
906
+ * Greedy rectangle merge over the solid cells: maximal horizontal runs,
907
+ * extended downward while the identical run repeats. A platform level's
908
+ * hundreds of solid tiles collapse to a handful of colliders.
909
+ */
910
+ function mergeSolidRects(grid, solid) {
911
+ const h = grid.length;
912
+ const w = h > 0 ? grid[0].length : 0;
913
+ const used = grid.map((row) => row.map(() => false));
914
+ const isSolid = (x, y) => solid.has(grid[y][x]);
915
+ const rects = [];
916
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) {
917
+ if (!isSolid(x, y) || used[y][x]) continue;
918
+ let runW = 1;
919
+ while (x + runW < w && isSolid(x + runW, y) && !used[y][x + runW]) runW++;
920
+ let runH = 1;
921
+ grow: while (y + runH < h) {
922
+ for (let i = 0; i < runW; i++) if (!isSolid(x + i, y + runH) || used[y + runH][x + i]) break grow;
923
+ runH++;
924
+ }
925
+ for (let dy = 0; dy < runH; dy++) for (let i = 0; i < runW; i++) used[y + dy][x + i] = true;
926
+ rects.push({
927
+ x,
928
+ y,
929
+ w: runW,
930
+ h: runH
931
+ });
932
+ }
933
+ return rects;
934
+ }
935
+ //#endregion
936
+ //#region src/2d/nodes/tile-map-2d.ts
937
+ /**
938
+ * A whole tile level as ONE node — grid render in a single draw call plus
939
+ * greedy-merged static colliders. Author the level as rows of characters:
940
+ *
941
+ * { "name": "Level", "type": "TileMap2D", "props": {
942
+ * "texture": "$tiles", "tileSize": 32,
943
+ * "cells": [
944
+ * "..................",
945
+ * "......111.........",
946
+ * "000000000000000000"
947
+ * ],
948
+ * "solid": [0, 1] } }
949
+ *
950
+ * `.` / space = empty, digits 0-9 = atlas tile index, other chars map through
951
+ * `legend` (e.g. `{"G": 12}`). The atlas is read left-to-right, top-to-bottom
952
+ * in `tileSize` squares. Tiles listed in `solid` become static colliders,
953
+ * greedy-merged into a few rectangles (hundreds of cells → a handful of
954
+ * bodies). Cell (0,0) renders with its TOP-LEFT on this node's origin.
955
+ */
956
+ var TileMap2D = class extends Node2D {
957
+ static typeName = "TileMap2D";
958
+ static props = {
959
+ /** Atlas texture asset ref; tiles are tileSize×tileSize squares in it. */
960
+ texture: { default: "" },
961
+ tileSize: { default: 32 },
962
+ /** Atlas columns; 0 = derive from the texture width. */
963
+ columns: { default: 0 },
964
+ /** Rows of chars (or number arrays, -1 empty). */
965
+ cells: { default: [] },
966
+ /** char → atlas tile index for non-digit chars. */
967
+ legend: { default: {} },
968
+ /** Tile indices that get merged static colliders. */
969
+ solid: { default: [] },
970
+ opacity: { default: 1 }
971
+ };
972
+ texture = "";
973
+ tileSize = 32;
974
+ columns = 0;
975
+ _cells = [];
976
+ _legend = {};
977
+ _solid = [];
978
+ /** Rebuild flags — raised when the map-shaping props are REPLACED. */
979
+ geometryDirty = true;
980
+ collidersDirty = true;
981
+ /** Replace the whole array to change the map (mutations are not watched). */
982
+ get cells() {
983
+ return this._cells;
984
+ }
985
+ set cells(value) {
986
+ this._cells = value;
987
+ this.geometryDirty = true;
988
+ this.collidersDirty = true;
989
+ }
990
+ get legend() {
991
+ return this._legend;
992
+ }
993
+ set legend(value) {
994
+ this._legend = value;
995
+ this.geometryDirty = true;
996
+ this.collidersDirty = true;
997
+ }
998
+ get solid() {
999
+ return this._solid;
1000
+ }
1001
+ set solid(value) {
1002
+ this._solid = value;
1003
+ this.collidersDirty = true;
1004
+ }
1005
+ opacity = 1;
1006
+ tileMesh = null;
1007
+ builtColumns = 0;
1008
+ builtRows = 0;
1009
+ /** Loader hook: bad cells fail at LOAD (unknown chars), not at render. */
1010
+ static validateJson(node) {
1011
+ const map = node;
1012
+ parseCells(map._cells, map._legend);
1013
+ }
1014
+ /** The parsed index grid (test/tooling hook). */
1015
+ grid() {
1016
+ return parseCells(this._cells, this._legend);
1017
+ }
1018
+ onReady() {
1019
+ this.rebuildColliders();
1020
+ }
1021
+ update(_dt) {
1022
+ if (this.collidersDirty) this.rebuildColliders();
1023
+ }
1024
+ rebuildColliders() {
1025
+ this.collidersDirty = false;
1026
+ for (const child of [...this.children]) if (child.name.startsWith("__tileSolid")) child.free();
1027
+ if (this._solid.length === 0) return;
1028
+ const ts = this.tileSize;
1029
+ mergeSolidRects(this.grid(), new Set(this._solid)).forEach((r, i) => {
1030
+ const body = new StaticBody2D(`__tileSolid${i}`);
1031
+ body.position = [(r.x + r.w / 2) * ts, (r.y + r.h / 2) * ts];
1032
+ body.collider = {
1033
+ shape: "rect",
1034
+ size: [r.w * ts, r.h * ts]
1035
+ };
1036
+ this.addChild(body);
1037
+ });
1038
+ }
1039
+ /** @internal The drawable mesh (lazily created under the backing object). */
1040
+ _mesh() {
1041
+ if (!this.tileMesh) {
1042
+ this.tileMesh = new Mesh(new BufferGeometry(), new MeshBasicMaterial({
1043
+ transparent: true,
1044
+ depthWrite: false
1045
+ }));
1046
+ this._ensureObject2D().add(this.tileMesh);
1047
+ }
1048
+ return this.tileMesh;
1049
+ }
1050
+ _syncObject2D(assets) {
1051
+ super._syncObject2D(assets);
1052
+ const mesh = this._mesh();
1053
+ const mat = mesh.material;
1054
+ if (!this.texture || !assets) {
1055
+ mesh.visible = false;
1056
+ return;
1057
+ }
1058
+ const texture = assets.getTexture(this.texture);
1059
+ const image = texture.image;
1060
+ if (!image?.width || !image.height) {
1061
+ mesh.visible = false;
1062
+ return;
1063
+ }
1064
+ mesh.visible = this.opacity > 0;
1065
+ mesh.renderOrder = this.effectiveRenderOrder;
1066
+ mat.opacity = this.opacity;
1067
+ if (mat.map !== texture) {
1068
+ texture.magFilter = NearestFilter;
1069
+ mat.map = texture;
1070
+ mat.needsUpdate = true;
1071
+ this.geometryDirty = true;
1072
+ }
1073
+ const columns = this.columns > 0 ? this.columns : Math.max(1, Math.floor(image.width / this.tileSize));
1074
+ const atlasRows = Math.max(1, Math.floor(image.height / this.tileSize));
1075
+ if (columns !== this.builtColumns || atlasRows !== this.builtRows) this.geometryDirty = true;
1076
+ if (!this.geometryDirty) return;
1077
+ this.geometryDirty = false;
1078
+ this.builtColumns = columns;
1079
+ this.builtRows = atlasRows;
1080
+ this.buildGeometry(mesh.geometry, columns, atlasRows);
1081
+ }
1082
+ /** One quad per visible tile, atlas UVs — a single draw call for the level. */
1083
+ buildGeometry(geometry, columns, atlasRows) {
1084
+ const grid = this.grid();
1085
+ const ts = this.tileSize;
1086
+ const positions = [];
1087
+ const uvs = [];
1088
+ const indices = [];
1089
+ for (let y = 0; y < grid.length; y++) {
1090
+ const row = grid[y];
1091
+ for (let x = 0; x < row.length; x++) {
1092
+ const tile = row[x];
1093
+ if (tile < 0) continue;
1094
+ const x0 = x * ts;
1095
+ const x1 = x0 + ts;
1096
+ const y0 = -(y * ts);
1097
+ const y1 = -((y + 1) * ts);
1098
+ const base = positions.length / 3;
1099
+ positions.push(x0, y0, 0, x1, y0, 0, x1, y1, 0, x0, y1, 0);
1100
+ const col = tile % columns;
1101
+ const rowIdx = Math.floor(tile / columns);
1102
+ const u0 = col / columns;
1103
+ const u1 = (col + 1) / columns;
1104
+ const v0 = 1 - rowIdx / atlasRows;
1105
+ const v1 = 1 - (rowIdx + 1) / atlasRows;
1106
+ uvs.push(u0, v0, u1, v0, u1, v1, u0, v1);
1107
+ indices.push(base, base + 2, base + 1, base, base + 3, base + 2);
1108
+ }
1109
+ }
1110
+ geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
1111
+ geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
1112
+ geometry.setIndex(indices);
1113
+ geometry.computeBoundingSphere();
1114
+ }
1115
+ /** Test hook: rendered tile count (quads in the built geometry). */
1116
+ get renderedTileCount() {
1117
+ const pos = this.tileMesh?.geometry.getAttribute("position");
1118
+ return pos ? pos.count / 4 : 0;
1119
+ }
1120
+ free() {
1121
+ this.tileMesh?.geometry.dispose();
1122
+ super.free();
1123
+ }
1124
+ };
1125
+ //#endregion
877
1126
  //#region src/2d/nodes/ui-layer.ts
878
1127
  const ANCHORS = [
879
1128
  "top-left",
@@ -949,6 +1198,7 @@ function registerNodes2D() {
949
1198
  registerNode(CharacterBody2D);
950
1199
  registerNode(CharacterController2D);
951
1200
  registerNode(Joint2D);
1201
+ registerNode(TileMap2D);
952
1202
  }
953
1203
  //#endregion
954
- export { validateCollider2D as _, ColorRect2D as a, AnimatedSprite2D as c, Area2D as d, CharacterBody2D as f, Node2D as g, StaticBody2D as h, Label as i, Sprite2D as l, RigidBody2D as m, UILayer as n, CharacterController2D as o, PhysicsBody2D as p, Particles2D as r, Camera2D as s, registerNodes2D as t, Joint2D as u };
1204
+ 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 };
@@ -1,6 +1,6 @@
1
1
  import { f as Node, t as buildNodeJson, y as Signal } from "./loader-B4OEXDZ8.js";
2
+ import { t as registerCoreNodes } from "./register-DRPIjrKG.js";
2
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { t as registerCoreNodes } from "./register-Dl_ixIJe.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { l as registerNode } from "./registry-BVJ2HbCn.js";
6
6
  //#region src/net/network-manager.ts