incanto 0.28.0 → 0.30.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/README.md +44 -0
- package/bin/incanto-editor.mjs +219 -29
- package/dist/2d.d.ts +54 -6
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +353 -16
- package/dist/3d.js +5 -5
- package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
- package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
- package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
- package/dist/debug-draw-BM3DsvtT.js +18 -0
- package/dist/debug.d.ts +51 -6
- package/dist/debug.js +242 -19
- package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
- package/dist/editor.d.ts +49 -0
- package/dist/editor.js +8554 -0
- package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +31 -3
- package/dist/index.js +2 -2
- package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
- package/dist/net.d.ts +1 -1
- package/dist/net.js +2 -2
- package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
- package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
- package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
- package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
- package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
- package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
- package/dist/teardown-ByzfDPyu.js +240 -0
- package/dist/test.d.ts +35 -3
- package/dist/test.js +104 -10
- package/dist/vite.d.ts +139 -1
- package/dist/vite.js +355 -4
- package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
- package/editor/assets/debug-BoEYfbqK.js +2 -0
- package/editor/assets/index-BO6WU8by.js +10696 -0
- package/editor/index.html +3 -157
- package/package.json +3 -2
- package/schemas/scene.schema.json +102 -4
- package/skills/incanto-3d-models.md +38 -0
- package/skills/incanto-assets.md +13 -0
- package/skills/incanto-building-3d-games.md +83 -6
- package/skills/incanto-editor.md +212 -8
- package/skills/incanto-environment.md +65 -1
- package/skills/incanto-node-reference.md +33 -1
- package/skills/incanto-physics-and-input.md +60 -6
- package/skills/incanto-verifying-your-game.md +57 -2
- package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
- package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/vite.config.ts +7 -0
- package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/vite.config.ts +7 -0
- package/dist/debug-draw-CZmOYjL2.js +0 -13
- package/editor/assets/index-D6RQgROR.js +0 -8330
- package/editor/assets/index-D8QvwvOm.css +0 -1
|
@@ -1,9 +1,206 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
-
import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-
|
|
4
|
-
import { n as registerDebugSource } from "./debug-draw-
|
|
5
|
-
import {
|
|
6
|
-
import { Euler, Quaternion } from "three";
|
|
3
|
+
import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-Cfr6aFZ1.js";
|
|
4
|
+
import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
|
|
5
|
+
import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-DJ0SByQg.js";
|
|
6
|
+
import { Euler, Matrix4, Quaternion, Vector3 } from "three";
|
|
7
|
+
//#region src/3d/physics/collider-lines.ts
|
|
8
|
+
/**
|
|
9
|
+
* Wireframe segments for INDIVIDUAL colliders.
|
|
10
|
+
*
|
|
11
|
+
* Rapier's own `world.debugRender()` is all-or-nothing: it draws every collider
|
|
12
|
+
* in the world, which on a real map means a terrain heightfield's grid drowning
|
|
13
|
+
* the one shape you were actually looking at. Drawing a chosen few means
|
|
14
|
+
* generating the outlines ourselves, from the same shape data the solver holds.
|
|
15
|
+
*
|
|
16
|
+
* Shapes are drawn at their true size — no bounding boxes standing in for real
|
|
17
|
+
* geometry, since a box around a rotated shape is exactly the lie this view
|
|
18
|
+
* exists to expose.
|
|
19
|
+
*/
|
|
20
|
+
const RING = 16;
|
|
21
|
+
const point = new Vector3();
|
|
22
|
+
const quat = new Quaternion();
|
|
23
|
+
function transformer(collider, out) {
|
|
24
|
+
const t = collider.translation();
|
|
25
|
+
const r = collider.rotation();
|
|
26
|
+
quat.set(r.x, r.y, r.z, r.w);
|
|
27
|
+
const put = (x, y, z) => {
|
|
28
|
+
point.set(x, y, z).applyQuaternion(quat);
|
|
29
|
+
out.push(point.x + t.x, point.y + t.y, point.z + t.z);
|
|
30
|
+
};
|
|
31
|
+
return (ax, ay, az, bx, by, bz) => {
|
|
32
|
+
put(ax, ay, az);
|
|
33
|
+
put(bx, by, bz);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** A circle in the plane spanned by two axes, at `offset` along the third. */
|
|
37
|
+
function ring(push, radius, plane, offset, arc = Math.PI * 2, from = 0) {
|
|
38
|
+
const at = (a) => {
|
|
39
|
+
const c = Math.cos(a) * radius;
|
|
40
|
+
const s = Math.sin(a) * radius;
|
|
41
|
+
if (plane === "xy") return [
|
|
42
|
+
c,
|
|
43
|
+
s,
|
|
44
|
+
offset
|
|
45
|
+
];
|
|
46
|
+
if (plane === "xz") return [
|
|
47
|
+
c,
|
|
48
|
+
offset,
|
|
49
|
+
s
|
|
50
|
+
];
|
|
51
|
+
return [
|
|
52
|
+
offset,
|
|
53
|
+
c,
|
|
54
|
+
s
|
|
55
|
+
];
|
|
56
|
+
};
|
|
57
|
+
const steps = Math.max(3, Math.round(RING * arc / (Math.PI * 2)));
|
|
58
|
+
for (let i = 0; i < steps; i++) {
|
|
59
|
+
const [ax, ay, az] = at(from + arc * i / steps);
|
|
60
|
+
const [bx, by, bz] = at(from + arc * (i + 1) / steps);
|
|
61
|
+
push(ax, ay, az, bx, by, bz);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function triangleEdges(push, vertices, indices) {
|
|
65
|
+
const v = (i) => [
|
|
66
|
+
vertices[i * 3],
|
|
67
|
+
vertices[i * 3 + 1],
|
|
68
|
+
vertices[i * 3 + 2]
|
|
69
|
+
];
|
|
70
|
+
if (indices) {
|
|
71
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
72
|
+
const a = v(indices[i]);
|
|
73
|
+
const b = v(indices[i + 1]);
|
|
74
|
+
const c = v(indices[i + 2]);
|
|
75
|
+
push(...a, ...b);
|
|
76
|
+
push(...b, ...c);
|
|
77
|
+
push(...c, ...a);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (let i = 0; i + 5 < vertices.length; i += 9) {
|
|
82
|
+
const a = v(i / 3);
|
|
83
|
+
const b = v(i / 3 + 1);
|
|
84
|
+
const c = v(i / 3 + 2);
|
|
85
|
+
push(...a, ...b);
|
|
86
|
+
push(...b, ...c);
|
|
87
|
+
push(...c, ...a);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The heights grid, drawn as its own quad lines (column-major, like Rapier). */
|
|
91
|
+
function heightfield(push, shape) {
|
|
92
|
+
const { nrows, ncols, heights, scale } = shape;
|
|
93
|
+
const rows = nrows + 1;
|
|
94
|
+
const cols = ncols + 1;
|
|
95
|
+
const at = (i, j) => [
|
|
96
|
+
(i / nrows - .5) * scale.x,
|
|
97
|
+
heights[j * rows + i] * scale.y,
|
|
98
|
+
(j / ncols - .5) * scale.z
|
|
99
|
+
];
|
|
100
|
+
for (let j = 0; j < cols; j++) for (let i = 0; i < rows; i++) {
|
|
101
|
+
if (i + 1 < rows) push(...at(i, j), ...at(i + 1, j));
|
|
102
|
+
if (j + 1 < cols) push(...at(i, j), ...at(i, j + 1));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Append one collider's wireframe to `out` (flat xyz, segment pairs). */
|
|
106
|
+
function colliderLines3D(collider, out) {
|
|
107
|
+
const push = transformer(collider, out);
|
|
108
|
+
const shape = collider.shape;
|
|
109
|
+
switch (collider.shape.type) {
|
|
110
|
+
case 0: {
|
|
111
|
+
const r = shape.radius;
|
|
112
|
+
ring(push, r, "xy", 0);
|
|
113
|
+
ring(push, r, "xz", 0);
|
|
114
|
+
ring(push, r, "yz", 0);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case 1: {
|
|
118
|
+
const h = shape.halfExtents;
|
|
119
|
+
for (const [sy, sz] of [
|
|
120
|
+
[-1, -1],
|
|
121
|
+
[-1, 1],
|
|
122
|
+
[1, -1],
|
|
123
|
+
[1, 1]
|
|
124
|
+
]) push(-h.x, sy * h.y, sz * h.z, h.x, sy * h.y, sz * h.z);
|
|
125
|
+
for (const [sx, sz] of [
|
|
126
|
+
[-1, -1],
|
|
127
|
+
[-1, 1],
|
|
128
|
+
[1, -1],
|
|
129
|
+
[1, 1]
|
|
130
|
+
]) push(sx * h.x, -h.y, sz * h.z, sx * h.x, h.y, sz * h.z);
|
|
131
|
+
for (const [sx, sy] of [
|
|
132
|
+
[-1, -1],
|
|
133
|
+
[-1, 1],
|
|
134
|
+
[1, -1],
|
|
135
|
+
[1, 1]
|
|
136
|
+
]) push(sx * h.x, sy * h.y, -h.z, sx * h.x, sy * h.y, h.z);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
case 2: {
|
|
140
|
+
const { radius, halfHeight } = shape;
|
|
141
|
+
for (const sign of [-1, 1]) {
|
|
142
|
+
ring(push, radius, "xz", sign * halfHeight);
|
|
143
|
+
for (const plane of ["xy", "yz"]) {
|
|
144
|
+
const from = sign > 0 ? 0 : Math.PI;
|
|
145
|
+
const cap = (ax, ay, az, bx, by, bz) => push(ax, ay + sign * halfHeight, az, bx, by + sign * halfHeight, bz);
|
|
146
|
+
ring(cap, radius, plane, 0, Math.PI, from);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const [x, z] of [
|
|
150
|
+
[1, 0],
|
|
151
|
+
[-1, 0],
|
|
152
|
+
[0, 1],
|
|
153
|
+
[0, -1]
|
|
154
|
+
]) push(x * radius, -halfHeight, z * radius, x * radius, halfHeight, z * radius);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case 3: {
|
|
158
|
+
const seg = shape;
|
|
159
|
+
push(seg.a.x, seg.a.y, seg.a.z, seg.b.x, seg.b.y, seg.b.z);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
case 6:
|
|
163
|
+
case 9:
|
|
164
|
+
case 16: {
|
|
165
|
+
const mesh = shape;
|
|
166
|
+
triangleEdges(push, mesh.vertices, mesh.indices);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
case 7:
|
|
170
|
+
heightfield(push, shape);
|
|
171
|
+
return;
|
|
172
|
+
case 10:
|
|
173
|
+
case 14: {
|
|
174
|
+
const { radius, halfHeight } = shape;
|
|
175
|
+
ring(push, radius, "xz", -halfHeight);
|
|
176
|
+
ring(push, radius, "xz", halfHeight);
|
|
177
|
+
for (const [x, z] of [
|
|
178
|
+
[1, 0],
|
|
179
|
+
[-1, 0],
|
|
180
|
+
[0, 1],
|
|
181
|
+
[0, -1]
|
|
182
|
+
]) push(x * radius, -halfHeight, z * radius, x * radius, halfHeight, z * radius);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
case 11:
|
|
186
|
+
case 15: {
|
|
187
|
+
const { radius, halfHeight } = shape;
|
|
188
|
+
ring(push, radius, "xz", -halfHeight);
|
|
189
|
+
for (let i = 0; i < 4; i++) {
|
|
190
|
+
const a = i / 4 * Math.PI * 2;
|
|
191
|
+
push(Math.cos(a) * radius, -halfHeight, Math.sin(a) * radius, 0, halfHeight, 0);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
default:
|
|
196
|
+
if (collider.shape.type === 12 || collider.shape.type === 13) {
|
|
197
|
+
const h = shape.halfExtents;
|
|
198
|
+
if (h) push(-h.x, -h.y, -h.z, h.x, h.y, h.z);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
7
204
|
//#region src/3d/physics/physics-3d.ts
|
|
8
205
|
var physics_3d_exports = /* @__PURE__ */ __exportAll({
|
|
9
206
|
Physics3D: () => Physics3D,
|
|
@@ -26,6 +223,8 @@ var Physics3D = class {
|
|
|
26
223
|
engine;
|
|
27
224
|
/** Render collider outlines in the GAME view (renderers pick this up). */
|
|
28
225
|
debugDraw = false;
|
|
226
|
+
/** Narrow those outlines to one node's subtree (see DebugLineSource). */
|
|
227
|
+
debugScope = null;
|
|
29
228
|
dimension = "3d";
|
|
30
229
|
unregisterDebug;
|
|
31
230
|
warnedNoCollider = /* @__PURE__ */ new WeakSet();
|
|
@@ -42,6 +241,7 @@ var Physics3D = class {
|
|
|
42
241
|
this.R = R;
|
|
43
242
|
this.engine = engine;
|
|
44
243
|
this.optsGravity = opts?.gravity;
|
|
244
|
+
this.simulate = opts?.simulate ?? true;
|
|
45
245
|
this.lastScene = engine.scene;
|
|
46
246
|
const declared = engine.scene?.physics?.gravity;
|
|
47
247
|
const g = opts?.gravity ?? declared ?? [
|
|
@@ -60,9 +260,16 @@ var Physics3D = class {
|
|
|
60
260
|
this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
|
|
61
261
|
this.syncBodies();
|
|
62
262
|
}
|
|
263
|
+
/** Whether the solver runs at all. False = collider outlines only. */
|
|
264
|
+
simulate;
|
|
63
265
|
/** @internal Driven by engine.fixedUpdated. */
|
|
64
266
|
step(dt) {
|
|
65
267
|
this.lastDt = dt;
|
|
268
|
+
if (!this.simulate) {
|
|
269
|
+
this.syncBodies();
|
|
270
|
+
this.poseFromTree();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
66
273
|
if (this.engine.scene !== this.lastScene) {
|
|
67
274
|
this.lastScene = this.engine.scene;
|
|
68
275
|
if (!this.optsGravity) {
|
|
@@ -133,9 +340,37 @@ var Physics3D = class {
|
|
|
133
340
|
}
|
|
134
341
|
});
|
|
135
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Move every body to its node's current transform.
|
|
345
|
+
*
|
|
346
|
+
* Only used in outline mode: normally physics owns the pose and writes it
|
|
347
|
+
* INTO the node, so nothing re-reads the tree. Deliberately computed the same
|
|
348
|
+
* way `ensureEntry` computes a body's initial pose — world position, node-local
|
|
349
|
+
* euler — so an outline in the editor is the outline the game will draw, right
|
|
350
|
+
* down to the places where that pair disagrees with a rotated parent.
|
|
351
|
+
*/
|
|
352
|
+
poseFromTree() {
|
|
353
|
+
for (const [node, e] of this.entries) {
|
|
354
|
+
const [wx, wy, wz] = worldPosition3D(node);
|
|
355
|
+
e.body.setTranslation({
|
|
356
|
+
x: wx,
|
|
357
|
+
y: wy,
|
|
358
|
+
z: wz
|
|
359
|
+
}, false);
|
|
360
|
+
quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
|
|
361
|
+
e.body.setRotation({
|
|
362
|
+
x: quatScratch.x,
|
|
363
|
+
y: quatScratch.y,
|
|
364
|
+
z: quatScratch.z,
|
|
365
|
+
w: quatScratch.w
|
|
366
|
+
}, false);
|
|
367
|
+
}
|
|
368
|
+
this.world.propagateModifiedBodyPositionsToColliders();
|
|
369
|
+
}
|
|
136
370
|
/** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
|
|
137
371
|
moveAndSlide(node) {
|
|
138
372
|
const e = this.ensureEntry(node);
|
|
373
|
+
if (!e) return;
|
|
139
374
|
const vy = node.velocity[1] ?? 0;
|
|
140
375
|
if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
|
|
141
376
|
else this.kcc.disableSnapToGround();
|
|
@@ -158,7 +393,36 @@ var Physics3D = class {
|
|
|
158
393
|
/** Rapier debug segments (meters). Null while debugDraw is off. */
|
|
159
394
|
debugLines() {
|
|
160
395
|
if (!this.debugDraw) return null;
|
|
161
|
-
|
|
396
|
+
const scope = this.debugScope;
|
|
397
|
+
if (!scope) return this.world.debugRender().vertices;
|
|
398
|
+
const bodies = this.scopedBodies(scope);
|
|
399
|
+
const out = [];
|
|
400
|
+
for (const body of bodies) for (let i = 0; i < body.numColliders(); i++) {
|
|
401
|
+
const collider = body.collider(i);
|
|
402
|
+
if (collider) colliderLines3D(collider, out);
|
|
403
|
+
}
|
|
404
|
+
return new Float32Array(out);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The Rapier bodies a selected node stands for: its own, its descendants',
|
|
408
|
+
* and — when it owns none — the nearest body ABOVE it, so clicking the visual
|
|
409
|
+
* mesh child of a StaticBody3D shows that body's collider rather than nothing.
|
|
410
|
+
*/
|
|
411
|
+
scopedBodies(node) {
|
|
412
|
+
const out = [];
|
|
413
|
+
const collect = (n) => {
|
|
414
|
+
const entry = this.entries.get(n);
|
|
415
|
+
if (entry) out.push(entry.body);
|
|
416
|
+
const scatter = this.scatterBodies.get(n);
|
|
417
|
+
if (scatter) out.push(scatter);
|
|
418
|
+
for (const child of n.children) collect(child);
|
|
419
|
+
};
|
|
420
|
+
collect(node);
|
|
421
|
+
for (let p = node.parent; p && out.length === 0; p = p.parent) {
|
|
422
|
+
const entry = this.entries.get(p);
|
|
423
|
+
if (entry) out.push(entry.body);
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
162
426
|
}
|
|
163
427
|
dispose() {
|
|
164
428
|
this.disconnect();
|
|
@@ -169,7 +433,63 @@ var Physics3D = class {
|
|
|
169
433
|
lastRoot = null;
|
|
170
434
|
jointSet = /* @__PURE__ */ new Set();
|
|
171
435
|
joints = /* @__PURE__ */ new Map();
|
|
436
|
+
/** One fixed body per solid scatter field, carrying a collider per row. */
|
|
437
|
+
scatterBodies = /* @__PURE__ */ new Map();
|
|
172
438
|
bodySet = /* @__PURE__ */ new Set();
|
|
439
|
+
/**
|
|
440
|
+
* Give every row of a scatter field its own static collider.
|
|
441
|
+
*
|
|
442
|
+
* A stone field is one node with fifty transforms in it, and a player walking
|
|
443
|
+
* through all fifty is what gives a world away as scenery. All of them hang
|
|
444
|
+
* off ONE fixed body — they never move, so there is nothing to step — and the
|
|
445
|
+
* whole set is rebuilt only when the tree's shape changes.
|
|
446
|
+
*/
|
|
447
|
+
syncScatters(root) {
|
|
448
|
+
for (const [, entry] of this.scatterBodies) this.world.removeRigidBody(entry);
|
|
449
|
+
this.scatterBodies.clear();
|
|
450
|
+
const fields = /* @__PURE__ */ new Set();
|
|
451
|
+
collectScatters(root, fields);
|
|
452
|
+
for (const field of fields) {
|
|
453
|
+
const spec = field.collider;
|
|
454
|
+
if (!spec) continue;
|
|
455
|
+
const [ox, oy, oz] = worldPosition3D(field);
|
|
456
|
+
const body = this.world.createRigidBody(this.R.RigidBodyDesc.fixed().setTranslation(0, 0, 0));
|
|
457
|
+
const [sx, sy, sz] = field.size;
|
|
458
|
+
let basePoints = null;
|
|
459
|
+
for (const row of field.transforms) {
|
|
460
|
+
const scale = row[4] ?? 1;
|
|
461
|
+
const x = ox + (row[0] ?? 0);
|
|
462
|
+
const y = oy + (row[1] ?? 0);
|
|
463
|
+
const z = oz + (row[2] ?? 0);
|
|
464
|
+
let desc = null;
|
|
465
|
+
if (spec.shape === "sphere") desc = this.R.ColliderDesc.ball(Math.max((spec.radius ?? Math.max(sx, sz) / 2) * scale, .01));
|
|
466
|
+
else if (spec.shape === "box") {
|
|
467
|
+
const ext = spec.size ?? [
|
|
468
|
+
sx,
|
|
469
|
+
sy,
|
|
470
|
+
sz
|
|
471
|
+
];
|
|
472
|
+
desc = this.R.ColliderDesc.cuboid(Math.max((ext[0] ?? 1) * scale / 2, .01), Math.max((ext[1] ?? 1) * scale / 2, .01), Math.max((ext[2] ?? 1) * scale / 2, .01));
|
|
473
|
+
} else {
|
|
474
|
+
basePoints ??= meshPoints(field.mesh, field.size);
|
|
475
|
+
const pts = new Float32Array(basePoints.length);
|
|
476
|
+
for (let i = 0; i < pts.length; i++) pts[i] = basePoints[i] * scale;
|
|
477
|
+
desc = this.R.ColliderDesc.convexHull(pts);
|
|
478
|
+
}
|
|
479
|
+
if (!desc) continue;
|
|
480
|
+
desc.setTranslation(x, y, z);
|
|
481
|
+
const yaw = (row[3] ?? 0) * Math.PI / 180;
|
|
482
|
+
if (yaw !== 0) desc.setRotation({
|
|
483
|
+
x: 0,
|
|
484
|
+
y: Math.sin(yaw / 2),
|
|
485
|
+
z: 0,
|
|
486
|
+
w: Math.cos(yaw / 2)
|
|
487
|
+
});
|
|
488
|
+
this.world.createCollider(desc, body);
|
|
489
|
+
}
|
|
490
|
+
this.scatterBodies.set(field, body);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
173
493
|
syncBodies() {
|
|
174
494
|
const scene = this.engine.scene;
|
|
175
495
|
if (!scene) return;
|
|
@@ -184,6 +504,7 @@ var Physics3D = class {
|
|
|
184
504
|
if (structureChanged) {
|
|
185
505
|
this.jointSet.clear();
|
|
186
506
|
collectJoints(scene.root, this.jointSet);
|
|
507
|
+
this.syncScatters(scene.root);
|
|
187
508
|
}
|
|
188
509
|
const seen = this.bodySet;
|
|
189
510
|
for (const node of seen) {
|
|
@@ -223,10 +544,18 @@ var Physics3D = class {
|
|
|
223
544
|
}
|
|
224
545
|
}
|
|
225
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Null while a `{"shape":"auto"}` body is waiting on its model — the shape
|
|
549
|
+
* lives in the model's triangles, and there is nothing honest to guess in the
|
|
550
|
+
* meantime. syncBodies runs every step, so the body appears the moment the
|
|
551
|
+
* download lands.
|
|
552
|
+
*/
|
|
226
553
|
ensureEntry(node) {
|
|
227
554
|
let e = this.entries.get(node);
|
|
228
555
|
const R = this.R;
|
|
229
556
|
if (!e) {
|
|
557
|
+
const colDesc = parseCollider3D(R, node);
|
|
558
|
+
if (!colDesc) return null;
|
|
230
559
|
let desc;
|
|
231
560
|
if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
|
|
232
561
|
else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
|
|
@@ -246,7 +575,6 @@ var Physics3D = class {
|
|
|
246
575
|
if (node.fixedRotation) desc.lockRotations();
|
|
247
576
|
}
|
|
248
577
|
const body = this.world.createRigidBody(desc);
|
|
249
|
-
const colDesc = parseCollider3D(R, node);
|
|
250
578
|
colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
|
|
251
579
|
colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
|
|
252
580
|
if (node instanceof Area3D) colDesc.setSensor(true);
|
|
@@ -448,6 +776,120 @@ function collectBodies(node, out) {
|
|
|
448
776
|
for (const c of node.children) collectBodies(c, out);
|
|
449
777
|
}
|
|
450
778
|
/**
|
|
779
|
+
* A collider shaped like the mesh actually being drawn.
|
|
780
|
+
*
|
|
781
|
+
* Hand-written extents drift from what the player sees — a box "around" a
|
|
782
|
+
* bridge is wider than the bridge, a sphere "for" a stone is smaller than the
|
|
783
|
+
* stone — and nobody notices until they walk into thin air. So the collider is
|
|
784
|
+
* built from the SAME geometry factory the renderer draws with: box → cuboid,
|
|
785
|
+
* sphere → ball, cylinder/capsule → their own shapes, and anything faceted
|
|
786
|
+
* (the `gem` stone) → a convex hull of its real vertices.
|
|
787
|
+
*
|
|
788
|
+
* Convex, always: a hull is a few planes to test against, where a triangle
|
|
789
|
+
* mesh is a tree of them, and nothing scattered across a map needs concavity.
|
|
790
|
+
*/
|
|
791
|
+
function meshShapedDesc(R, kind, size, scale) {
|
|
792
|
+
const x = (size[0] ?? 1) * scale;
|
|
793
|
+
const y = (size[1] ?? 1) * scale;
|
|
794
|
+
const z = (size[2] ?? 1) * scale;
|
|
795
|
+
switch (kind) {
|
|
796
|
+
case "box": return R.ColliderDesc.cuboid(x / 2, y / 2, z / 2);
|
|
797
|
+
case "sphere": return R.ColliderDesc.ball(x);
|
|
798
|
+
case "capsule": return R.ColliderDesc.capsule(y / 2, x);
|
|
799
|
+
case "cylinder": return R.ColliderDesc.cylinder(y / 2, x);
|
|
800
|
+
case "plane": return R.ColliderDesc.cuboid(x / 2, .01, z / 2);
|
|
801
|
+
default: return R.ColliderDesc.convexHull(meshPoints(kind, [
|
|
802
|
+
x,
|
|
803
|
+
y,
|
|
804
|
+
z
|
|
805
|
+
]));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/** The drawn geometry's vertices, flat xyz — the same mesh the player sees. */
|
|
809
|
+
function meshPoints(kind, size) {
|
|
810
|
+
const geo = buildMeshGeometry(kind, size);
|
|
811
|
+
const pos = geo.getAttribute("position");
|
|
812
|
+
const points = new Float32Array(pos.count * 3);
|
|
813
|
+
for (let i = 0; i < pos.count; i++) {
|
|
814
|
+
points[i * 3] = pos.getX(i);
|
|
815
|
+
points[i * 3 + 1] = pos.getY(i);
|
|
816
|
+
points[i * 3 + 2] = pos.getZ(i);
|
|
817
|
+
}
|
|
818
|
+
geo.dispose();
|
|
819
|
+
return points;
|
|
820
|
+
}
|
|
821
|
+
function isMountedModel(node) {
|
|
822
|
+
return typeof node._mountedObject3D === "function";
|
|
823
|
+
}
|
|
824
|
+
const localMat = new Matrix4();
|
|
825
|
+
const meshMat = new Matrix4();
|
|
826
|
+
const vertScratch = new Vector3();
|
|
827
|
+
/**
|
|
828
|
+
* The triangles of a LOADED model, in the body's own frame.
|
|
829
|
+
*
|
|
830
|
+
* A GLB is not a primitive — its shape lives in its vertices, and those are the
|
|
831
|
+
* only honest source for a collider around it. Every drawn mesh under the model
|
|
832
|
+
* is baked through its own world matrix into the body's local space, so a model
|
|
833
|
+
* that is offset, turned or fitted to `targetHeight` collides where it is drawn
|
|
834
|
+
* rather than where it was authored.
|
|
835
|
+
*
|
|
836
|
+
* Null while the asset is still downloading — the caller retries next step.
|
|
837
|
+
*/
|
|
838
|
+
function modelTriangles(body, model) {
|
|
839
|
+
const mounted = model._mountedObject3D();
|
|
840
|
+
if (!mounted) return null;
|
|
841
|
+
const bodyObj = body._ensureObject3D();
|
|
842
|
+
bodyObj.updateWorldMatrix(true, true);
|
|
843
|
+
localMat.copy(bodyObj.matrixWorld).invert();
|
|
844
|
+
const verts = [];
|
|
845
|
+
const idx = [];
|
|
846
|
+
mounted.traverse((obj) => {
|
|
847
|
+
const mesh = obj;
|
|
848
|
+
if (!mesh.isMesh) return;
|
|
849
|
+
const pos = mesh.geometry?.getAttribute("position");
|
|
850
|
+
if (!pos) return;
|
|
851
|
+
meshMat.multiplyMatrices(localMat, mesh.matrixWorld);
|
|
852
|
+
const base = verts.length / 3;
|
|
853
|
+
for (let i = 0; i < pos.count; i++) {
|
|
854
|
+
vertScratch.set(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(meshMat);
|
|
855
|
+
verts.push(vertScratch.x, vertScratch.y, vertScratch.z);
|
|
856
|
+
}
|
|
857
|
+
const index = mesh.geometry.getIndex();
|
|
858
|
+
if (index) for (let i = 0; i < index.count; i++) idx.push(base + index.getX(i));
|
|
859
|
+
else for (let i = 0; i < pos.count; i++) idx.push(base + i);
|
|
860
|
+
});
|
|
861
|
+
if (idx.length < 3) return null;
|
|
862
|
+
return {
|
|
863
|
+
vertices: new Float32Array(verts),
|
|
864
|
+
indices: new Uint32Array(idx),
|
|
865
|
+
triangles: idx.length / 3
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/** Past this, a trimesh is worth a word — level art is not collision art. */
|
|
869
|
+
const TRIMESH_BUDGET = 2e4;
|
|
870
|
+
/** A body Rapier will never move, so a hollow triangle mesh is safe on it. */
|
|
871
|
+
function isStaticShape(node) {
|
|
872
|
+
return !(node instanceof RigidBody3D || node instanceof CharacterBody3D);
|
|
873
|
+
}
|
|
874
|
+
/** Every mesh a body carries, itself first — `auto` shapes itself on one. */
|
|
875
|
+
function meshesUnder(node, out, models) {
|
|
876
|
+
if (isMountedModel(node)) models.push(node);
|
|
877
|
+
else {
|
|
878
|
+
const self = node;
|
|
879
|
+
if (typeof self.mesh === "string" && Array.isArray(self.size)) out.push({
|
|
880
|
+
name: node.name,
|
|
881
|
+
mesh: self.mesh,
|
|
882
|
+
size: self.size
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
for (const child of node.children) meshesUnder(child, out, models);
|
|
886
|
+
}
|
|
887
|
+
/** Every scatter field asking to be solid (see InstancedMesh3D.collider). */
|
|
888
|
+
function collectScatters(node, out) {
|
|
889
|
+
if (node instanceof InstancedMesh3D && node.collider) out.add(node);
|
|
890
|
+
for (const c of node.children) collectScatters(c, out);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
451
893
|
* Composed world position (translation only — ancestor rotation/scale are NOT
|
|
452
894
|
* supported for physics bodies; keep body ancestors untransformed or offset-only).
|
|
453
895
|
*/
|
|
@@ -495,7 +937,8 @@ function parseCollider3D(R, node) {
|
|
|
495
937
|
const collider = node.collider;
|
|
496
938
|
validateCollider3D(collider, node.name);
|
|
497
939
|
if (collider.shape === "heightfield") return buildHeightfieldDesc3D(R, node);
|
|
498
|
-
const desc = buildColliderDesc3D(R, collider, node.name);
|
|
940
|
+
const desc = buildColliderDesc3D(R, collider, node.name, node);
|
|
941
|
+
if (!desc) return null;
|
|
499
942
|
const offset = collider.offset;
|
|
500
943
|
if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
|
|
501
944
|
return desc;
|
|
@@ -522,7 +965,8 @@ function buildHeightfieldDesc3D(R, node) {
|
|
|
522
965
|
desc.setTranslation((terrain.position[0] ?? 0) + (offset[0] ?? 0), (terrain.position[1] ?? 0) + (offset[1] ?? 0), (terrain.position[2] ?? 0) + (offset[2] ?? 0));
|
|
523
966
|
return desc;
|
|
524
967
|
}
|
|
525
|
-
|
|
968
|
+
/** Null means "not ready yet" (a model still downloading), never "no shape". */
|
|
969
|
+
function buildColliderDesc3D(R, collider, nodeName, node) {
|
|
526
970
|
const shape = collider.shape;
|
|
527
971
|
if (shape === "box") {
|
|
528
972
|
const size = collider.size;
|
|
@@ -537,13 +981,35 @@ function buildColliderDesc3D(R, collider, nodeName) {
|
|
|
537
981
|
if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
|
|
538
982
|
return R.ColliderDesc.capsule(collider.height / 2, collider.radius);
|
|
539
983
|
}
|
|
984
|
+
if (shape === "auto") {
|
|
985
|
+
const meshes = [];
|
|
986
|
+
const models = [];
|
|
987
|
+
if (node) meshesUnder(node, meshes, models);
|
|
988
|
+
const mesh = meshes[0];
|
|
989
|
+
const model = models[0];
|
|
990
|
+
if (!mesh && !model) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': shape 'auto' needs a MeshInstance3D or ModelInstance3D on the body (or as a child) to take its shape from.`);
|
|
991
|
+
const parts = meshes.length + models.length;
|
|
992
|
+
if (parts > 1) console.warn(`[incanto] Collider on '${nodeName}': shape 'auto' fits ONE mesh and took '${(mesh ?? model)?.name}'; ${parts - 1} other mesh(es) here are not solid. Give each part its own body.`);
|
|
993
|
+
if (mesh) {
|
|
994
|
+
const desc = meshShapedDesc(R, mesh.mesh, mesh.size, 1);
|
|
995
|
+
if (desc) return desc;
|
|
996
|
+
} else if (model && node) {
|
|
997
|
+
const tri = modelTriangles(node, model);
|
|
998
|
+
if (!tri) return null;
|
|
999
|
+
if (isStaticShape(node)) {
|
|
1000
|
+
if (tri.triangles > TRIMESH_BUDGET) console.warn(`[incanto] Collider on '${nodeName}': shape 'auto' built a ${tri.triangles}-triangle mesh from '${model.name}'. Consider a low-poly collision model or an explicit box.`);
|
|
1001
|
+
return R.ColliderDesc.trimesh(tri.vertices, tri.indices);
|
|
1002
|
+
}
|
|
1003
|
+
return R.ColliderDesc.convexHull(tri.vertices);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
540
1006
|
if (shape === "trimesh") {
|
|
541
1007
|
const vertices = collider.vertices;
|
|
542
1008
|
const indices = collider.indices;
|
|
543
1009
|
if (!Array.isArray(vertices) || !Array.isArray(indices)) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz) and "indices" (triangles).`);
|
|
544
1010
|
return R.ColliderDesc.trimesh(new Float32Array(vertices), new Uint32Array(indices));
|
|
545
1011
|
}
|
|
546
|
-
throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
|
|
1012
|
+
throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'auto', 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
|
|
547
1013
|
}
|
|
548
1014
|
//#endregion
|
|
549
1015
|
export { enablePhysics3D as n, physics_3d_exports as r, Physics3D as t };
|
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-
|
|
2
|
+
import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.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-
|
|
159
|
+
const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DuBTv2zI.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-5z_QVtLx.js").then((n) => n.n)).createGame2D(o)))(opts);
|
|
160
160
|
if (disposed) {
|
|
161
161
|
next.dispose();
|
|
162
162
|
return;
|
|
@@ -1432,6 +1432,16 @@ var Engine = class {
|
|
|
1432
1432
|
* `engine.music.play(src)` / `crossfadeTo(src, secs)` / `stop(fadeOut)`;
|
|
1433
1433
|
* routes through the music bus and is advanced each frame by the loop. */
|
|
1434
1434
|
music = new MusicManager(this.audio);
|
|
1435
|
+
/**
|
|
1436
|
+
* Where nodes that build DOM (HudLayer and its widgets) mount their elements.
|
|
1437
|
+
*
|
|
1438
|
+
* `null` — the default — means `document.body` with `position: fixed`, i.e. a
|
|
1439
|
+
* game's HUD covers the window, which is what a game wants. A host that only
|
|
1440
|
+
* OWNS PART of the page sets this to its own (positioned) container so the
|
|
1441
|
+
* HUD stays inside it: the scene editor points it at the viewport pane, which
|
|
1442
|
+
* is why an edited scene's HUD no longer floats over the inspector.
|
|
1443
|
+
*/
|
|
1444
|
+
uiHost = null;
|
|
1435
1445
|
fixedStep;
|
|
1436
1446
|
maxFixedSteps;
|
|
1437
1447
|
scheduler;
|
|
@@ -1566,7 +1576,7 @@ var Engine = class {
|
|
|
1566
1576
|
this.lastMs = nowMs;
|
|
1567
1577
|
return;
|
|
1568
1578
|
}
|
|
1569
|
-
const rawDt = Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS);
|
|
1579
|
+
const rawDt = Math.max(0, Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS));
|
|
1570
1580
|
const dt = rawDt * this.timeScale;
|
|
1571
1581
|
this._time += dt;
|
|
1572
1582
|
this._unscaledTime += rawDt;
|
|
@@ -1836,8 +1846,27 @@ var HudLayer = class extends Node {
|
|
|
1836
1846
|
const el = document.createElement("div");
|
|
1837
1847
|
el.dataset.incantoHud = this.name;
|
|
1838
1848
|
el.style.cssText = `position:fixed;inset:0;pointer-events:none;z-index:${this.zIndex};font:${FONT};`;
|
|
1839
|
-
document.body.appendChild(el);
|
|
1840
1849
|
this._element = el;
|
|
1850
|
+
this._mount();
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Put the overlay where the page's owner says UI goes: `engine.uiHost`, or
|
|
1854
|
+
* the window when nobody claims it (a game — a HUD covers the screen).
|
|
1855
|
+
*
|
|
1856
|
+
* Re-checked every frame because a scene's tree is BUILT before it is handed
|
|
1857
|
+
* to an engine: at onEnterTree there is no engine to ask yet, so the first
|
|
1858
|
+
* update is where the answer actually arrives. appendChild moves the element
|
|
1859
|
+
* if it already landed somewhere else, so the HUD follows the host across a
|
|
1860
|
+
* scene swap into a differently-hosted engine (game → editor) instead of
|
|
1861
|
+
* being stranded on document.body over the inspector.
|
|
1862
|
+
*/
|
|
1863
|
+
_mount() {
|
|
1864
|
+
const el = this._element;
|
|
1865
|
+
if (!el) return;
|
|
1866
|
+
const host = this.tree?.engine?.uiHost ?? document.body;
|
|
1867
|
+
if (el.parentElement === host) return;
|
|
1868
|
+
el.style.position = host === document.body ? "fixed" : "absolute";
|
|
1869
|
+
host.appendChild(el);
|
|
1841
1870
|
}
|
|
1842
1871
|
onExitTree() {
|
|
1843
1872
|
this._element?.remove();
|
|
@@ -1845,7 +1874,10 @@ var HudLayer = class extends Node {
|
|
|
1845
1874
|
this.slots.clear();
|
|
1846
1875
|
}
|
|
1847
1876
|
update() {
|
|
1848
|
-
if (this._element)
|
|
1877
|
+
if (this._element) {
|
|
1878
|
+
this._mount();
|
|
1879
|
+
this._element.style.display = this.visible ? "" : "none";
|
|
1880
|
+
}
|
|
1849
1881
|
}
|
|
1850
1882
|
/** @internal Widgets mount into per-anchor flex columns. */
|
|
1851
1883
|
_slot(anchor) {
|