incanto 0.1.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/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
|
+
import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
|
|
4
|
+
import { C as Area3D, E as RigidBody3D, O as Node3D, T as PhysicsBody3D, k as validateCollider3D, w as CharacterBody3D, y as Terrain3D } from "./register-CNlYAS1_.js";
|
|
5
|
+
import { Euler, Quaternion } from "three";
|
|
6
|
+
//#region src/3d/physics/physics-3d.ts
|
|
7
|
+
var physics_3d_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
Physics3D: () => Physics3D,
|
|
9
|
+
enablePhysics3D: () => enablePhysics3D
|
|
10
|
+
});
|
|
11
|
+
const DEG = Math.PI / 180;
|
|
12
|
+
/**
|
|
13
|
+
* Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
|
|
14
|
+
* so games without physics never pay its bundle cost. 1 unit = 1 meter, y-up —
|
|
15
|
+
* three's native space, no conversion.
|
|
16
|
+
*/
|
|
17
|
+
async function enablePhysics3D(engine, opts) {
|
|
18
|
+
const R = await import("@dimforge/rapier3d-compat");
|
|
19
|
+
await R.init();
|
|
20
|
+
return new Physics3D(R, engine, opts);
|
|
21
|
+
}
|
|
22
|
+
/** Per-engine 3D physics world. Same contract as Physics2D. */
|
|
23
|
+
var Physics3D = class {
|
|
24
|
+
R;
|
|
25
|
+
engine;
|
|
26
|
+
/** Render collider outlines in the GAME view (renderers pick this up). */
|
|
27
|
+
debugDraw = false;
|
|
28
|
+
dimension = "3d";
|
|
29
|
+
unregisterDebug;
|
|
30
|
+
warnedNoCollider = /* @__PURE__ */ new WeakSet();
|
|
31
|
+
entries = /* @__PURE__ */ new Map();
|
|
32
|
+
byColliderHandle = /* @__PURE__ */ new Map();
|
|
33
|
+
world;
|
|
34
|
+
events;
|
|
35
|
+
kcc;
|
|
36
|
+
disconnect;
|
|
37
|
+
lastDt = 1 / 60;
|
|
38
|
+
optsGravity;
|
|
39
|
+
lastScene = null;
|
|
40
|
+
constructor(R, engine, opts) {
|
|
41
|
+
this.R = R;
|
|
42
|
+
this.engine = engine;
|
|
43
|
+
this.optsGravity = opts?.gravity;
|
|
44
|
+
this.lastScene = engine.scene;
|
|
45
|
+
const declared = engine.scene?.physics?.gravity;
|
|
46
|
+
const g = opts?.gravity ?? declared ?? [
|
|
47
|
+
0,
|
|
48
|
+
-9.81,
|
|
49
|
+
0
|
|
50
|
+
];
|
|
51
|
+
this.world = new R.World({
|
|
52
|
+
x: g[0] ?? 0,
|
|
53
|
+
y: g[1] ?? 0,
|
|
54
|
+
z: g[2] ?? 0
|
|
55
|
+
});
|
|
56
|
+
this.kcc = this.world.createCharacterController(.01);
|
|
57
|
+
this.events = new R.EventQueue(true);
|
|
58
|
+
this.unregisterDebug = registerDebugSource(this);
|
|
59
|
+
this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
|
|
60
|
+
this.syncBodies();
|
|
61
|
+
}
|
|
62
|
+
/** @internal Driven by engine.fixedUpdated. */
|
|
63
|
+
step(dt) {
|
|
64
|
+
this.lastDt = dt;
|
|
65
|
+
if (this.engine.scene !== this.lastScene) {
|
|
66
|
+
this.lastScene = this.engine.scene;
|
|
67
|
+
if (!this.optsGravity) {
|
|
68
|
+
const g = this.engine.scene?.physics?.gravity ?? [
|
|
69
|
+
0,
|
|
70
|
+
-9.81,
|
|
71
|
+
0
|
|
72
|
+
];
|
|
73
|
+
this.world.gravity = {
|
|
74
|
+
x: g[0] ?? 0,
|
|
75
|
+
y: g[1] ?? 0,
|
|
76
|
+
z: g[2] ?? 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.syncBodies();
|
|
81
|
+
this.world.timestep = dt;
|
|
82
|
+
this.world.step(this.events);
|
|
83
|
+
for (const [node, e] of this.entries) {
|
|
84
|
+
const t = e.body.translation();
|
|
85
|
+
if (!e.body.isFixed()) {
|
|
86
|
+
const [ox, oy, oz] = parentWorldOffset3D(node);
|
|
87
|
+
const next = [
|
|
88
|
+
t.x - ox,
|
|
89
|
+
t.y - oy,
|
|
90
|
+
t.z - oz
|
|
91
|
+
];
|
|
92
|
+
node._interpPrev = node._interpCurr ?? next;
|
|
93
|
+
node._interpCurr = next;
|
|
94
|
+
node.position = [
|
|
95
|
+
next[0],
|
|
96
|
+
next[1],
|
|
97
|
+
next[2]
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
if (node instanceof RigidBody3D) {
|
|
101
|
+
if (!node.fixedRotation) {
|
|
102
|
+
const q = e.body.rotation();
|
|
103
|
+
eulerScratch.setFromQuaternion(quatScratch.set(q.x, q.y, q.z, q.w), "XYZ");
|
|
104
|
+
node.rotation = [
|
|
105
|
+
eulerScratch.x / DEG,
|
|
106
|
+
eulerScratch.y / DEG,
|
|
107
|
+
eulerScratch.z / DEG
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
const lv = e.body.linvel();
|
|
111
|
+
node.linearVelocity = [
|
|
112
|
+
lv.x,
|
|
113
|
+
lv.y,
|
|
114
|
+
lv.z
|
|
115
|
+
];
|
|
116
|
+
e.lastV = [
|
|
117
|
+
lv.x,
|
|
118
|
+
lv.y,
|
|
119
|
+
lv.z
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
e.last = worldPosition3D(node);
|
|
123
|
+
}
|
|
124
|
+
this.events.drainCollisionEvents((h1, h2, started) => {
|
|
125
|
+
const a = this.byColliderHandle.get(h1);
|
|
126
|
+
const b = this.byColliderHandle.get(h2);
|
|
127
|
+
if (!a || !b) return;
|
|
128
|
+
const sig = started ? "triggerEnter" : "triggerExit";
|
|
129
|
+
a.emit(sig, b);
|
|
130
|
+
b.emit(sig, a);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
|
|
134
|
+
moveAndSlide(node) {
|
|
135
|
+
const e = this.ensureEntry(node);
|
|
136
|
+
const vy = node.velocity[1] ?? 0;
|
|
137
|
+
if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
|
|
138
|
+
else this.kcc.disableSnapToGround();
|
|
139
|
+
this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
|
|
140
|
+
const desired = {
|
|
141
|
+
x: (node.velocity[0] ?? 0) * this.lastDt,
|
|
142
|
+
y: vy * this.lastDt,
|
|
143
|
+
z: (node.velocity[2] ?? 0) * this.lastDt
|
|
144
|
+
};
|
|
145
|
+
this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS);
|
|
146
|
+
const c = this.kcc.computedMovement();
|
|
147
|
+
const t = e.body.translation();
|
|
148
|
+
e.body.setNextKinematicTranslation({
|
|
149
|
+
x: t.x + c.x,
|
|
150
|
+
y: t.y + c.y,
|
|
151
|
+
z: t.z + c.z
|
|
152
|
+
});
|
|
153
|
+
node._grounded = this.kcc.computedGrounded();
|
|
154
|
+
}
|
|
155
|
+
/** Rapier debug segments (meters). Null while debugDraw is off. */
|
|
156
|
+
debugLines() {
|
|
157
|
+
if (!this.debugDraw) return null;
|
|
158
|
+
return this.world.debugRender().vertices;
|
|
159
|
+
}
|
|
160
|
+
dispose() {
|
|
161
|
+
this.disconnect();
|
|
162
|
+
this.unregisterDebug();
|
|
163
|
+
this.world.free();
|
|
164
|
+
}
|
|
165
|
+
syncBodies() {
|
|
166
|
+
const scene = this.engine.scene;
|
|
167
|
+
if (!scene) return;
|
|
168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
169
|
+
collectBodies(scene.root, seen);
|
|
170
|
+
for (const node of seen) {
|
|
171
|
+
if (node.collider.shape === void 0) {
|
|
172
|
+
const stale = this.entries.get(node);
|
|
173
|
+
if (stale) {
|
|
174
|
+
this.byColliderHandle.delete(stale.collider.handle);
|
|
175
|
+
this.world.removeRigidBody(stale.body);
|
|
176
|
+
this.entries.delete(node);
|
|
177
|
+
node._physics = null;
|
|
178
|
+
node._interpPrev = node._interpCurr = null;
|
|
179
|
+
}
|
|
180
|
+
if (!this.warnedNoCollider.has(node)) {
|
|
181
|
+
this.warnedNoCollider.add(node);
|
|
182
|
+
console.warn(`[incanto] '${node.name}' has no collider — physics skips it.`);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const existing = this.entries.get(node);
|
|
187
|
+
if (existing && existing.colliderKey !== JSON.stringify(node.collider)) {
|
|
188
|
+
this.byColliderHandle.delete(existing.collider.handle);
|
|
189
|
+
this.world.removeRigidBody(existing.body);
|
|
190
|
+
this.entries.delete(node);
|
|
191
|
+
node._physics = null;
|
|
192
|
+
node._interpPrev = node._interpCurr = null;
|
|
193
|
+
}
|
|
194
|
+
this.ensureEntry(node);
|
|
195
|
+
}
|
|
196
|
+
for (const [node, e] of this.entries) if (!seen.has(node)) {
|
|
197
|
+
this.byColliderHandle.delete(e.collider.handle);
|
|
198
|
+
this.world.removeRigidBody(e.body);
|
|
199
|
+
this.entries.delete(node);
|
|
200
|
+
node._physics = null;
|
|
201
|
+
node._interpPrev = node._interpCurr = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
ensureEntry(node) {
|
|
205
|
+
let e = this.entries.get(node);
|
|
206
|
+
const R = this.R;
|
|
207
|
+
if (!e) {
|
|
208
|
+
let desc;
|
|
209
|
+
if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
|
|
210
|
+
else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
|
|
211
|
+
else desc = R.RigidBodyDesc.fixed();
|
|
212
|
+
const [wx, wy, wz] = worldPosition3D(node);
|
|
213
|
+
desc.setTranslation(wx, wy, wz);
|
|
214
|
+
quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
|
|
215
|
+
desc.setRotation({
|
|
216
|
+
x: quatScratch.x,
|
|
217
|
+
y: quatScratch.y,
|
|
218
|
+
z: quatScratch.z,
|
|
219
|
+
w: quatScratch.w
|
|
220
|
+
});
|
|
221
|
+
if (node instanceof RigidBody3D) {
|
|
222
|
+
desc.setGravityScale(node.gravityScale);
|
|
223
|
+
desc.setLinvel(node.linearVelocity[0] ?? 0, node.linearVelocity[1] ?? 0, node.linearVelocity[2] ?? 0);
|
|
224
|
+
if (node.fixedRotation) desc.lockRotations();
|
|
225
|
+
}
|
|
226
|
+
const body = this.world.createRigidBody(desc);
|
|
227
|
+
const colDesc = parseCollider3D(R, node);
|
|
228
|
+
colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
|
|
229
|
+
colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
|
|
230
|
+
if (node instanceof Area3D) colDesc.setSensor(true);
|
|
231
|
+
if (node instanceof RigidBody3D) colDesc.setMass(node.mass).setFriction(node.friction).setRestitution(node.restitution);
|
|
232
|
+
const collider = this.world.createCollider(colDesc, body);
|
|
233
|
+
e = {
|
|
234
|
+
colliderKey: JSON.stringify(node.collider),
|
|
235
|
+
body,
|
|
236
|
+
collider,
|
|
237
|
+
last: [
|
|
238
|
+
wx,
|
|
239
|
+
wy,
|
|
240
|
+
wz
|
|
241
|
+
],
|
|
242
|
+
lastV: [
|
|
243
|
+
0,
|
|
244
|
+
0,
|
|
245
|
+
0
|
|
246
|
+
]
|
|
247
|
+
};
|
|
248
|
+
this.entries.set(node, e);
|
|
249
|
+
this.byColliderHandle.set(collider.handle, node);
|
|
250
|
+
node._physics = this;
|
|
251
|
+
if (node instanceof RigidBody3D) node._physics3d = this;
|
|
252
|
+
return e;
|
|
253
|
+
}
|
|
254
|
+
const p = worldPosition3D(node);
|
|
255
|
+
if (p[0] !== e.last[0] || p[1] !== e.last[1] || p[2] !== e.last[2]) {
|
|
256
|
+
e.body.setTranslation({
|
|
257
|
+
x: p[0],
|
|
258
|
+
y: p[1],
|
|
259
|
+
z: p[2]
|
|
260
|
+
}, true);
|
|
261
|
+
e.last = p;
|
|
262
|
+
node._interpPrev = node._interpCurr = null;
|
|
263
|
+
}
|
|
264
|
+
if (node instanceof RigidBody3D) {
|
|
265
|
+
const v = [
|
|
266
|
+
node.linearVelocity[0] ?? 0,
|
|
267
|
+
node.linearVelocity[1] ?? 0,
|
|
268
|
+
node.linearVelocity[2] ?? 0
|
|
269
|
+
];
|
|
270
|
+
if (v[0] !== e.lastV[0] || v[1] !== e.lastV[1] || v[2] !== e.lastV[2]) {
|
|
271
|
+
e.body.setLinvel({
|
|
272
|
+
x: v[0],
|
|
273
|
+
y: v[1],
|
|
274
|
+
z: v[2]
|
|
275
|
+
}, true);
|
|
276
|
+
e.lastV = v;
|
|
277
|
+
}
|
|
278
|
+
if (node.gravityScale !== e.body.gravityScale()) e.body.setGravityScale(node.gravityScale, true);
|
|
279
|
+
}
|
|
280
|
+
return e;
|
|
281
|
+
}
|
|
282
|
+
/** Apply a world-space impulse to a dynamic body (character controllers). */
|
|
283
|
+
applyImpulse(node, impulse) {
|
|
284
|
+
this.entries.get(node)?.body.applyImpulse({
|
|
285
|
+
x: impulse[0],
|
|
286
|
+
y: impulse[1],
|
|
287
|
+
z: impulse[2]
|
|
288
|
+
}, true);
|
|
289
|
+
}
|
|
290
|
+
/** Mass the solver actually uses (collider-derived unless overridden). */
|
|
291
|
+
massOf(node) {
|
|
292
|
+
return this.entries.get(node)?.body.mass() ?? 1;
|
|
293
|
+
}
|
|
294
|
+
/** Current solver velocity (fresher than the node prop mid-step). */
|
|
295
|
+
velocityOf(node) {
|
|
296
|
+
const v = this.entries.get(node)?.body.linvel();
|
|
297
|
+
return v ? [
|
|
298
|
+
v.x,
|
|
299
|
+
v.y,
|
|
300
|
+
v.z
|
|
301
|
+
] : [
|
|
302
|
+
0,
|
|
303
|
+
0,
|
|
304
|
+
0
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* World-space raycast (meters). `exclude` skips that body's collider —
|
|
309
|
+
* ground probes from inside a capsule need it. `opts.staticOnly` hits ONLY the
|
|
310
|
+
* fixed/static world (excludes both dynamic AND kinematic bodies): the camera
|
|
311
|
+
* spring arm uses it so neither a dynamic projectile nor a KINEMATIC enemy
|
|
312
|
+
* (CharacterBody3D) passing behind the player yanks the camera in.
|
|
313
|
+
*/
|
|
314
|
+
castRay(origin, dir, maxLen, exclude, opts) {
|
|
315
|
+
const ray = new this.R.Ray({
|
|
316
|
+
x: origin[0],
|
|
317
|
+
y: origin[1],
|
|
318
|
+
z: origin[2]
|
|
319
|
+
}, {
|
|
320
|
+
x: dir[0],
|
|
321
|
+
y: dir[1],
|
|
322
|
+
z: dir[2]
|
|
323
|
+
});
|
|
324
|
+
const excludeCollider = exclude ? this.entries.get(exclude)?.collider : void 0;
|
|
325
|
+
const flags = opts?.staticOnly ? this.R.QueryFilterFlags.EXCLUDE_DYNAMIC | this.R.QueryFilterFlags.EXCLUDE_KINEMATIC : void 0;
|
|
326
|
+
const hit = this.world.castRayAndGetNormal(ray, maxLen, true, flags, void 0, excludeCollider, void 0, (collider) => !collider.isSensor());
|
|
327
|
+
if (!hit) return null;
|
|
328
|
+
return {
|
|
329
|
+
distance: hit.timeOfImpact,
|
|
330
|
+
normal: [
|
|
331
|
+
hit.normal.x,
|
|
332
|
+
hit.normal.y,
|
|
333
|
+
hit.normal.z
|
|
334
|
+
],
|
|
335
|
+
node: this.byColliderHandle.get(hit.collider.handle) ?? null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const quatScratch = new Quaternion();
|
|
340
|
+
const eulerScratch = new Euler();
|
|
341
|
+
function collectBodies(node, out) {
|
|
342
|
+
if (node instanceof PhysicsBody3D) out.add(node);
|
|
343
|
+
for (const c of node.children) collectBodies(c, out);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Composed world position (translation only — ancestor rotation/scale are NOT
|
|
347
|
+
* supported for physics bodies; keep body ancestors untransformed or offset-only).
|
|
348
|
+
*/
|
|
349
|
+
function worldPosition3D(node) {
|
|
350
|
+
let x = 0;
|
|
351
|
+
let y = 0;
|
|
352
|
+
let z = 0;
|
|
353
|
+
for (let n = node; n; n = n.parent) if (n instanceof Node3D) {
|
|
354
|
+
x += n.position[0] ?? 0;
|
|
355
|
+
y += n.position[1] ?? 0;
|
|
356
|
+
z += n.position[2] ?? 0;
|
|
357
|
+
}
|
|
358
|
+
return [
|
|
359
|
+
x,
|
|
360
|
+
y,
|
|
361
|
+
z
|
|
362
|
+
];
|
|
363
|
+
}
|
|
364
|
+
function parentWorldOffset3D(node) {
|
|
365
|
+
let x = 0;
|
|
366
|
+
let y = 0;
|
|
367
|
+
let z = 0;
|
|
368
|
+
for (let n = node.parent; n; n = n.parent) if (n instanceof Node3D) {
|
|
369
|
+
x += n.position[0] ?? 0;
|
|
370
|
+
y += n.position[1] ?? 0;
|
|
371
|
+
z += n.position[2] ?? 0;
|
|
372
|
+
}
|
|
373
|
+
return [
|
|
374
|
+
x,
|
|
375
|
+
y,
|
|
376
|
+
z
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
function parseCollider3D(R, node) {
|
|
380
|
+
const collider = node.collider;
|
|
381
|
+
validateCollider3D(collider, node.name);
|
|
382
|
+
if (collider.shape === "heightfield") return buildHeightfieldDesc3D(R, node);
|
|
383
|
+
const desc = buildColliderDesc3D(R, collider, node.name);
|
|
384
|
+
const offset = collider.offset;
|
|
385
|
+
if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
|
|
386
|
+
return desc;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* `{shape:'heightfield'}` pulls its height grid from a Terrain3D CHILD of the
|
|
390
|
+
* body — no duplicated data in the collider prop, terrain and collider can
|
|
391
|
+
* never drift apart.
|
|
392
|
+
*/
|
|
393
|
+
function buildHeightfieldDesc3D(R, node) {
|
|
394
|
+
const terrain = node.children.find((c) => c instanceof Terrain3D);
|
|
395
|
+
if (!terrain) throw new IncantoError("BAD_FORMAT", `Collider on '${node.name}': heightfield needs a Terrain3D CHILD of this body — add { "type": "Terrain3D" } under the StaticBody3D; the collider reads its height grid.`, { prop: "collider" });
|
|
396
|
+
const hm = terrain._heightmap();
|
|
397
|
+
const nrows = hm.segsZ;
|
|
398
|
+
const ncols = hm.segsX;
|
|
399
|
+
const heights = new Float32Array((nrows + 1) * (ncols + 1));
|
|
400
|
+
for (let ix = 0; ix <= ncols; ix++) for (let iz = 0; iz <= nrows; iz++) heights[ix * (nrows + 1) + iz] = hm.heights[iz * (ncols + 1) + ix];
|
|
401
|
+
const desc = R.ColliderDesc.heightfield(nrows, ncols, heights, {
|
|
402
|
+
x: hm.width,
|
|
403
|
+
y: 1,
|
|
404
|
+
z: hm.depth
|
|
405
|
+
});
|
|
406
|
+
const offset = Array.isArray(node.collider.offset) ? node.collider.offset : [];
|
|
407
|
+
desc.setTranslation((terrain.position[0] ?? 0) + (offset[0] ?? 0), (terrain.position[1] ?? 0) + (offset[1] ?? 0), (terrain.position[2] ?? 0) + (offset[2] ?? 0));
|
|
408
|
+
return desc;
|
|
409
|
+
}
|
|
410
|
+
function buildColliderDesc3D(R, collider, nodeName) {
|
|
411
|
+
const shape = collider.shape;
|
|
412
|
+
if (shape === "box") {
|
|
413
|
+
const size = collider.size;
|
|
414
|
+
if (!Array.isArray(size) || size.length !== 3 || size.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': box needs "size": [x, y, z].`);
|
|
415
|
+
return R.ColliderDesc.cuboid(size[0] / 2, size[1] / 2, size[2] / 2);
|
|
416
|
+
}
|
|
417
|
+
if (shape === "sphere") {
|
|
418
|
+
if (typeof collider.radius !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': sphere needs "radius".`);
|
|
419
|
+
return R.ColliderDesc.ball(collider.radius);
|
|
420
|
+
}
|
|
421
|
+
if (shape === "capsule") {
|
|
422
|
+
if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
|
|
423
|
+
return R.ColliderDesc.capsule(collider.height / 2, collider.radius);
|
|
424
|
+
}
|
|
425
|
+
if (shape === "trimesh") {
|
|
426
|
+
const vertices = collider.vertices;
|
|
427
|
+
const indices = collider.indices;
|
|
428
|
+
if (!Array.isArray(vertices) || !Array.isArray(indices)) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz) and "indices" (triangles).`);
|
|
429
|
+
return R.ColliderDesc.trimesh(new Float32Array(vertices), new Uint32Array(indices));
|
|
430
|
+
}
|
|
431
|
+
throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
|
|
432
|
+
}
|
|
433
|
+
//#endregion
|
|
434
|
+
export { enablePhysics3D as n, physics_3d_exports as r, Physics3D as t };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { c as JsonValue } from "./schema-CcoWb32N.js";
|
|
2
|
+
import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAQq7HGM.js";
|
|
3
|
+
import { CSSProperties, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/react/index.d.ts
|
|
6
|
+
/** The common surface of Game2D/Game3D the React layer relies on. */
|
|
7
|
+
interface GameHandle {
|
|
8
|
+
engine: Engine;
|
|
9
|
+
scene: Scene;
|
|
10
|
+
renderer: {
|
|
11
|
+
dispose(): void; /** Both renderers provide it (3D takes a z too) — world → canvas px. */
|
|
12
|
+
screenFromWorld?(wx: number, wy: number, wz?: number): {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
/** The running game (null while booting). Must be used under <IncantoCanvas>. */
|
|
20
|
+
declare function useGame(): GameHandle | null;
|
|
21
|
+
/** The running engine (null while booting). */
|
|
22
|
+
declare function useEngine(): Engine | null;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to a node prop by path — re-renders ONLY when the value changes
|
|
25
|
+
* (compared per frame via engine.updated). The React-HUD bread and butter:
|
|
26
|
+
* `const text = useNodeProp<string>('UI/Score', 'text')`.
|
|
27
|
+
*/
|
|
28
|
+
declare function useNodeProp<T extends JsonValue>(path: string, prop: string): T | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to a node signal — auto-disconnects on unmount and re-wires on
|
|
31
|
+
* scene swaps. The handler may change between renders without re-wiring.
|
|
32
|
+
*/
|
|
33
|
+
declare function useSignal(path: string, signal: string, handler: (...args: unknown[]) => void): void;
|
|
34
|
+
interface IncantoCanvasProps {
|
|
35
|
+
/** Scene JSON (cloned internally). Pass a stable reference — a new object re-boots. */
|
|
36
|
+
scene: unknown;
|
|
37
|
+
/** '2d' | '3d' (default: the scene's dimension, falling back to '2d'). */
|
|
38
|
+
render?: "2d" | "3d";
|
|
39
|
+
behaviors?: Record<string, BehaviorCtor>;
|
|
40
|
+
physics?: "auto" | boolean;
|
|
41
|
+
touch?: "auto" | boolean;
|
|
42
|
+
debug?: boolean;
|
|
43
|
+
seed?: number;
|
|
44
|
+
fixedHz?: number;
|
|
45
|
+
pixelRatio?: number;
|
|
46
|
+
antialias?: boolean;
|
|
47
|
+
/** 3D: pointer-look on the canvas. */
|
|
48
|
+
pointer?: boolean | {
|
|
49
|
+
lockOnClick?: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** 'window' (default) | 'canvas' (tabIndex + focus scoped) | false. */
|
|
52
|
+
keyboard?: "window" | "canvas" | false;
|
|
53
|
+
className?: string;
|
|
54
|
+
style?: CSSProperties;
|
|
55
|
+
/** Shown in the overlay until the game is ready. */
|
|
56
|
+
fallback?: ReactNode;
|
|
57
|
+
onReady?: (game: GameHandle) => void;
|
|
58
|
+
/** DOM HUD overlay — absolutely positioned over the canvas, game context inside. */
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
/** @internal Test seam — replaces the createGame call. */
|
|
61
|
+
_gameFactory?: (opts: Record<string, unknown>) => Promise<GameHandle>;
|
|
62
|
+
}
|
|
63
|
+
declare function IncantoCanvas(props: IncantoCanvasProps): ReactNode;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { GameHandle, IncantoCanvas, IncantoCanvasProps, useEngine, useGame, useNodeProp, useSignal };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
|
|
2
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/react/index.tsx
|
|
5
|
+
/**
|
|
6
|
+
* incanto/react — embed an Incanto game in a React app like any component.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { IncantoCanvas, useNodeProp } from 'incanto/react';
|
|
10
|
+
* import sceneJson from './game.scene.json';
|
|
11
|
+
* import { PlayerControl } from './behaviors';
|
|
12
|
+
*
|
|
13
|
+
* <IncantoCanvas scene={sceneJson} behaviors={{ PlayerControl }}>
|
|
14
|
+
* <Hud /> // children = DOM HUD overlay, with useGame()/useNodeProp() access
|
|
15
|
+
* </IncantoCanvas>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* StrictMode-safe (every booted game is disposed), SSR-safe (boot happens in
|
|
19
|
+
* an effect), and bundle-lean (the 2D or 3D stack lazy-loads by scene
|
|
20
|
+
* dimension — a 2D game never pulls the 3D renderer). React is an optional
|
|
21
|
+
* peer dependency: nothing here loads unless you import 'incanto/react'.
|
|
22
|
+
*/
|
|
23
|
+
const GameContext = createContext(null);
|
|
24
|
+
/** The running game (null while booting). Must be used under <IncantoCanvas>. */
|
|
25
|
+
function useGame() {
|
|
26
|
+
return useContext(GameContext);
|
|
27
|
+
}
|
|
28
|
+
/** The running engine (null while booting). */
|
|
29
|
+
function useEngine() {
|
|
30
|
+
return useGame()?.engine ?? null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to a node prop by path — re-renders ONLY when the value changes
|
|
34
|
+
* (compared per frame via engine.updated). The React-HUD bread and butter:
|
|
35
|
+
* `const text = useNodeProp<string>('UI/Score', 'text')`.
|
|
36
|
+
*/
|
|
37
|
+
function useNodeProp(path, prop) {
|
|
38
|
+
const game = useGame();
|
|
39
|
+
const [value, setValue] = useState(void 0);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!game) return;
|
|
42
|
+
const read = () => {
|
|
43
|
+
let next;
|
|
44
|
+
try {
|
|
45
|
+
const node = game.engine.scene?.root.getNodeOrNull(path);
|
|
46
|
+
next = node ? node[prop] : void 0;
|
|
47
|
+
} catch {
|
|
48
|
+
next = void 0;
|
|
49
|
+
}
|
|
50
|
+
setValue((prev) => {
|
|
51
|
+
if (jsonEquals(prev ?? null, next ?? null)) return prev;
|
|
52
|
+
return next === void 0 ? void 0 : jsonClone(next);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
read();
|
|
56
|
+
const offUpdated = game.engine.updated.connect(read);
|
|
57
|
+
const offScene = game.engine.sceneChanged.connect(read);
|
|
58
|
+
return () => {
|
|
59
|
+
offUpdated();
|
|
60
|
+
offScene();
|
|
61
|
+
};
|
|
62
|
+
}, [
|
|
63
|
+
game,
|
|
64
|
+
path,
|
|
65
|
+
prop
|
|
66
|
+
]);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to a node signal — auto-disconnects on unmount and re-wires on
|
|
71
|
+
* scene swaps. The handler may change between renders without re-wiring.
|
|
72
|
+
*/
|
|
73
|
+
function useSignal(path, signal, handler) {
|
|
74
|
+
const game = useGame();
|
|
75
|
+
const handlerRef = useRef(handler);
|
|
76
|
+
handlerRef.current = handler;
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!game) return;
|
|
79
|
+
const wire = () => {
|
|
80
|
+
try {
|
|
81
|
+
const node = game.engine.scene?.root.getNodeOrNull(path);
|
|
82
|
+
return node ? node.on(signal, (...args) => handlerRef.current(...args)) : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
let off = wire();
|
|
88
|
+
const offScene = game.engine.sceneChanged.connect(() => {
|
|
89
|
+
off?.();
|
|
90
|
+
off = wire();
|
|
91
|
+
});
|
|
92
|
+
return () => {
|
|
93
|
+
off?.();
|
|
94
|
+
offScene();
|
|
95
|
+
};
|
|
96
|
+
}, [
|
|
97
|
+
game,
|
|
98
|
+
path,
|
|
99
|
+
signal
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
function shallowEqual(a, b) {
|
|
103
|
+
if (a === b) return true;
|
|
104
|
+
if (!a || !b) return false;
|
|
105
|
+
const keysA = Object.keys(a);
|
|
106
|
+
return keysA.length === Object.keys(b).length && keysA.every((k) => a[k] === b[k]);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Inline `behaviors={{ PlayerControl }}` creates a new object every render —
|
|
110
|
+
* keep the previous reference while the contents are shallow-equal so the
|
|
111
|
+
* boot effect never sees a phantom change (no re-boot loops).
|
|
112
|
+
*/
|
|
113
|
+
function useStableShallow(value) {
|
|
114
|
+
const ref = useRef(value);
|
|
115
|
+
if (!shallowEqual(value, ref.current)) ref.current = value;
|
|
116
|
+
return ref.current;
|
|
117
|
+
}
|
|
118
|
+
function resolveKeyboard(keyboard, canvas) {
|
|
119
|
+
if (keyboard === "canvas") {
|
|
120
|
+
canvas.tabIndex = 0;
|
|
121
|
+
canvas.focus?.();
|
|
122
|
+
return canvas;
|
|
123
|
+
}
|
|
124
|
+
if (keyboard === false) return false;
|
|
125
|
+
}
|
|
126
|
+
function IncantoCanvas(props) {
|
|
127
|
+
const canvasRef = useRef(null);
|
|
128
|
+
const containerRef = useRef(null);
|
|
129
|
+
const [game, setGame] = useState(null);
|
|
130
|
+
const propsRef = useRef(props);
|
|
131
|
+
propsRef.current = props;
|
|
132
|
+
const mode = props.render ?? props.scene.dimension ?? "2d";
|
|
133
|
+
const { scene, _gameFactory } = props;
|
|
134
|
+
const behaviors = useStableShallow(props.behaviors);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const canvas = canvasRef.current;
|
|
137
|
+
const container = containerRef.current;
|
|
138
|
+
if (!canvas || !container) return;
|
|
139
|
+
let disposed = false;
|
|
140
|
+
let booted = null;
|
|
141
|
+
(async () => {
|
|
142
|
+
const latest = propsRef.current;
|
|
143
|
+
const keyboard = resolveKeyboard(latest.keyboard, canvas);
|
|
144
|
+
const opts = {
|
|
145
|
+
canvas,
|
|
146
|
+
scene,
|
|
147
|
+
behaviors,
|
|
148
|
+
physics: latest.physics,
|
|
149
|
+
touch: latest.touch,
|
|
150
|
+
touchContainer: container,
|
|
151
|
+
debug: latest.debug,
|
|
152
|
+
seed: latest.seed,
|
|
153
|
+
fixedHz: latest.fixedHz,
|
|
154
|
+
pixelRatio: latest.pixelRatio,
|
|
155
|
+
antialias: latest.antialias,
|
|
156
|
+
pointer: latest.pointer,
|
|
157
|
+
...keyboard !== void 0 ? { keyboard } : {}
|
|
158
|
+
};
|
|
159
|
+
const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BdjpTHrW.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CZHROKcT.js").then((n) => n.n)).createGame2D(o)))(opts);
|
|
160
|
+
if (disposed) {
|
|
161
|
+
next.dispose();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
booted = next;
|
|
165
|
+
setGame(next);
|
|
166
|
+
propsRef.current.onReady?.(next);
|
|
167
|
+
})();
|
|
168
|
+
return () => {
|
|
169
|
+
disposed = true;
|
|
170
|
+
booted?.dispose();
|
|
171
|
+
setGame(null);
|
|
172
|
+
};
|
|
173
|
+
}, [
|
|
174
|
+
scene,
|
|
175
|
+
mode,
|
|
176
|
+
behaviors,
|
|
177
|
+
_gameFactory
|
|
178
|
+
]);
|
|
179
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
180
|
+
ref: containerRef,
|
|
181
|
+
className: props.className,
|
|
182
|
+
style: {
|
|
183
|
+
position: "relative",
|
|
184
|
+
width: "100%",
|
|
185
|
+
height: "100%",
|
|
186
|
+
...props.style
|
|
187
|
+
},
|
|
188
|
+
children: [/* @__PURE__ */ jsx("canvas", {
|
|
189
|
+
ref: canvasRef,
|
|
190
|
+
style: {
|
|
191
|
+
width: "100%",
|
|
192
|
+
height: "100%",
|
|
193
|
+
display: "block"
|
|
194
|
+
}
|
|
195
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
196
|
+
style: {
|
|
197
|
+
position: "absolute",
|
|
198
|
+
inset: 0,
|
|
199
|
+
pointerEvents: "none"
|
|
200
|
+
},
|
|
201
|
+
children: /* @__PURE__ */ jsx(GameContext.Provider, {
|
|
202
|
+
value: game,
|
|
203
|
+
children: game ? props.children : props.fallback ?? null
|
|
204
|
+
})
|
|
205
|
+
})]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
export { IncantoCanvas, useEngine, useGame, useNodeProp, useSignal };
|