incanto 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/incanto-editor.mjs +101 -28
- package/dist/2d.d.ts +28 -6
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +81 -7
- package/dist/3d.js +4 -4
- package/dist/{audit-C6rMyict.js → audit-C4kmDK0o.js} +16 -0
- package/dist/{behavior-BAc0erXF.d.ts → behavior-D_jMpFh8.d.ts} +117 -55
- package/dist/{create-game-CNKXGfpr.js → create-game-DqOjMBUS.js} +61 -48
- package/dist/{create-game-CbuLWorm.js → create-game-z5XaB1p5.js} +27 -15
- package/dist/debug-draw-BM3DsvtT.js +18 -0
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +8 -3
- package/dist/{duplicate-BgtFrFo4.js → duplicate-KRPtUtzl.js} +1 -1
- package/dist/{editor-switch-B0wB_DSr.d.ts → editor-switch-BJb-CWfA.d.ts} +12 -1
- package/dist/editor.js +1346 -1231
- package/dist/{register-C44aSduO.js → environment-presets--DigHNg4.js} +42 -11
- package/dist/{gameplay-L05WgWd1.js → gameplay-BftxM_It.js} +14 -5
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -6
- package/dist/{loader-B242FF6N.d.ts → loader-BYBrqTxP.d.ts} +1 -1
- package/dist/{loader-BZqOKfI2.js → loader-BlaRQGaA.js} +482 -6
- package/dist/net.d.ts +1 -1
- package/dist/net.js +3 -3
- package/dist/{pathfinding-BwD974Ss.d.ts → pathfinding-BwhqPD3i.d.ts} +1 -1
- package/dist/{physics-2d-CCVTrKOd.js → physics-2d-D9wquBvK.js} +30 -4
- package/dist/{physics-3d-BZZLtwJu.js → physics-3d-CnPygVGo.js} +41 -5
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-MelqEdza.js → register-CUY284Is.js} +3 -3
- package/dist/{register-BTg0EM7s.js → register-Dzkd6-os.js} +56 -380
- package/dist/{register-DWcWq4QG.js → register-tkR_8tWg.js} +4 -4
- package/dist/{registry-BVJ2HbCn.js → registry-CJdGpT2V.js} +6 -2
- package/dist/{editor-switch-CAKlJMIY.js → teardown-ByzfDPyu.js} +83 -4
- package/dist/test.d.ts +2 -7
- package/dist/test.js +43 -14
- package/dist/vite.d.ts +87 -1
- package/dist/vite.js +262 -3
- package/editor/assets/{agent8-DEVkEa3d.js → agent8-o27_Y1xN.js} +1 -1
- package/editor/assets/{debug-zGAtpDF0.js → debug-DfcWX3uW.js} +3 -2
- package/editor/assets/index-C9fb5QcT.js +10696 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/skills/incanto-editor.md +61 -8
- package/skills/incanto-physics-and-input.md +15 -0
- package/skills/incanto-verifying-your-game.md +68 -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/package.json +1 -1
- package/templates-app/tps-3d/vite.config.ts +7 -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-Bx4UtWYY.js +0 -10586
package/bin/incanto-editor.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* API (the page can only address *.scene.json files under the launch directory):
|
|
11
11
|
* GET /api/meta → { version, mode, root, input?, output? }
|
|
12
|
-
* GET /api/scenes → [{ rel, abs }]
|
|
12
|
+
* GET /api/scenes → [{ rel, abs, size, modified, scene?|notScene? }]
|
|
13
13
|
* POST /api/scenes → { path } creates a minimal scene (project mode)
|
|
14
14
|
* GET /api/scene[?file=] → scene JSON (single mode ignores ?file=)
|
|
15
15
|
* PUT /api/scene[?file=] → validates the scene shape, writes the file
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* request host. Bodies are capped at 8MB. Addressable paths are root-bounded
|
|
28
28
|
* and must end in .scene.json.
|
|
29
29
|
*/
|
|
30
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
31
31
|
import { createServer } from 'node:http';
|
|
32
32
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
@@ -213,9 +213,20 @@ function discoverScenes() {
|
|
|
213
213
|
for (const e of entries) {
|
|
214
214
|
if (e.isDirectory()) {
|
|
215
215
|
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), depth + 1);
|
|
216
|
-
} else if (e.name.endsWith('.
|
|
216
|
+
} else if (e.name.endsWith('.json')) {
|
|
217
217
|
const abs = join(dir, e.name);
|
|
218
|
-
|
|
218
|
+
// size + mtime are what the browser prints beside each row; a folder
|
|
219
|
+
// whose files are all "just now" is the one you were working in
|
|
220
|
+
let size = 0;
|
|
221
|
+
let modified = 0;
|
|
222
|
+
try {
|
|
223
|
+
const stat = statSync(abs);
|
|
224
|
+
size = stat.size;
|
|
225
|
+
modified = stat.mtimeMs;
|
|
226
|
+
} catch {
|
|
227
|
+
// listed without facts beats not listed at all
|
|
228
|
+
}
|
|
229
|
+
found.push({ rel: relative(ROOT, abs), abs, size, modified, ...inspectJson(abs, size) });
|
|
219
230
|
}
|
|
220
231
|
}
|
|
221
232
|
};
|
|
@@ -223,9 +234,65 @@ function discoverScenes() {
|
|
|
223
234
|
return found.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
224
235
|
}
|
|
225
236
|
|
|
226
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Is this JSON a scene? The file says so itself — `type: "scene"`, the same key
|
|
239
|
+
* the loader demands — so a row can be marked instead of guessed from its name.
|
|
240
|
+
*/
|
|
241
|
+
function sceneFacts(json) {
|
|
242
|
+
if (json === null || typeof json !== 'object' || Array.isArray(json)) {
|
|
243
|
+
return { ok: false, why: 'the file is JSON, but not a JSON object' };
|
|
244
|
+
}
|
|
245
|
+
if (json.type !== 'scene') {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
why:
|
|
249
|
+
json.type === undefined
|
|
250
|
+
? '"type" must be "scene", and this file has no "type"'
|
|
251
|
+
: `"type" must be "scene", not ${JSON.stringify(json.type)}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (json.format !== 1)
|
|
255
|
+
return { ok: false, why: `"format" must be 1, not ${JSON.stringify(json.format)}` };
|
|
256
|
+
if (typeof json.root !== 'object' || json.root === null) {
|
|
257
|
+
return { ok: false, why: '"root" must be a node object' };
|
|
258
|
+
}
|
|
259
|
+
const count = (n) =>
|
|
260
|
+
Array.isArray(n?.children) ? n.children.reduce((t, c) => t + count(c), 1) : 1;
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
name: typeof json.name === 'string' ? json.name : '',
|
|
264
|
+
// only when the file SAYS so — `dimension` is optional and has no default
|
|
265
|
+
...(json.dimension === '2d' || json.dimension === '3d' ? { dimension: json.dimension } : {}),
|
|
266
|
+
nodes: count(json.root),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function inspectJson(abs, size) {
|
|
271
|
+
if (size > 16_000_000) return { notScene: 'too large to check without opening it' };
|
|
272
|
+
let parsed;
|
|
273
|
+
try {
|
|
274
|
+
parsed = JSON.parse(readFileSync(abs, 'utf-8'));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return { notScene: `not valid JSON: ${String(error.message ?? error)}` };
|
|
277
|
+
}
|
|
278
|
+
const facts = sceneFacts(parsed);
|
|
279
|
+
return facts.ok
|
|
280
|
+
? {
|
|
281
|
+
scene: {
|
|
282
|
+
name: facts.name,
|
|
283
|
+
...(facts.dimension ? { dimension: facts.dimension } : {}),
|
|
284
|
+
nodes: facts.nodes,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
: { notScene: facts.why };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Root-bounded, .json-only — the browser offers every JSON and the LOAD decides,
|
|
292
|
+
* but this must never become a way to read .env or somebody's key file.
|
|
293
|
+
*/
|
|
227
294
|
function resolveSceneFile(rel) {
|
|
228
|
-
if (typeof rel !== 'string' || !rel.endsWith('.
|
|
295
|
+
if (typeof rel !== 'string' || !rel.endsWith('.json')) return null;
|
|
229
296
|
const abs = normalize(resolve(ROOT, rel));
|
|
230
297
|
if (abs !== ROOT && !abs.startsWith(ROOT + sep)) return null;
|
|
231
298
|
return abs;
|
|
@@ -276,18 +343,6 @@ function originAllowed(req) {
|
|
|
276
343
|
}
|
|
277
344
|
}
|
|
278
345
|
|
|
279
|
-
function isSceneShaped(json) {
|
|
280
|
-
return (
|
|
281
|
-
json !== null &&
|
|
282
|
-
typeof json === 'object' &&
|
|
283
|
-
json.format === 1 &&
|
|
284
|
-
json.type === 'scene' &&
|
|
285
|
-
typeof json.name === 'string' &&
|
|
286
|
-
json.root !== null &&
|
|
287
|
-
typeof json.root === 'object'
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
346
|
function readBody(req, res, onJson) {
|
|
292
347
|
const chunks = [];
|
|
293
348
|
let received = 0;
|
|
@@ -346,18 +401,23 @@ const server = createServer((req, res) => {
|
|
|
346
401
|
return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
|
|
347
402
|
}
|
|
348
403
|
readBody(req, res, (body) => {
|
|
349
|
-
|
|
404
|
+
// a path with no extension means the usual thing, so `levels/arena` works
|
|
405
|
+
const wanted =
|
|
406
|
+
typeof body?.path === 'string' && body.path && !body.path.endsWith('.json')
|
|
407
|
+
? `${body.path}.scene.json`
|
|
408
|
+
: body?.path;
|
|
409
|
+
const abs = resolveSceneFile(wanted);
|
|
350
410
|
if (!abs) {
|
|
351
411
|
return send(
|
|
352
412
|
res,
|
|
353
413
|
400,
|
|
354
|
-
JSON.stringify({ error: 'path must be a
|
|
414
|
+
JSON.stringify({ error: 'path must be a .json file inside the project' }),
|
|
355
415
|
);
|
|
356
416
|
}
|
|
357
417
|
if (existsSync(abs)) {
|
|
358
|
-
return send(res, 409, JSON.stringify({ error: `already exists: ${
|
|
418
|
+
return send(res, 409, JSON.stringify({ error: `already exists: ${wanted}` }));
|
|
359
419
|
}
|
|
360
|
-
const name = basename(abs).replace(/\.scene\.json$/, '') || 'Scene';
|
|
420
|
+
const name = basename(abs).replace(/\.scene\.json$|\.json$/, '') || 'Scene';
|
|
361
421
|
const scene = {
|
|
362
422
|
format: 1,
|
|
363
423
|
type: 'scene',
|
|
@@ -388,26 +448,39 @@ const server = createServer((req, res) => {
|
|
|
388
448
|
);
|
|
389
449
|
}
|
|
390
450
|
if (req.method === 'GET') {
|
|
451
|
+
let text;
|
|
452
|
+
let parsed;
|
|
391
453
|
try {
|
|
392
|
-
|
|
393
|
-
JSON.parse(text); // must at least be JSON before the page gets it
|
|
394
|
-
return send(res, 200, text);
|
|
454
|
+
text = readFileSync(target.input, 'utf-8');
|
|
455
|
+
parsed = JSON.parse(text); // must at least be JSON before the page gets it
|
|
395
456
|
} catch (error) {
|
|
396
457
|
return send(res, 422, JSON.stringify({ error: String(error.message ?? error) }));
|
|
397
458
|
}
|
|
459
|
+
// A JSON that is not a scene is refused WITH THE REASON and its contents
|
|
460
|
+
// never leave: "offer everything, explain the no" must not also mean
|
|
461
|
+
// "this server will read you any JSON in the project".
|
|
462
|
+
const facts = sceneFacts(parsed);
|
|
463
|
+
if (!facts.ok) {
|
|
464
|
+
return send(
|
|
465
|
+
res,
|
|
466
|
+
422,
|
|
467
|
+
JSON.stringify({ error: `not an incanto scene — ${facts.why}.`, notScene: true }),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return send(res, 200, text);
|
|
398
471
|
}
|
|
399
472
|
if (req.method === 'PUT') {
|
|
400
473
|
if (!originAllowed(req)) {
|
|
401
474
|
return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
|
|
402
475
|
}
|
|
403
476
|
readBody(req, res, (json, raw) => {
|
|
404
|
-
|
|
477
|
+
const shape = sceneFacts(json);
|
|
478
|
+
if (!shape.ok) {
|
|
405
479
|
return send(
|
|
406
480
|
res,
|
|
407
481
|
422,
|
|
408
482
|
JSON.stringify({
|
|
409
|
-
error:
|
|
410
|
-
'not an incanto scene — required: format: 1, type: "scene", name: string, root: object. The file was NOT written.',
|
|
483
|
+
error: `not an incanto scene — ${shape.why}. The file was NOT written.`,
|
|
411
484
|
}),
|
|
412
485
|
);
|
|
413
486
|
}
|
package/dist/2d.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
|
|
2
|
+
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
|
|
1
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as EditorSwitchOptions } from "./editor-switch-B0wB_DSr.js";
|
|
4
|
-
import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
|
|
4
|
+
import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
|
|
5
5
|
import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
|
|
6
6
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
7
7
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
@@ -24,9 +24,15 @@ interface SheetInfo {
|
|
|
24
24
|
* `"$key"`. The texture loader is injectable so tests run headless.
|
|
25
25
|
*/
|
|
26
26
|
declare class AssetStore2D {
|
|
27
|
+
private readonly engine;
|
|
27
28
|
private readonly entries;
|
|
28
29
|
private readonly loader;
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `engine` is optional so a bare store still constructs in tests. Given one,
|
|
32
|
+
* a texture that fails to load lands in `engine.log` — where the overlay and
|
|
33
|
+
* `runScript()` can see it — instead of only in the browser console.
|
|
34
|
+
*/
|
|
35
|
+
constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture, engine?: DiagnosticSink | null);
|
|
30
36
|
/** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
|
|
31
37
|
load(assets: Record<string, JsonObject>): void;
|
|
32
38
|
/** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
|
|
@@ -146,6 +152,12 @@ type Rapier = typeof RapierNs;
|
|
|
146
152
|
interface Physics2DOptions {
|
|
147
153
|
/** px/s², y-down. Default: scene `physics.gravity`, else [0, 980]. */
|
|
148
154
|
gravity?: [number, number];
|
|
155
|
+
/**
|
|
156
|
+
* Build the colliders and keep them on the tree, but never advance the world
|
|
157
|
+
* (default: `true`, i.e. simulate). See {@link Physics3DOptions.simulate} —
|
|
158
|
+
* the scene editor uses it to draw the REAL colliders without running them.
|
|
159
|
+
*/
|
|
160
|
+
simulate?: boolean;
|
|
149
161
|
}
|
|
150
162
|
/**
|
|
151
163
|
* Enable 2D physics for an engine. Dynamically imports Rapier (compat build,
|
|
@@ -158,7 +170,7 @@ declare function enablePhysics2D(engine: Engine, opts?: Physics2DOptions): Promi
|
|
|
158
170
|
*/
|
|
159
171
|
declare class Physics2D {
|
|
160
172
|
private readonly R;
|
|
161
|
-
|
|
173
|
+
readonly engine: Engine;
|
|
162
174
|
/** Render collider outlines in the GAME view (renderers pick this up). */
|
|
163
175
|
debugDraw: boolean;
|
|
164
176
|
/** Narrow those outlines to one node's subtree (see DebugLineSource). */
|
|
@@ -177,6 +189,8 @@ declare class Physics2D {
|
|
|
177
189
|
private lastScene;
|
|
178
190
|
constructor(R: Rapier, engine: Engine, opts?: Physics2DOptions);
|
|
179
191
|
/** @internal Driven by engine.fixedUpdated. */
|
|
192
|
+
/** Whether the solver runs at all. False = collider outlines only. */
|
|
193
|
+
private readonly simulate;
|
|
180
194
|
step(dt: number): void;
|
|
181
195
|
/** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */
|
|
182
196
|
moveAndSlide(node: CharacterBody2D): void;
|
|
@@ -210,6 +224,13 @@ declare class Physics2D {
|
|
|
210
224
|
private readonly jointSet;
|
|
211
225
|
private readonly joints;
|
|
212
226
|
private readonly bodySet;
|
|
227
|
+
/**
|
|
228
|
+
* Move every body to its node's current transform — outline mode only.
|
|
229
|
+
*
|
|
230
|
+
* Computed exactly as `ensureEntry` computes a body's first pose, so an
|
|
231
|
+
* outline drawn in the editor is the outline the running game draws.
|
|
232
|
+
*/
|
|
233
|
+
private poseFromTree;
|
|
213
234
|
private syncBodies;
|
|
214
235
|
private ensureEntry;
|
|
215
236
|
}
|
|
@@ -742,7 +763,8 @@ declare class Renderer2D {
|
|
|
742
763
|
private readonly worldCam;
|
|
743
764
|
private readonly uiCam;
|
|
744
765
|
private readonly engine;
|
|
745
|
-
|
|
766
|
+
/** The texture store. Public so `renderer.assets.status(key)` is reachable. */
|
|
767
|
+
readonly assets: AssetStore2D;
|
|
746
768
|
private readonly loadedAssetScenes;
|
|
747
769
|
private readonly disconnect;
|
|
748
770
|
private readonly canvas;
|
package/dist/2d.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-
|
|
3
|
-
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-
|
|
4
|
-
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-
|
|
2
|
+
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-z5XaB1p5.js";
|
|
3
|
+
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-tkR_8tWg.js";
|
|
4
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-D9wquBvK.js";
|
|
5
5
|
//#region src/2d/library-sprite.ts
|
|
6
6
|
/**
|
|
7
7
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
package/dist/3d.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
|
|
2
|
+
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
|
|
1
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as EditorSwitchOptions } from "./editor-switch-B0wB_DSr.js";
|
|
4
|
-
import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
|
|
4
|
+
import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
|
|
5
5
|
import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
|
|
6
|
-
import { n as PathGrid, s as SpatialPose } from "./pathfinding-
|
|
6
|
+
import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwhqPD3i.js";
|
|
7
7
|
import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Vector3, WebGLRenderer } from "three";
|
|
8
8
|
import { VRM } from "@pixiv/three-vrm";
|
|
9
9
|
import { Sky } from "three/examples/jsm/objects/Sky.js";
|
|
@@ -44,12 +44,28 @@ interface AnimationEntry {
|
|
|
44
44
|
* @pixiv/three-vrm (VRM 0.x facing fixed via rotateVRM0).
|
|
45
45
|
*/
|
|
46
46
|
declare class AssetStore3D {
|
|
47
|
+
private readonly engine;
|
|
47
48
|
private readonly models;
|
|
48
49
|
private readonly animations;
|
|
49
50
|
private readonly modelKeys;
|
|
50
51
|
private readonly animationKeys;
|
|
51
52
|
private readonly loader;
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* `engine` is optional only so a bare store still constructs in tests. Give it
|
|
55
|
+
* one and every load failure lands in `engine.log` — where the debug overlay,
|
|
56
|
+
* `runScript()` and any agent tool can see it. Without it a 404'd GLB was a
|
|
57
|
+
* console line and an invisible model.
|
|
58
|
+
*/
|
|
59
|
+
constructor(engine?: DiagnosticSink | null);
|
|
60
|
+
/**
|
|
61
|
+
* Every asset that failed, with the reason — the answer to "why is my model
|
|
62
|
+
* not there". Empty is the healthy case.
|
|
63
|
+
*/
|
|
64
|
+
errors(): Array<{
|
|
65
|
+
ref: string;
|
|
66
|
+
url: string;
|
|
67
|
+
error: string;
|
|
68
|
+
}>;
|
|
53
69
|
/** Map a scene's `assets` header. */
|
|
54
70
|
load(assets: JsonObject): void;
|
|
55
71
|
/** Animation `$key`s declared by the scene (editor dropdowns). */
|
|
@@ -185,6 +201,13 @@ declare class Node3D extends Node {
|
|
|
185
201
|
* back to a local Y by subtracting the parents' offset.
|
|
186
202
|
*/
|
|
187
203
|
applyGroundSnap(): void;
|
|
204
|
+
/**
|
|
205
|
+
* @internal The backing object IF it has been built — never builds one.
|
|
206
|
+
*
|
|
207
|
+
* `_ensureObject3D()` is the wrong call during teardown: it allocates a mesh
|
|
208
|
+
* for a node that never rendered, purely so the next line can dispose it.
|
|
209
|
+
*/
|
|
210
|
+
protected get _builtObject3D(): Object3D | null;
|
|
188
211
|
/** @internal The backing three object (lazily created). */
|
|
189
212
|
_ensureObject3D(): Object3D;
|
|
190
213
|
/** @internal Override point for subclasses (mesh, camera, light…). */
|
|
@@ -269,6 +292,20 @@ type Rapier = typeof RapierNs;
|
|
|
269
292
|
interface Physics3DOptions {
|
|
270
293
|
/** m/s², y-up. Default: scene `physics.gravity`, else [0, -9.81, 0]. */
|
|
271
294
|
gravity?: [number, number, number];
|
|
295
|
+
/**
|
|
296
|
+
* Build the colliders and keep them on the tree, but never advance the world
|
|
297
|
+
* (default: `true`, i.e. simulate).
|
|
298
|
+
*
|
|
299
|
+
* This exists for the scene editor. "Show colliders" there used to hand-draw
|
|
300
|
+
* wireframes from the `collider` JSON — which cannot show what the engine
|
|
301
|
+
* actually creates, because the shape is not always in the JSON (a mesh-fitted
|
|
302
|
+
* body, a scatter's hulls, a character capsule) and the drawing ignored
|
|
303
|
+
* rotation and scale. With `simulate: false` the editor gets the SAME lines
|
|
304
|
+
* play mode gets, from the same Rapier world, without anything falling over
|
|
305
|
+
* the moment you look at it: bodies are posed FROM the tree each tick and the
|
|
306
|
+
* solver never runs.
|
|
307
|
+
*/
|
|
308
|
+
simulate?: boolean;
|
|
272
309
|
}
|
|
273
310
|
/**
|
|
274
311
|
* Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
|
|
@@ -279,7 +316,7 @@ declare function enablePhysics3D(engine: Engine, opts?: Physics3DOptions): Promi
|
|
|
279
316
|
/** Per-engine 3D physics world. Same contract as Physics2D. */
|
|
280
317
|
declare class Physics3D {
|
|
281
318
|
private readonly R;
|
|
282
|
-
|
|
319
|
+
readonly engine: Engine;
|
|
283
320
|
/** Render collider outlines in the GAME view (renderers pick this up). */
|
|
284
321
|
debugDraw: boolean;
|
|
285
322
|
/** Narrow those outlines to one node's subtree (see DebugLineSource). */
|
|
@@ -297,8 +334,20 @@ declare class Physics3D {
|
|
|
297
334
|
private readonly optsGravity;
|
|
298
335
|
private lastScene;
|
|
299
336
|
constructor(R: Rapier, engine: Engine, opts?: Physics3DOptions);
|
|
337
|
+
/** Whether the solver runs at all. False = collider outlines only. */
|
|
338
|
+
private readonly simulate;
|
|
300
339
|
/** @internal Driven by engine.fixedUpdated. */
|
|
301
340
|
step(dt: number): void;
|
|
341
|
+
/**
|
|
342
|
+
* Move every body to its node's current transform.
|
|
343
|
+
*
|
|
344
|
+
* Only used in outline mode: normally physics owns the pose and writes it
|
|
345
|
+
* INTO the node, so nothing re-reads the tree. Deliberately computed the same
|
|
346
|
+
* way `ensureEntry` computes a body's initial pose — world position, node-local
|
|
347
|
+
* euler — so an outline in the editor is the outline the game will draw, right
|
|
348
|
+
* down to the places where that pair disagrees with a rotated parent.
|
|
349
|
+
*/
|
|
350
|
+
private poseFromTree;
|
|
302
351
|
/** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
|
|
303
352
|
moveAndSlide(node: CharacterBody3D): void;
|
|
304
353
|
/** Rapier debug segments (meters). Null while debugDraw is off. */
|
|
@@ -462,6 +511,14 @@ interface CreateGame3DOptions {
|
|
|
462
511
|
interface GameRenderer {
|
|
463
512
|
dispose(): void;
|
|
464
513
|
stats?(): RendererStats;
|
|
514
|
+
/** Loaded models/animations — `assets.errors()` answers "why is my model missing". */
|
|
515
|
+
assets?: {
|
|
516
|
+
errors(): Array<{
|
|
517
|
+
ref: string;
|
|
518
|
+
url: string;
|
|
519
|
+
error: string;
|
|
520
|
+
}>;
|
|
521
|
+
};
|
|
465
522
|
}
|
|
466
523
|
interface Game3D {
|
|
467
524
|
engine: Engine;
|
|
@@ -480,6 +537,18 @@ interface Game3D {
|
|
|
480
537
|
/** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
|
|
481
538
|
stats(): GameStats;
|
|
482
539
|
/**
|
|
540
|
+
* Assets that failed to load, with the reason.
|
|
541
|
+
*
|
|
542
|
+
* "The model just never appeared" used to be unanswerable from outside: the
|
|
543
|
+
* store was private on the renderer and the 404 went to the console only.
|
|
544
|
+
* Check this — and `stats().errors` — before assuming a scene is fine.
|
|
545
|
+
*/
|
|
546
|
+
assetErrors(): Array<{
|
|
547
|
+
ref: string;
|
|
548
|
+
url: string;
|
|
549
|
+
error: string;
|
|
550
|
+
}>;
|
|
551
|
+
/**
|
|
483
552
|
* Turn this page into the scene editor, right where the player is standing —
|
|
484
553
|
* what the ☰ debug menu's "edit this scene" calls. Bind it to your own key or
|
|
485
554
|
* button if you want the switch without the overlay.
|
|
@@ -2958,7 +3027,12 @@ declare class Renderer3D {
|
|
|
2958
3027
|
private readonly engine;
|
|
2959
3028
|
private readonly disconnect;
|
|
2960
3029
|
private readonly canvas;
|
|
2961
|
-
|
|
3030
|
+
/**
|
|
3031
|
+
* The model/animation store. Public so a game (and an agent's tool) can ask
|
|
3032
|
+
* `renderer.assets.errors()` why a model is not there — it used to be private,
|
|
3033
|
+
* so a 404'd GLB was unanswerable from outside.
|
|
3034
|
+
*/
|
|
3035
|
+
readonly assets: AssetStore3D;
|
|
2962
3036
|
private readonly ownsAssets;
|
|
2963
3037
|
private readonly loadedAssetScenes;
|
|
2964
3038
|
/** The scene whose GPU programs have been pre-compiled (warm-up, see render). */
|
package/dist/3d.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-
|
|
3
|
-
import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-
|
|
4
|
-
import { A as
|
|
2
|
+
import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BftxM_It.js";
|
|
3
|
+
import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DqOjMBUS.js";
|
|
4
|
+
import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, V as movementState, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets--DigHNg4.js";
|
|
5
5
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
6
|
-
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-
|
|
6
|
+
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CnPygVGo.js";
|
|
7
7
|
//#region src/3d/environment-runtime.ts
|
|
8
8
|
/**
|
|
9
9
|
* Live environment editing — the renderer re-applies `scene.environment`
|
|
@@ -25,6 +25,19 @@ const HUD_WIDGETS = new Set([
|
|
|
25
25
|
"UiButton",
|
|
26
26
|
"UiDialogue"
|
|
27
27
|
]);
|
|
28
|
+
const LIGHT_TYPES = new Set([
|
|
29
|
+
"DirectionalLight3D",
|
|
30
|
+
"OmniLight3D",
|
|
31
|
+
"SpotLight3D"
|
|
32
|
+
]);
|
|
33
|
+
/** Does the scene header light anything by itself (sky, hdri, ambient, preset)? */
|
|
34
|
+
function isLit(environment) {
|
|
35
|
+
if (!environment) return false;
|
|
36
|
+
if (environment.preset !== void 0 || environment.sky !== void 0) return true;
|
|
37
|
+
if (environment.hdri !== void 0) return true;
|
|
38
|
+
const ambient = environment.ambient;
|
|
39
|
+
return typeof ambient?.intensity === "number" && ambient.intensity > 0;
|
|
40
|
+
}
|
|
28
41
|
/** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
|
|
29
42
|
function auditScene(scene) {
|
|
30
43
|
const warnings = [];
|
|
@@ -32,6 +45,7 @@ function auditScene(scene) {
|
|
|
32
45
|
if (!root) return warnings;
|
|
33
46
|
let currentCameras = 0;
|
|
34
47
|
let cameras = 0;
|
|
48
|
+
let lights = 0;
|
|
35
49
|
const walk = (node, path, insideHud, staticRoot) => {
|
|
36
50
|
const type = node.type ?? "";
|
|
37
51
|
const props = node.props ?? {};
|
|
@@ -39,6 +53,7 @@ function auditScene(scene) {
|
|
|
39
53
|
cameras += 1;
|
|
40
54
|
if (props.current === true) currentCameras += 1;
|
|
41
55
|
}
|
|
56
|
+
if (LIGHT_TYPES.has(type)) lights += 1;
|
|
42
57
|
if (BODY_TYPES.has(type)) {
|
|
43
58
|
const collider = props.collider;
|
|
44
59
|
if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
|
|
@@ -52,6 +67,7 @@ function auditScene(scene) {
|
|
|
52
67
|
walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null);
|
|
53
68
|
if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
|
|
54
69
|
if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
|
|
70
|
+
if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
|
|
55
71
|
return warnings;
|
|
56
72
|
}
|
|
57
73
|
//#endregion
|