incanto 0.48.0 → 0.50.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-model.mjs +13 -1
- package/bin/incanto-play.mjs +42 -17
- package/bin/incanto-playtest.mjs +7 -0
- package/bin/incanto-verify.mjs +10 -2
- package/dist/2d.d.ts +21 -12
- package/dist/2d.js +57 -3
- package/dist/3d.d.ts +38 -1
- package/dist/3d.js +114 -4
- package/dist/{create-game-CniOiWzN.js → create-game-BLDjy_PW.js} +5 -5
- package/dist/{create-game-akjPkFv5.js → create-game-DqqxEax1.js} +5 -5
- package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
- package/dist/{environment-presets-D1b0ydTS.js → environment-presets-XFuqu5jv.js} +15 -20
- package/dist/{gameplay-BQOeAid6.js → gameplay-DbaI313d.js} +96 -11
- package/dist/gameplay.d.ts +17 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +40 -2
- package/dist/index.js +7 -7
- package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
- package/dist/net.js +3 -3
- package/dist/{physics-2d-BmgXBNDB.js → physics-2d-_9VBOHn6.js} +4 -4
- package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DrpF5hcG.js} +9 -9
- package/dist/react.js +1 -1
- package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
- package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
- package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
- package/dist/{replay-CAphXMyM.d.ts → replay-DYdy1wb0.d.ts} +5 -1
- package/dist/{replay-DilbZgQI.js → replay-j-m6lJ4W.js} +23 -3
- package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
- package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
- package/dist/{src-Bl1Kire1.js → src-B3HrKuAi.js} +1 -1
- package/dist/{test-E4-otKqK.js → test-D16igj4C.js} +245 -38
- package/dist/test.d.ts +39 -7
- package/dist/test.js +2 -2
- package/dist/vite.js +2 -2
- package/editor/assets/{agent8-_007gPF8.js → agent8-t3kl5q9K.js} +1 -1
- package/editor/assets/{debug-0DI_MJaq.js → debug-Bu3eeAlO.js} +1 -1
- package/editor/assets/{index-B-6eYZEi.js → index-Df5g8ofT.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +10 -3
- package/skills/incanto-3d-character.md +13 -2
- package/skills/incanto-3d-models.md +40 -0
- package/skills/incanto-assets.md +15 -0
- package/skills/incanto-building-2d-games.md +57 -5
- package/skills/incanto-building-3d-games.md +7 -0
- package/skills/incanto-gameplay-behaviors.md +29 -1
- package/skills/incanto-hud.md +39 -0
- package/skills/incanto-node-reference.md +5 -3
- package/skills/incanto-physics-and-input.md +1 -1
- package/skills/incanto-playtesting.md +22 -4
- package/skills/incanto-verifying-your-game.md +14 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/village-quest-3d/package.json +1 -1
package/bin/incanto-model.mjs
CHANGED
|
@@ -353,7 +353,19 @@ if (args.json) {
|
|
|
353
353
|
// The verdict is the part you act on, so it goes last — closest to the
|
|
354
354
|
// prompt, where a reader stops.
|
|
355
355
|
out.push('', verdictText(verdict));
|
|
356
|
-
|
|
356
|
+
// A rig that takes the library clips gets the WHOLE character: the model on
|
|
357
|
+
// its own is a statue, and the steps from there (a dynamic body under the
|
|
358
|
+
// controller, the skin fitted to the capsule and dropped to its feet, the
|
|
359
|
+
// clips declared, the input actions that make anything read the keyboard)
|
|
360
|
+
// are documented in four different places and all needed at once.
|
|
361
|
+
if (verdict.characterJson) {
|
|
362
|
+
const { assets, input, node } = verdict.characterJson;
|
|
363
|
+
out.push(
|
|
364
|
+
'',
|
|
365
|
+
'paste a playable character into your scene:',
|
|
366
|
+
JSON.stringify({ assets, input, node }, null, 2),
|
|
367
|
+
);
|
|
368
|
+
} else if (verdict.sceneJson) {
|
|
357
369
|
const { assets, node } = verdict.sceneJson;
|
|
358
370
|
out.push(
|
|
359
371
|
'',
|
package/bin/incanto-play.mjs
CHANGED
|
@@ -62,8 +62,11 @@ wheel/capture/describe/framing/logs/quit), JSON-line responses on stdout.`);
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const out = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
65
|
-
|
|
65
|
+
let failures = 0;
|
|
66
|
+
const fail = (cmd, e) => {
|
|
67
|
+
failures += 1;
|
|
66
68
|
out({ ok: false, cmd, error: { code: e?.code ?? 'ERROR', message: e?.message ?? String(e) } });
|
|
69
|
+
};
|
|
67
70
|
|
|
68
71
|
const { createPlaySession } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
|
|
69
72
|
const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
@@ -204,21 +207,43 @@ function handle(line) {
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
if (args.commands) {
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
const text = readFileSync(resolve(args.commands), 'utf-8');
|
|
211
|
+
// A REPLAY, not a command script. `incanto-playtest` writes its failing runs
|
|
212
|
+
// as ReplayJson and the docs point this flag at them; reading one as text
|
|
213
|
+
// rejected every line as UNKNOWN_COMMAND and still exited 0, so the tool's
|
|
214
|
+
// whole payoff — "the failure comes back as a file you can watch" — silently
|
|
215
|
+
// did nothing.
|
|
216
|
+
const recording = text.trimStart().startsWith('{') ? JSON.parse(text) : null;
|
|
217
|
+
if (recording?.replay) {
|
|
218
|
+
incanto.replay(session.engine, recording);
|
|
219
|
+
out({
|
|
220
|
+
ok: true,
|
|
221
|
+
cmd: 'replay',
|
|
222
|
+
frames: recording.ticks.length,
|
|
223
|
+
events: recording.events.length,
|
|
224
|
+
});
|
|
225
|
+
out({ ok: true, cmd: 'describe', text: session.describe() });
|
|
226
|
+
session.dispose();
|
|
227
|
+
process.exitCode = 0;
|
|
228
|
+
} else {
|
|
229
|
+
for (const line of text.split('\n')) {
|
|
230
|
+
if (!handle(line)) break;
|
|
231
|
+
}
|
|
232
|
+
session.dispose();
|
|
233
|
+
// A script whose every line was rejected must not report success: that is
|
|
234
|
+
// exactly how a replay pointed at the wrong reader stayed invisible.
|
|
235
|
+
process.exitCode = failures > 0 ? 1 : 0;
|
|
209
236
|
}
|
|
210
|
-
|
|
211
|
-
process.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
} else {
|
|
238
|
+
const rl = createInterface({ input: process.stdin });
|
|
239
|
+
rl.on('line', (line) => {
|
|
240
|
+
if (!handle(line)) {
|
|
241
|
+
rl.close();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
rl.on('close', () => {
|
|
246
|
+
session.dispose();
|
|
218
247
|
process.exit(0);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
rl.on('close', () => {
|
|
222
|
-
session.dispose();
|
|
223
|
-
process.exit(0);
|
|
224
|
-
});
|
|
248
|
+
});
|
|
249
|
+
}
|
package/bin/incanto-playtest.mjs
CHANGED
|
@@ -34,6 +34,8 @@ function parseArgs(argv) {
|
|
|
34
34
|
else if (a === '--runs') args.runs = Number(argv[++i]);
|
|
35
35
|
else if (a === '--seconds') args.seconds = Number(argv[++i]);
|
|
36
36
|
else if (a === '--seed') args.seed = Number(argv[++i]);
|
|
37
|
+
else if (a === '--fall-below') args.fallBelow = Number(argv[++i]);
|
|
38
|
+
else if (a === '--reach-radius') args.reachRadius = Number(argv[++i]);
|
|
37
39
|
else if (a === '--out') args.out = argv[++i];
|
|
38
40
|
else if (a === '--no-replays') args.noReplays = true;
|
|
39
41
|
else if (a === '--json') args.json = true;
|
|
@@ -53,6 +55,9 @@ if (args.help || !args.scene) {
|
|
|
53
55
|
--runs N independent seeded runs (default 20)
|
|
54
56
|
--seconds N simulated seconds per run before calling it stuck (default 60)
|
|
55
57
|
--seed N first seed; run i uses seed+i (default 1)
|
|
58
|
+
--fall-below Y left the world past this y (default: 50 under the spawn in
|
|
59
|
+
3D, 1000 px under it in 2D — 2D counts DOWN the screen)
|
|
60
|
+
--reach-radius R how close counts as "reached" (default 2 in 3D, 32 px in 2D)
|
|
56
61
|
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
57
62
|
--out DIR where failing replays go (default .incanto/playtest)
|
|
58
63
|
--no-replays report only, write nothing
|
|
@@ -101,6 +106,8 @@ try {
|
|
|
101
106
|
seed: args.seed,
|
|
102
107
|
behaviors,
|
|
103
108
|
stubMissingBehaviors: !args.behaviors,
|
|
109
|
+
...(Number.isFinite(args.fallBelow) ? { fallBelow: args.fallBelow } : {}),
|
|
110
|
+
...(Number.isFinite(args.reachRadius) ? { reachRadius: args.reachRadius } : {}),
|
|
104
111
|
// sub-scenes resolve relative to the scene file, as the loader does
|
|
105
112
|
resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
|
|
106
113
|
});
|
package/bin/incanto-verify.mjs
CHANGED
|
@@ -50,10 +50,18 @@ NOT MEASURED rather than failed — a missing measurement is not a broken game.`
|
|
|
50
50
|
process.exit(0);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The scene to verify: the one named, or the only one that can be found.
|
|
55
|
+
*
|
|
56
|
+
* ALWAYS the same shape. It used to return a bare string for a named scene and
|
|
57
|
+
* an object for a search, and the caller destructures — so `{ scene } = "a.json"`
|
|
58
|
+
* was `undefined` and naming your scene was the one way to make the whole
|
|
59
|
+
* ladder do nothing. It only ever worked when you named nothing, which is how
|
|
60
|
+
* it was run while being written.
|
|
61
|
+
*/
|
|
54
62
|
function findScene() {
|
|
55
63
|
const named = argv.find((a) => !a.startsWith('-') && a.endsWith('.json'));
|
|
56
|
-
if (named) return named;
|
|
64
|
+
if (named) return { scene: named, candidates: [named] };
|
|
57
65
|
const found = [];
|
|
58
66
|
const walk = (dir, depth) => {
|
|
59
67
|
if (depth > 4 || found.length > 8) return;
|
package/dist/2d.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engi
|
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
|
|
3
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
4
|
import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
|
|
5
|
+
import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
|
|
5
6
|
import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
|
|
6
7
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
7
8
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
@@ -135,10 +136,16 @@ declare class RigidBody2D extends PhysicsBody2D {
|
|
|
135
136
|
declare class CharacterBody2D extends PhysicsBody2D {
|
|
136
137
|
static override readonly typeName: string;
|
|
137
138
|
static override readonly props: PropSchema;
|
|
139
|
+
/**
|
|
140
|
+
* Renamed to match the 3D body, where `snapToGround` had to give the name
|
|
141
|
+
* back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is
|
|
142
|
+
* ambiguous here and old scenes keep loading unchanged.
|
|
143
|
+
*/
|
|
144
|
+
static readonly propAliases: Record<string, string>;
|
|
138
145
|
/** px/s, y-down (up = -y). */
|
|
139
146
|
velocity: number[];
|
|
140
147
|
/** Keep contact on slopes/steps while not moving upward. */
|
|
141
|
-
|
|
148
|
+
stickToGround: boolean;
|
|
142
149
|
/** Max climbable slope angle. */
|
|
143
150
|
slopeLimitDeg: number;
|
|
144
151
|
/** @internal Updated by Physics2D.moveAndSlide. */
|
|
@@ -373,6 +380,11 @@ interface SpriteFromLibraryResult {
|
|
|
373
380
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
|
374
381
|
* declaration + AnimatedSprite2D props. `autoplay` defaults to the first
|
|
375
382
|
* LOOPING animation (idle-like), falling back to the first one.
|
|
383
|
+
*
|
|
384
|
+
* The animation map also gets ALIASES for the movement states a character
|
|
385
|
+
* controller emits but the sheet does not name (`run` → `move`, `fall` →
|
|
386
|
+
* `idle`), so the converted props and the documented
|
|
387
|
+
* `movementStateChanged → play` wiring work together out of the box.
|
|
376
388
|
*/
|
|
377
389
|
declare function spriteFromLibraryMeta(meta: unknown, opts: {
|
|
378
390
|
url: string;
|
|
@@ -411,15 +423,6 @@ declare class Sprite2D extends Node2D {
|
|
|
411
423
|
}
|
|
412
424
|
//#endregion
|
|
413
425
|
//#region src/2d/nodes/animated-sprite-2d.d.ts
|
|
414
|
-
interface AnimationDef {
|
|
415
|
-
/**
|
|
416
|
-
* Two numbers = inclusive [start, end] frame RANGE.
|
|
417
|
-
* Any other length = explicit frame list.
|
|
418
|
-
*/
|
|
419
|
-
frames: number[];
|
|
420
|
-
fps: number;
|
|
421
|
-
loop?: boolean;
|
|
422
|
-
}
|
|
423
426
|
/**
|
|
424
427
|
* Spritesheet animation: pure-JSON animation map, frame selection via a UV
|
|
425
428
|
* window on the node's own texture clone. Frames advance in `update(dt)` —
|
|
@@ -431,7 +434,7 @@ declare class AnimatedSprite2D extends Sprite2D {
|
|
|
431
434
|
static override readonly props: PropSchema;
|
|
432
435
|
/** `'$assetKey'` of a spritesheet asset. */
|
|
433
436
|
sheet: string;
|
|
434
|
-
animations: Record<string,
|
|
437
|
+
animations: Record<string, AnimationEntry>;
|
|
435
438
|
autoplay: string;
|
|
436
439
|
playing: boolean;
|
|
437
440
|
/** Absolute frame index within the sheet. */
|
|
@@ -440,8 +443,14 @@ declare class AnimatedSprite2D extends Sprite2D {
|
|
|
440
443
|
private frameList;
|
|
441
444
|
private frameIndex;
|
|
442
445
|
private frameTime;
|
|
446
|
+
private currentDef;
|
|
443
447
|
private ownTexture;
|
|
444
448
|
private loadedSheetRef;
|
|
449
|
+
/**
|
|
450
|
+
* `currentAnimation` reports the clip that is RUNNING, not the name asked
|
|
451
|
+
* for: an alias is a rename, and a state machine that reads back `fall` while
|
|
452
|
+
* the idle frames play is describing something that is not happening.
|
|
453
|
+
*/
|
|
445
454
|
play(name: string): void;
|
|
446
455
|
stop(): void;
|
|
447
456
|
override onReady(): void;
|
|
@@ -920,4 +929,4 @@ declare function parseCells(cells: readonly (string | readonly number[])[], lege
|
|
|
920
929
|
*/
|
|
921
930
|
declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set<number>): TileRect[];
|
|
922
931
|
//#endregion
|
|
923
|
-
export { AnimatedSprite2D,
|
|
932
|
+
export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
|
package/dist/2d.js
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
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-BLDjy_PW.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-BNPZYJmd.js";
|
|
4
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-_9VBOHn6.js";
|
|
5
5
|
//#region src/2d/library-sprite.ts
|
|
6
6
|
/**
|
|
7
|
+
* What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
|
|
8
|
+
* a library sheet that answers for it.
|
|
9
|
+
*
|
|
10
|
+
* The two vocabularies never met: the controller emits `run`, every library
|
|
11
|
+
* sheet calls it `move`, and no sheet anywhere ships a `fall` — so the wiring
|
|
12
|
+
* the 2D skill documents in one line (`movementStateChanged → play`) asked for
|
|
13
|
+
* animations that do not exist, on artwork from the engine's own catalog.
|
|
14
|
+
*
|
|
15
|
+
* These are ALIASES, so they read as what they are, and the author can point
|
|
16
|
+
* any of them somewhere better.
|
|
17
|
+
*/
|
|
18
|
+
const CONTROLLER_STATES = [
|
|
19
|
+
["run", [
|
|
20
|
+
"run",
|
|
21
|
+
"move",
|
|
22
|
+
"walk"
|
|
23
|
+
]],
|
|
24
|
+
["jump", ["jump", "idle"]],
|
|
25
|
+
["fall", [
|
|
26
|
+
"fall",
|
|
27
|
+
"jump",
|
|
28
|
+
"idle"
|
|
29
|
+
]],
|
|
30
|
+
["wallSlide", ["wallSlide", "idle"]],
|
|
31
|
+
["dash", [
|
|
32
|
+
"dash",
|
|
33
|
+
"run",
|
|
34
|
+
"move",
|
|
35
|
+
"idle"
|
|
36
|
+
]]
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Fill in the states the sheet does not name, and touch nothing it does.
|
|
40
|
+
*
|
|
41
|
+
* Silent when there is no `idle` to fall back on: a spinning-coin sheet is not
|
|
42
|
+
* a character, and inventing movement states for it would put clips in the map
|
|
43
|
+
* that mean nothing.
|
|
44
|
+
*/
|
|
45
|
+
function addControllerStates(animations) {
|
|
46
|
+
const real = new Set(Object.keys(animations));
|
|
47
|
+
if (!real.has("idle")) return;
|
|
48
|
+
for (const [state, candidates] of CONTROLLER_STATES) {
|
|
49
|
+
if (real.has(state)) continue;
|
|
50
|
+
const target = candidates.find((c) => c !== state && real.has(c));
|
|
51
|
+
if (target) animations[state] = target;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
7
55
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
|
8
56
|
* declaration + AnimatedSprite2D props. `autoplay` defaults to the first
|
|
9
57
|
* LOOPING animation (idle-like), falling back to the first one.
|
|
58
|
+
*
|
|
59
|
+
* The animation map also gets ALIASES for the movement states a character
|
|
60
|
+
* controller emits but the sheet does not name (`run` → `move`, `fall` →
|
|
61
|
+
* `idle`), so the converted props and the documented
|
|
62
|
+
* `movementStateChanged → play` wiring work together out of the box.
|
|
10
63
|
*/
|
|
11
64
|
function spriteFromLibraryMeta(meta, opts) {
|
|
12
65
|
const m = meta;
|
|
@@ -25,6 +78,7 @@ function spriteFromLibraryMeta(meta, opts) {
|
|
|
25
78
|
firstName ??= name;
|
|
26
79
|
if (loop && firstLooping === null) firstLooping = name;
|
|
27
80
|
}
|
|
81
|
+
addControllerStates(animations);
|
|
28
82
|
const autoplay = opts.autoplay ?? firstLooping ?? firstName;
|
|
29
83
|
return {
|
|
30
84
|
asset: {
|
package/dist/3d.d.ts
CHANGED
|
@@ -370,9 +370,27 @@ declare class CharacterBody3D extends PhysicsBody3D {
|
|
|
370
370
|
static override readonly props: PropSchema;
|
|
371
371
|
/** m/s, y-up (up = +y). */
|
|
372
372
|
velocity: number[];
|
|
373
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Hold the capsule against the surface while it moves, so walking down a
|
|
375
|
+
* slope does not become a series of small falls.
|
|
376
|
+
*
|
|
377
|
+
* Named apart from `Node3D.snapToGround` (which PLACES a node on the ground
|
|
378
|
+
* at load) because sharing the name silently broke both: the boolean shadowed
|
|
379
|
+
* the placement prop, its `true` default made the placement pass teleport
|
|
380
|
+
* every character body in a terrain scene, and a lift value was rejected
|
|
381
|
+
* outright — so the only escape from the teleport also turned this off.
|
|
382
|
+
*/
|
|
383
|
+
stickToGround: boolean;
|
|
374
384
|
/** Max climbable slope angle. */
|
|
375
385
|
slopeLimitDeg: number;
|
|
386
|
+
/**
|
|
387
|
+
* `snapToGround` on a character body is ambiguous, so it is refused.
|
|
388
|
+
*
|
|
389
|
+
* A boolean here used to mean the ground-stick, and as placement it means
|
|
390
|
+
* "leave y alone" — which is already the default. Rather than pick one and
|
|
391
|
+
* silently do the other thing to an existing scene, say which prop was meant.
|
|
392
|
+
*/
|
|
393
|
+
override onReady(): void;
|
|
376
394
|
/** @internal Updated by Physics3D.moveAndSlide. */
|
|
377
395
|
_grounded: boolean;
|
|
378
396
|
moveAndSlide(): void;
|
|
@@ -1006,6 +1024,25 @@ interface ModelVerdict {
|
|
|
1006
1024
|
/** Clip names baked into the file itself (playable by name, no asset entry). */
|
|
1007
1025
|
embeddedClips: string[];
|
|
1008
1026
|
sceneJson: SceneJson | null;
|
|
1027
|
+
/**
|
|
1028
|
+
* The whole playable character — body, controller, skin, clips and input —
|
|
1029
|
+
* for a rig that can take the library's locomotion clips. Null for a prop.
|
|
1030
|
+
*
|
|
1031
|
+
* The model on its own is a statue, and every step from there is documented
|
|
1032
|
+
* somewhere different: the controller must sit under a dynamic body, the skin
|
|
1033
|
+
* must be sized to the capsule and dropped to its feet, the clips must be
|
|
1034
|
+
* declared, and the input actions must exist or nothing reads the keyboard.
|
|
1035
|
+
*/
|
|
1036
|
+
characterJson: CharacterJson | null;
|
|
1037
|
+
}
|
|
1038
|
+
interface CharacterJson {
|
|
1039
|
+
assets: Record<string, {
|
|
1040
|
+
type: string;
|
|
1041
|
+
url: string;
|
|
1042
|
+
}>;
|
|
1043
|
+
/** The actions `CharacterController3D` reads — without them it never moves. */
|
|
1044
|
+
input: Record<string, unknown>;
|
|
1045
|
+
node: Record<string, unknown>;
|
|
1009
1046
|
}
|
|
1010
1047
|
interface VerdictTarget {
|
|
1011
1048
|
url: string;
|
package/dist/3d.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
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-
|
|
2
|
-
import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-
|
|
3
|
-
import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-
|
|
1
|
+
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-DbaI313d.js";
|
|
2
|
+
import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-XFuqu5jv.js";
|
|
3
|
+
import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-DqqxEax1.js";
|
|
4
4
|
import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.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-DrpF5hcG.js";
|
|
7
7
|
//#region src/3d/model-verdict.ts
|
|
8
8
|
/** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
|
|
9
9
|
const MIXAMO = /^mixamorig[:_]?/i;
|
|
@@ -25,6 +25,115 @@ function rigOf(facts) {
|
|
|
25
25
|
if (facts.nodes.some((n) => MIXAMO.test(n.name))) return "mixamorig";
|
|
26
26
|
return facts.skins > 0 ? "other" : "none";
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The locomotion clips every mixamorig character can play as they are.
|
|
30
|
+
*
|
|
31
|
+
* They are not art you go looking for — they are the rig's vocabulary, they
|
|
32
|
+
* retarget onto anything mixamo-shaped (and through the humanoid map onto a
|
|
33
|
+
* VRM), and this repo's own examples reference them 29 times. Nothing shipped
|
|
34
|
+
* ever told an agent where they live, so a character found through an asset MCP
|
|
35
|
+
* arrived with no way to walk.
|
|
36
|
+
*
|
|
37
|
+
* Keyed by the states `CharacterController3D` emits, so the map below IS the
|
|
38
|
+
* controller's `animations` prop.
|
|
39
|
+
*/
|
|
40
|
+
const CLIP_BASE = "https://agent8-games.verse8.io/assets/3d/animations/mixamorig";
|
|
41
|
+
const LOCOMOTION_CLIPS = {
|
|
42
|
+
idle: `${CLIP_BASE}/idle-00.glb`,
|
|
43
|
+
walk: `${CLIP_BASE}/walk.glb`,
|
|
44
|
+
run: `${CLIP_BASE}/run-medium.glb`,
|
|
45
|
+
fastRun: `${CLIP_BASE}/run-fast.glb`,
|
|
46
|
+
airborne: `${CLIP_BASE}/jump.glb`
|
|
47
|
+
};
|
|
48
|
+
/** The capsule the shipped templates use, and the skin fitted to it. */
|
|
49
|
+
const BODY = {
|
|
50
|
+
radius: .34,
|
|
51
|
+
height: 1
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A found character, as a thing that walks.
|
|
55
|
+
*
|
|
56
|
+
* The model alone draws a statue: `CharacterController3D` must sit under a
|
|
57
|
+
* dynamic body, the skin has to be sized to the capsule and dropped to its
|
|
58
|
+
* feet, the clips have to be declared, and the input actions have to exist or
|
|
59
|
+
* nothing reads the keyboard. Every one of those is a documented step and all
|
|
60
|
+
* of them are needed at once, which is exactly the shape of thing worth
|
|
61
|
+
* printing rather than describing.
|
|
62
|
+
*/
|
|
63
|
+
function characterJson(target) {
|
|
64
|
+
const assets = { [target.key]: {
|
|
65
|
+
type: "model",
|
|
66
|
+
url: target.url
|
|
67
|
+
} };
|
|
68
|
+
const animations = {};
|
|
69
|
+
for (const [state, url] of Object.entries(LOCOMOTION_CLIPS)) {
|
|
70
|
+
const key = `anims/${state}`;
|
|
71
|
+
assets[key] = {
|
|
72
|
+
type: "animation",
|
|
73
|
+
url
|
|
74
|
+
};
|
|
75
|
+
animations[state] = `$${key}`;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
assets,
|
|
79
|
+
input: {
|
|
80
|
+
move: {
|
|
81
|
+
type: "vector2",
|
|
82
|
+
keys: {
|
|
83
|
+
up: ["KeyW"],
|
|
84
|
+
down: ["KeyS"],
|
|
85
|
+
left: ["KeyA"],
|
|
86
|
+
right: ["KeyD"]
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
jump: {
|
|
90
|
+
type: "button",
|
|
91
|
+
keys: ["Space"]
|
|
92
|
+
},
|
|
93
|
+
sprint: {
|
|
94
|
+
type: "button",
|
|
95
|
+
keys: ["ShiftLeft"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
node: {
|
|
99
|
+
name: "Player",
|
|
100
|
+
type: "RigidBody3D",
|
|
101
|
+
groups: ["player"],
|
|
102
|
+
props: {
|
|
103
|
+
fixedRotation: true,
|
|
104
|
+
friction: 0,
|
|
105
|
+
collider: {
|
|
106
|
+
shape: "capsule",
|
|
107
|
+
radius: BODY.radius,
|
|
108
|
+
height: BODY.height
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
children: [{
|
|
112
|
+
name: "Controller",
|
|
113
|
+
type: "CharacterController3D",
|
|
114
|
+
props: {
|
|
115
|
+
view: "free",
|
|
116
|
+
coyoteSeconds: .12,
|
|
117
|
+
jumpBufferSeconds: .15,
|
|
118
|
+
animations
|
|
119
|
+
}
|
|
120
|
+
}, {
|
|
121
|
+
name: "Skin",
|
|
122
|
+
type: "ModelInstance3D",
|
|
123
|
+
props: {
|
|
124
|
+
model: `$${target.key}`,
|
|
125
|
+
targetHeight: BODY.height + 2 * BODY.radius,
|
|
126
|
+
position: [
|
|
127
|
+
0,
|
|
128
|
+
-(BODY.height / 2 + BODY.radius),
|
|
129
|
+
0
|
|
130
|
+
],
|
|
131
|
+
castShadow: true
|
|
132
|
+
}
|
|
133
|
+
}]
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
28
137
|
function modelVerdict(facts, target) {
|
|
29
138
|
const rig = rigOf(facts);
|
|
30
139
|
const kind = facts.meshes === 0 && facts.animations.length > 0 ? "animation" : "model";
|
|
@@ -51,6 +160,7 @@ function modelVerdict(facts, target) {
|
|
|
51
160
|
originOffset: offset,
|
|
52
161
|
groundedAtOrigin: grounded,
|
|
53
162
|
embeddedClips: facts.animations.map((a) => a.name),
|
|
163
|
+
characterJson: target && kind === "model" && (rig === "mixamorig" || rig === "vrm-humanoid") ? characterJson(target) : null,
|
|
54
164
|
sceneJson: target ? {
|
|
55
165
|
assets: { [target.key]: {
|
|
56
166
|
type: kind,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-
|
|
3
|
-
import { h as Engine, m as AudioPlayer } from "./register-
|
|
2
|
+
import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-DwazzlQb.js";
|
|
3
|
+
import { h as Engine, m as AudioPlayer } from "./register-uvaZj1KX.js";
|
|
4
4
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
5
5
|
import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
|
|
6
6
|
import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
|
|
7
|
-
import { n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
-
import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-
|
|
7
|
+
import { n as registerGameplayBehaviors } from "./gameplay-DbaI313d.js";
|
|
8
|
+
import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BNPZYJmd.js";
|
|
9
9
|
import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
|
|
10
|
-
import { n as enablePhysics2D } from "./physics-2d-
|
|
10
|
+
import { n as enablePhysics2D } from "./physics-2d-_9VBOHn6.js";
|
|
11
11
|
import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
|
|
12
12
|
//#region src/2d/assets.ts
|
|
13
13
|
/**
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-
|
|
3
|
-
import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-
|
|
2
|
+
import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-DwazzlQb.js";
|
|
3
|
+
import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-uvaZj1KX.js";
|
|
4
4
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
5
5
|
import { r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
|
|
6
6
|
import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
|
|
7
7
|
import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
|
|
8
|
-
import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-
|
|
8
|
+
import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-DbaI313d.js";
|
|
9
9
|
import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
|
|
10
|
-
import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-
|
|
10
|
+
import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-XFuqu5jv.js";
|
|
11
11
|
import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
|
|
12
|
-
import { n as enablePhysics3D } from "./physics-3d-
|
|
12
|
+
import { n as enablePhysics3D } from "./physics-3d-DrpF5hcG.js";
|
|
13
13
|
import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
|
|
14
14
|
import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
|
|
15
15
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { t as registerCoreNodes } from "./register-
|
|
1
|
+
import { t as registerCoreNodes } from "./register-uvaZj1KX.js";
|
|
2
2
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
3
3
|
import { t as Rng } from "./rng-DP-SR7eg.js";
|
|
4
4
|
import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
|
|
5
5
|
import { t as createNoise2D } from "./noise-CGUMx44x.js";
|
|
6
|
-
import { i as
|
|
7
|
-
import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-
|
|
6
|
+
import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-D_p28jwU.js";
|
|
7
|
+
import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-DbaI313d.js";
|
|
8
8
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
9
9
|
import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
|
|
10
10
|
import { clone } from "three/addons/utils/SkeletonUtils.js";
|
|
@@ -2112,13 +2112,19 @@ var AnimatedSprite3D = class extends Sprite3D {
|
|
|
2112
2112
|
frameList = [];
|
|
2113
2113
|
frameIndex = 0;
|
|
2114
2114
|
frameTime = 0;
|
|
2115
|
+
currentDef = null;
|
|
2115
2116
|
sheetTexture = null;
|
|
2116
2117
|
loadedSheetRef = "";
|
|
2118
|
+
/**
|
|
2119
|
+
* `currentAnimation` reports the clip that is RUNNING, not the name asked
|
|
2120
|
+
* for: an alias is a rename, and a state machine that reads back `fall` while
|
|
2121
|
+
* the idle frames play is describing something that is not happening.
|
|
2122
|
+
*/
|
|
2117
2123
|
play(name) {
|
|
2118
|
-
const def = this.animations
|
|
2119
|
-
|
|
2120
|
-
this.
|
|
2121
|
-
this.frameList = resolveFrames(def.frames,
|
|
2124
|
+
const { name: resolved, def } = resolveAnimation(this.animations, name, this.name);
|
|
2125
|
+
this.currentAnimation = resolved;
|
|
2126
|
+
this.currentDef = def;
|
|
2127
|
+
this.frameList = resolveFrames(def.frames, resolved);
|
|
2122
2128
|
this.frameIndex = 0;
|
|
2123
2129
|
this.currentFrame = this.frameList[0] ?? 0;
|
|
2124
2130
|
this.frameTime = 0;
|
|
@@ -2128,11 +2134,12 @@ var AnimatedSprite3D = class extends Sprite3D {
|
|
|
2128
2134
|
this.playing = false;
|
|
2129
2135
|
}
|
|
2130
2136
|
onReady() {
|
|
2137
|
+
validateAnimationAliases(this.animations, this.name);
|
|
2131
2138
|
if (this.autoplay) this.play(this.autoplay);
|
|
2132
2139
|
}
|
|
2133
2140
|
update(dt) {
|
|
2134
2141
|
if (!this.playing || this.currentAnimation === "") return;
|
|
2135
|
-
const def = this.
|
|
2142
|
+
const def = this.currentDef;
|
|
2136
2143
|
if (!def || !Number.isFinite(def.fps) || def.fps <= 0) return;
|
|
2137
2144
|
const perFrame = 1 / def.fps;
|
|
2138
2145
|
this.frameTime += dt;
|
|
@@ -2173,18 +2180,6 @@ var AnimatedSprite3D = class extends Sprite3D {
|
|
|
2173
2180
|
super.free();
|
|
2174
2181
|
}
|
|
2175
2182
|
};
|
|
2176
|
-
function resolveFrames(frames, name) {
|
|
2177
|
-
if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
|
|
2178
|
-
if (frames.length === 2) {
|
|
2179
|
-
const [start, end] = frames;
|
|
2180
|
-
if (end >= start) {
|
|
2181
|
-
const out = [];
|
|
2182
|
-
for (let f = start; f <= end; f++) out.push(f);
|
|
2183
|
-
return out;
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
return [...frames];
|
|
2187
|
-
}
|
|
2188
2183
|
//#endregion
|
|
2189
2184
|
//#region src/3d/nodes/billboard-3d.ts
|
|
2190
2185
|
const _parentQ = new Quaternion();
|