incanto 0.68.0 → 0.69.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/assets/catalog.json +9 -5
- package/bin/incanto-assets.mjs +14 -1
- package/bin/incanto-check.mjs +53 -3
- package/bin/incanto-new.mjs +29 -1
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-verify.mjs +40 -0
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +7 -5
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +4 -4
- package/dist/3d.js +7 -7
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-C0j2k-yB.d.ts} +32 -13
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-rm8vVzu_.d.ts} +67 -0
- package/dist/{create-game-DbWtVTxD.js → create-game-CvJ1wSRP.js} +13 -9
- package/dist/{create-game-IZIydDwI.js → create-game-CwxEDlCm.js} +13 -9
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-IWIqk0HJ.js → duplicate-Dj4L-5AA.js} +1 -1
- package/dist/editor.js +1 -1
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-BO-7XoFV.js} +4 -4
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-DM1eu_cV.js → gameplay-D1KixWK8.js} +157 -151
- package/dist/gameplay.d.ts +1 -1
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/{loader-BC4PNtJX.js → loader-DZE7B07H.js} +22 -6
- package/dist/{loader-CcB533FR.d.ts → loader-DadV2QCT.d.ts} +1 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CF5yyjbj.js} +2 -2
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-DbYaEcZy.js} +3 -3
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-ibCjm-wH.js → register-DbVArsns.js} +19 -10
- package/dist/{register-3ta-2Xig.js → register-mmfugpaQ.js} +107 -15
- package/dist/{replay-CYvhVHHN.js → replay-BEHLDcQV.js} +2 -2
- package/dist/{replay-Dvn8aeBd.d.ts → replay-BQfkX4x7.d.ts} +1 -1
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-CYwDkbLF.js → split-screen-CauMwu6y.js} +2 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen-Cg76MCV5.d.ts} +2 -2
- package/dist/{src-D7RIqXYF.js → src-CYEu-_S9.js} +1 -1
- package/dist/{teardown-D2NEmxPB.js → teardown-CMqFVxO9.js} +5 -4
- package/dist/{test-8hoHeRmo.js → test-D6ISmuFE.js} +60 -17
- package/dist/test.d.ts +15 -4
- package/dist/test.js +2 -2
- package/dist/vite.d.ts +70 -2
- package/dist/vite.js +176 -14
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-Z5_g8x9H.js} +12 -1
- package/editor/assets/{agent8-BrrHOjMJ.js → agent8-BDvaZMQJ.js} +1 -1
- package/editor/assets/{debug-BBhKuBNV.js → debug-CMULinBW.js} +1 -1
- package/editor/assets/{index-eVd0BToA.js → index-CZIRr5EL.js} +91 -91
- package/editor/index.html +1 -1
- package/package.json +4 -3
- package/skills/incanto-assets.md +16 -2
- package/skills/incanto-audio.md +19 -3
- package/skills/incanto-building-2d-games.md +29 -0
- package/skills/incanto-building-3d-games.md +60 -2
- package/skills/incanto-gameplay-behaviors.md +18 -1
- package/skills/incanto-save-slots.md +27 -0
- package/skills/incanto-verifying-your-game.md +96 -2
- package/skills/incanto-your-first-game.md +2 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/main.ts +13 -3
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +17 -27
- package/templates-app/platformer-2d/src/main.ts +24 -12
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/main.ts +17 -6
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/main.ts +26 -15
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/main.ts +13 -3
package/assets/catalog.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "incanto/assets/characters/2dbasic.png",
|
|
6
6
|
"kind": "character",
|
|
7
7
|
"bytes": 30499,
|
|
8
|
-
"description": "2dbasic sprite sheet image.anything,base character.
|
|
8
|
+
"description": "2dbasic sprite sheet image.anything,base character.",
|
|
9
9
|
"animation": "characters/2dbasic.json",
|
|
10
10
|
"frameWidth": 111,
|
|
11
11
|
"frameHeight": 83
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"url": "incanto/assets/characters/ghost.png",
|
|
145
145
|
"kind": "character",
|
|
146
146
|
"bytes": 22933,
|
|
147
|
-
"description": "Ghost character with translucent appearance sprite sheet image
|
|
147
|
+
"description": "Ghost character with translucent appearance sprite sheet image",
|
|
148
148
|
"animation": "characters/ghost.json",
|
|
149
149
|
"frameWidth": 112,
|
|
150
150
|
"frameHeight": 128
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"url": "incanto/assets/characters/goblin.png",
|
|
156
156
|
"kind": "character",
|
|
157
157
|
"bytes": 57994,
|
|
158
|
-
"description": "Medieval goblin with torch sprite sheet image
|
|
158
|
+
"description": "Medieval goblin with torch sprite sheet image",
|
|
159
159
|
"animation": "characters/goblin.json",
|
|
160
160
|
"frameWidth": 192,
|
|
161
161
|
"frameHeight": 192
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"url": "incanto/assets/characters/medieval-knight.png",
|
|
287
287
|
"kind": "character",
|
|
288
288
|
"bytes": 84367,
|
|
289
|
-
"description": "
|
|
289
|
+
"description": "Using a medieval-themed SD(Super Deformed) knight sprite sheet image, you can apply idle, move, and attack animations, among others.",
|
|
290
290
|
"animation": "characters/medieval-knight.json",
|
|
291
291
|
"frameWidth": 192,
|
|
292
292
|
"frameHeight": 192
|
|
@@ -297,7 +297,11 @@
|
|
|
297
297
|
"url": "incanto/assets/tiles/minecraft-tiles.png",
|
|
298
298
|
"kind": "tile",
|
|
299
299
|
"bytes": 10511,
|
|
300
|
-
"description": "Minecraft-themed tiles sprite sheet image
|
|
300
|
+
"description": "Minecraft-themed tiles sprite sheet image",
|
|
301
|
+
"frameWidth": 16,
|
|
302
|
+
"frameHeight": 16,
|
|
303
|
+
"columns": 5,
|
|
304
|
+
"tiles": 25
|
|
301
305
|
},
|
|
302
306
|
{
|
|
303
307
|
"name": "monster-died",
|
package/bin/incanto-assets.mjs
CHANGED
|
@@ -40,8 +40,21 @@ if (command === 'list') {
|
|
|
40
40
|
} else {
|
|
41
41
|
for (const e of entries) {
|
|
42
42
|
const anim = e.animation ? ' [animated]' : '';
|
|
43
|
+
/*
|
|
44
|
+
* The GRID, from the fields rather than the prose.
|
|
45
|
+
*
|
|
46
|
+
* `list` printed only the description, and a description carrying a frame
|
|
47
|
+
* size is a second source of truth that drifts: `2dbasic` said
|
|
48
|
+
* "(frame size 192x192)" against a real grid of 111x83 — 666x664 does not
|
|
49
|
+
* divide by 192 — and the wrong number was the first one an agent read.
|
|
50
|
+
* The sizes live in `frameWidth`/`frameHeight` now, and the prose says
|
|
51
|
+
* what the art IS.
|
|
52
|
+
*/
|
|
53
|
+
const grid = e.frameWidth
|
|
54
|
+
? ` ${e.frameWidth}×${e.frameHeight}${e.tiles ? ` (${e.tiles} tiles)` : ''}`
|
|
55
|
+
: '';
|
|
43
56
|
console.log(
|
|
44
|
-
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)} ${e.description.slice(0,
|
|
57
|
+
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)}${grid.padEnd(16)} ${e.description.slice(0, 64)}`,
|
|
45
58
|
);
|
|
46
59
|
}
|
|
47
60
|
console.log(`\n${entries.length} built-in asset(s) — incanto-assets info <name> for details`);
|
package/bin/incanto-check.mjs
CHANGED
|
@@ -18,14 +18,27 @@ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
21
22
|
|
|
22
23
|
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
23
24
|
|
|
24
25
|
function parseArgs(argv) {
|
|
25
|
-
const args = {
|
|
26
|
-
|
|
26
|
+
const args = {
|
|
27
|
+
files: [],
|
|
28
|
+
json: false,
|
|
29
|
+
strict: false,
|
|
30
|
+
help: false,
|
|
31
|
+
invalid: false,
|
|
32
|
+
behaviors: null,
|
|
33
|
+
};
|
|
34
|
+
// Indexed, because `--behaviors` takes the NEXT argument. A `for…of` with a
|
|
35
|
+
// hand-rolled `++i` reads fine and throws `i is not defined` at runtime,
|
|
36
|
+
// which is a flag that exists in the help and cannot be used.
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
const a = argv[i];
|
|
27
39
|
if (a === '--json') args.json = true;
|
|
28
40
|
else if (a === '--strict-behaviors') args.strict = true;
|
|
41
|
+
else if (a === '--behaviors') args.behaviors = argv[++i];
|
|
29
42
|
else if (a === '--strict-warnings') args.strictWarnings = true;
|
|
30
43
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
31
44
|
else if (a.startsWith('--')) {
|
|
@@ -38,7 +51,7 @@ function parseArgs(argv) {
|
|
|
38
51
|
|
|
39
52
|
const args = parseArgs(process.argv.slice(2));
|
|
40
53
|
if (args.help || args.invalid) {
|
|
41
|
-
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--strict-behaviors] [--strict-warnings]
|
|
54
|
+
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--behaviors FILE] [--strict-behaviors] [--strict-warnings]
|
|
42
55
|
|
|
43
56
|
Validates *.scene.json files headlessly with the installed engine.
|
|
44
57
|
With no arguments, scans the current directory recursively
|
|
@@ -159,6 +172,30 @@ const { auditScene, instancedScenes, validateScene } = await import(
|
|
|
159
172
|
pathToFileURL(join(DIST, 'test.js')).href
|
|
160
173
|
);
|
|
161
174
|
|
|
175
|
+
/** Your Behavior subclasses, when you name a file — same contract as the other bins. */
|
|
176
|
+
const behaviors = {};
|
|
177
|
+
if (args.behaviors) {
|
|
178
|
+
const { Behavior } = await import(pathToFileURL(join(DIST, 'index.js')).href);
|
|
179
|
+
let mod;
|
|
180
|
+
try {
|
|
181
|
+
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
const why = e?.message ?? String(e);
|
|
184
|
+
// node cannot import the pattern this package teaches (a JSON import
|
|
185
|
+
// without an attribute, an extensionless relative path); bun can. Same
|
|
186
|
+
// hand-off as `incanto-playtest`.
|
|
187
|
+
reexecUnderBun(import.meta.url, why);
|
|
188
|
+
console.error(behaviorsHelp(args.behaviors, why));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
192
|
+
if (typeof value === 'function' && value.prototype instanceof Behavior) behaviors[name] = value;
|
|
193
|
+
}
|
|
194
|
+
if (Object.keys(behaviors).length === 0) {
|
|
195
|
+
console.error(`warning: no Behavior subclasses exported from ${args.behaviors}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
/**
|
|
163
200
|
* Every `strings` table in the corpus, unioned.
|
|
164
201
|
*
|
|
@@ -211,6 +248,19 @@ const results = files.map((file) => {
|
|
|
211
248
|
}
|
|
212
249
|
const res = validateScene(json, {
|
|
213
250
|
strictBehaviors: args.strict,
|
|
251
|
+
/*
|
|
252
|
+
* `validateScene` has always taken `behaviors`; this bin had no way to pass
|
|
253
|
+
* any. So `--strict-behaviors` could only ever FAIL on a project with a
|
|
254
|
+
* behaviour of its own — which is every project past its first hour:
|
|
255
|
+
*
|
|
256
|
+
* [UNKNOWN_BEHAVIOR] Unknown behavior 'Hunter'. … Did you forget
|
|
257
|
+
* registerBehavior('Hunter', Hunter)?
|
|
258
|
+
* 0/1 scene(s) valid EXIT=1
|
|
259
|
+
*
|
|
260
|
+
* A flag whose only outcome is failure is a flag nobody uses, and the check
|
|
261
|
+
* it gates — a script name that resolves nowhere — is worth having.
|
|
262
|
+
*/
|
|
263
|
+
behaviors,
|
|
214
264
|
declaredAssets,
|
|
215
265
|
resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
|
|
216
266
|
});
|
package/bin/incanto-new.mjs
CHANGED
|
@@ -70,7 +70,11 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (!target) {
|
|
73
|
-
console.error(
|
|
73
|
+
console.error(
|
|
74
|
+
'usage: incanto-new <dir> [--template <name>] [--local] | --list\n' +
|
|
75
|
+
' --local pin `file:` to the incanto this command is running from,\n' +
|
|
76
|
+
' instead of the registry — for verifying a release candidate.',
|
|
77
|
+
);
|
|
74
78
|
process.exit(1);
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -93,10 +97,30 @@ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
|
93
97
|
pkg.name = basename(dest)
|
|
94
98
|
.toLowerCase()
|
|
95
99
|
.replace(/[^a-z0-9-]/g, '-');
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* WHICH incanto this scaffold will install.
|
|
103
|
+
*
|
|
104
|
+
* The template pins `^<version>`, so `bun install` resolves the REGISTRY build
|
|
105
|
+
* — even when the scaffolder itself came from a local tarball. Someone
|
|
106
|
+
* verifying a release candidate that shares its version with a published one
|
|
107
|
+
* then measures the published artifact for an hour and never sees a difference:
|
|
108
|
+
* the version matches, the install succeeds, and a `main.ts` that typechecks
|
|
109
|
+
* against the candidate does not typecheck against the registry copy.
|
|
110
|
+
*
|
|
111
|
+
* `--local` pins the package this bin is running from, and either way the pin
|
|
112
|
+
* is printed. A silent resolution is the part that cost the hour.
|
|
113
|
+
*/
|
|
114
|
+
const ENGINE_ROOT = PKG;
|
|
115
|
+
if (argv.includes('--local')) {
|
|
116
|
+
pkg.dependencies = { ...pkg.dependencies, incanto: `file:${ENGINE_ROOT}` };
|
|
117
|
+
}
|
|
118
|
+
const pin = pkg.dependencies?.incanto ?? '(none)';
|
|
96
119
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
97
120
|
|
|
98
121
|
console.log(`
|
|
99
122
|
${template} → ${target}
|
|
123
|
+
incanto ${pin}
|
|
100
124
|
|
|
101
125
|
cd ${target}
|
|
102
126
|
bun install # or: npm install
|
|
@@ -104,6 +128,10 @@ console.log(`
|
|
|
104
128
|
bun run verify # prove it plays, headlessly
|
|
105
129
|
bun run check # validate the scene JSON
|
|
106
130
|
|
|
131
|
+
When it plays, ship it:
|
|
132
|
+
bun run build # the artifact a player gets
|
|
133
|
+
bunx incanto-serve # serve dist/ so verify/frame/logs can see the BUILT game
|
|
134
|
+
|
|
107
135
|
Vibe-coding? Install the agent skills too:
|
|
108
136
|
bunx incanto-skills claude # or: opencode
|
|
109
137
|
`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Serve a BUILT game, and let the inspect tools see it.
|
|
5
|
+
*
|
|
6
|
+
* `incanto-frame` and `incanto-logs` ask a dev server, which asks the page over
|
|
7
|
+
* vite's HMR channel. A production build has no HMR, so nothing could look at
|
|
8
|
+
* the artifact an author actually deploys:
|
|
9
|
+
*
|
|
10
|
+
* $ node …/incanto-logs.mjs --port 8791 # dist/ on a static server
|
|
11
|
+
* no incanto dev server found.
|
|
12
|
+
* $ incanto-verify src/game.scene.json
|
|
13
|
+
* ? draws — no incanto dev server found.
|
|
14
|
+
* ? says — no incanto dev server found.
|
|
15
|
+
*
|
|
16
|
+
* The boot path is the only code a BUILD adds over a SCENE — `main.ts`, the
|
|
17
|
+
* loading overlay, the asset base path, everything after `createGame*` resolves
|
|
18
|
+
* — and it was the one thing no rung could reach.
|
|
19
|
+
*
|
|
20
|
+
* This serves `dist/` as plain static files (no bundler, no transform) and adds
|
|
21
|
+
* exactly two things: the `/__incanto/*` endpoints the CLIs already speak, and
|
|
22
|
+
* one inline script injected into `index.html` that relays them to the page.
|
|
23
|
+
* Nothing is added to the files on disk, so what you measure is what you ship.
|
|
24
|
+
*
|
|
25
|
+
* bunx incanto-serve # ./dist on :4180
|
|
26
|
+
* bunx incanto-serve build --port 5050
|
|
27
|
+
* bunx incanto-verify # now measures the BUILT game
|
|
28
|
+
*/
|
|
29
|
+
import { createServer } from 'node:http';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const PKG = resolve(fileURLToPath(import.meta.url), '../..');
|
|
35
|
+
const DIST = join(PKG, 'dist');
|
|
36
|
+
if (!existsSync(join(DIST, 'vite.js'))) {
|
|
37
|
+
console.error('incanto: the package is not built (dist/vite.js missing).');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const { createStaticInspectHost, serveFrameEndpoints } = await import(
|
|
41
|
+
pathToFileURL(join(DIST, 'vite.js')).href
|
|
42
|
+
);
|
|
43
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
47
|
+
console.error(`Usage: incanto-serve [dir] [--port N] [--host H]
|
|
48
|
+
|
|
49
|
+
Serve a BUILT game (default ./dist) so \`incanto-frame\`, \`incanto-logs\` and
|
|
50
|
+
\`incanto-verify\` can measure the artifact you actually deploy. Files are served
|
|
51
|
+
byte-for-byte; the inspect client is injected into index.html at serve time and
|
|
52
|
+
never written to disk.`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const flag = (name, fallback) => {
|
|
56
|
+
const at = args.indexOf(name);
|
|
57
|
+
return at >= 0 && args[at + 1] ? args[at + 1] : fallback;
|
|
58
|
+
};
|
|
59
|
+
const port = Number(flag('--port', '4180'));
|
|
60
|
+
const host = flag('--host', '127.0.0.1');
|
|
61
|
+
const dir = resolve(
|
|
62
|
+
args.find(
|
|
63
|
+
(a) =>
|
|
64
|
+
!a.startsWith('--') &&
|
|
65
|
+
args[args.indexOf(a) - 1] !== '--port' &&
|
|
66
|
+
args[args.indexOf(a) - 1] !== '--host',
|
|
67
|
+
) ?? 'dist',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
71
|
+
console.error(
|
|
72
|
+
`incanto-serve: no such directory: ${dir}\n` +
|
|
73
|
+
' Build first (`bun run build`), or name the output directory.',
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync(join(dir, 'index.html'))) {
|
|
78
|
+
console.error(
|
|
79
|
+
`incanto-serve: ${dir} has no index.html — that is not a built game.\n` +
|
|
80
|
+
' Point this at the directory your bundler wrote.',
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inspect = createStaticInspectHost(process.cwd());
|
|
86
|
+
serveFrameEndpoints(inspect, VERSION);
|
|
87
|
+
|
|
88
|
+
const TYPES = {
|
|
89
|
+
'.html': 'text/html; charset=utf-8',
|
|
90
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
91
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
92
|
+
'.css': 'text/css; charset=utf-8',
|
|
93
|
+
'.json': 'application/json; charset=utf-8',
|
|
94
|
+
'.png': 'image/png',
|
|
95
|
+
'.jpg': 'image/jpeg',
|
|
96
|
+
'.jpeg': 'image/jpeg',
|
|
97
|
+
'.webp': 'image/webp',
|
|
98
|
+
'.gif': 'image/gif',
|
|
99
|
+
'.svg': 'image/svg+xml',
|
|
100
|
+
'.ico': 'image/x-icon',
|
|
101
|
+
'.glb': 'model/gltf-binary',
|
|
102
|
+
'.gltf': 'model/gltf+json',
|
|
103
|
+
'.mp3': 'audio/mpeg',
|
|
104
|
+
'.ogg': 'audio/ogg',
|
|
105
|
+
'.wav': 'audio/wav',
|
|
106
|
+
'.wasm': 'application/wasm',
|
|
107
|
+
'.woff2': 'font/woff2',
|
|
108
|
+
'.ttf': 'font/ttf',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Inject the relay ahead of everything, so it exists before the game boots. */
|
|
112
|
+
function withInspectClient(html) {
|
|
113
|
+
const tag = `<script>${inspect.clientScript()}</script>`;
|
|
114
|
+
if (html.includes('</head>')) return html.replace('</head>', `${tag}</head>`);
|
|
115
|
+
return tag + html;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const server = createServer((req, res) => {
|
|
119
|
+
const url = (req.url ?? '/').split('?')[0] ?? '/';
|
|
120
|
+
if (url.startsWith('/__incanto/')) {
|
|
121
|
+
// `originalUrl` is what the endpoints read for the query string; connect
|
|
122
|
+
// strips the mount path off `url` and these were written against that.
|
|
123
|
+
req.originalUrl = req.url;
|
|
124
|
+
if (inspect.handleInspect(req, res)) return;
|
|
125
|
+
res.statusCode = 404;
|
|
126
|
+
res.end('{}');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A path outside the served directory is a traversal attempt, not a typo.
|
|
130
|
+
const rel = normalize(decodeURIComponent(url)).replace(/^(\.\.[/\\])+/, '');
|
|
131
|
+
let file = join(dir, rel);
|
|
132
|
+
if (!file.startsWith(dir + sep) && file !== dir) file = dir;
|
|
133
|
+
if (!existsSync(file) || statSync(file).isDirectory()) file = join(dir, 'index.html');
|
|
134
|
+
|
|
135
|
+
const type = TYPES[extname(file)] ?? 'application/octet-stream';
|
|
136
|
+
if (file.endsWith('index.html')) {
|
|
137
|
+
const html = withInspectClient(readFileSync(file, 'utf8'));
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
res.setHeader('content-type', TYPES['.html']);
|
|
140
|
+
res.end(html);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.statusCode = 200;
|
|
144
|
+
res.setHeader('content-type', type);
|
|
145
|
+
res.end(readFileSync(file));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.on('error', (e) => {
|
|
149
|
+
if (e?.code === 'EADDRINUSE') {
|
|
150
|
+
console.error(`incanto-serve: port ${port} is already in use — pass --port N.`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
throw e;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.listen(port, host, () => {
|
|
157
|
+
console.log(`incanto-serve ${dir}`);
|
|
158
|
+
console.log(` http://${host}:${port}/`);
|
|
159
|
+
console.log(' open it, then run `incanto-verify` / `incanto-frame` / `incanto-logs`.');
|
|
160
|
+
});
|
package/bin/incanto-verify.mjs
CHANGED
|
@@ -449,6 +449,30 @@ if (rungs[0].status === 'pass') {
|
|
|
449
449
|
? 'bring the preview window to the front — a hidden tab does not render, and a frame is captured inside a render'
|
|
450
450
|
: 'start your dev server, open the page, and run this again — the pixels are in the browser',
|
|
451
451
|
});
|
|
452
|
+
} else if (report.hidden) {
|
|
453
|
+
/*
|
|
454
|
+
* A hidden page is UNMEASURED, not passed.
|
|
455
|
+
*
|
|
456
|
+
* A browser stops animating a page it considers hidden — a window merely
|
|
457
|
+
* covered by another counts — and the back buffer keeps whatever it last
|
|
458
|
+
* drew. Measured at one instant, same page:
|
|
459
|
+
*
|
|
460
|
+
* incanto-frame --do the page is hidden … a frozen game (exit 1)
|
|
461
|
+
* incanto-logs frame rate not measurable — hidden
|
|
462
|
+
* incanto-frame subject fills 17.5% of frame
|
|
463
|
+
* incanto-verify ✓ draws — subject fills 17.5%
|
|
464
|
+
*
|
|
465
|
+
* Two of six rungs reported ✓ off a stale buffer while the engine already
|
|
466
|
+
* knew, and said elsewhere, that the page was drawing nothing. The skills
|
|
467
|
+
* state the rule: an unmeasured rung is not a failure, and this is exactly
|
|
468
|
+
* the unmeasured case.
|
|
469
|
+
*/
|
|
470
|
+
rungs.push({
|
|
471
|
+
name: 'draws',
|
|
472
|
+
status: 'unmeasured',
|
|
473
|
+
summary: 'the page is HIDDEN — those pixels are whatever it last drew',
|
|
474
|
+
fix: 'bring the window to the front (one covered by another counts as hidden) and run this again',
|
|
475
|
+
});
|
|
452
476
|
} else if (report.black) {
|
|
453
477
|
rungs.push({
|
|
454
478
|
name: 'draws',
|
|
@@ -464,10 +488,26 @@ if (rungs[0].status === 'pass') {
|
|
|
464
488
|
fix: 'check the camera: `incanto-frame --out shot.png` and look at what it is aimed at',
|
|
465
489
|
});
|
|
466
490
|
} else {
|
|
491
|
+
/*
|
|
492
|
+
* A rung that PASSED can still have something to say, and this one was
|
|
493
|
+
* dropping it. `incanto-frame` prints `CLIPPED by the frame edge` and
|
|
494
|
+
* `the subject fills almost nothing` — real observations about a shot that
|
|
495
|
+
* is technically non-black — and the ladder replaced all of it with one
|
|
496
|
+
* coverage number. The `loads` rung has carried its tool's warnings since
|
|
497
|
+
* the day a scene described as "It will render black" came back green.
|
|
498
|
+
*/
|
|
499
|
+
const notes = [];
|
|
500
|
+
if (report.subject.clipped) {
|
|
501
|
+
notes.push('the subject is CLIPPED by the frame edge — part of it is off screen');
|
|
502
|
+
}
|
|
503
|
+
if (report.uniform) {
|
|
504
|
+
notes.push('one flat colour — the camera may be inside geometry, or only the sky is drawn');
|
|
505
|
+
}
|
|
467
506
|
rungs.push({
|
|
468
507
|
name: 'draws',
|
|
469
508
|
status: 'pass',
|
|
470
509
|
summary: `subject fills ${(report.subject.coverage * 100).toFixed(1)}% of the frame`,
|
|
510
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
471
511
|
});
|
|
472
512
|
}
|
|
473
513
|
}
|
package/bin/incanto.mjs
CHANGED
|
@@ -40,6 +40,7 @@ const COMMANDS = {
|
|
|
40
40
|
feel: ['incanto-feel.mjs', 'measure jump height, run speed, facing'],
|
|
41
41
|
frame: ['incanto-frame.mjs', 'what the running page is drawing right now'],
|
|
42
42
|
logs: ['incanto-logs.mjs', 'what the running page has been saying'],
|
|
43
|
+
serve: ['incanto-serve.mjs', 'serve a BUILT game so the tools can measure it'],
|
|
43
44
|
editor: ['incanto-editor.mjs', 'the visual scene composer'],
|
|
44
45
|
model: ['incanto-model.mjs', "a GLB's real bounds, animations and rig"],
|
|
45
46
|
assets: ['incanto-assets.mjs', 'the built-in art and audio catalog'],
|
package/dist/2d.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-
|
|
2
|
-
import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-
|
|
1
|
+
import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-C0j2k-yB.js";
|
|
2
|
+
import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-rm8vVzu_.js";
|
|
3
3
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
|
|
4
4
|
import { i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
|
|
5
|
-
import { r as FrameStats } from "./frame-report-
|
|
6
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
5
|
+
import { r as FrameStats } from "./frame-report-DNxDAb1w.js";
|
|
6
|
+
import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
|
|
7
7
|
import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
|
|
8
8
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
9
9
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
@@ -972,9 +972,11 @@ declare class TileMap2D extends Node2D {
|
|
|
972
972
|
private _solid;
|
|
973
973
|
/** Rebuild flags — raised when the map-shaping props are REPLACED. */
|
|
974
974
|
private geometryDirty;
|
|
975
|
-
/**
|
|
975
|
+
/** `texture|highestTile` already reported, so a bad grid says so once. */
|
|
976
976
|
private gridChecked;
|
|
977
977
|
private collidersDirty;
|
|
978
|
+
/** The `scale` the colliders were last built for — see `rebuildColliders`. */
|
|
979
|
+
private builtScale;
|
|
978
980
|
/** Replace the whole array to change the map (mutations are not watched). */
|
|
979
981
|
get cells(): (string | number[])[];
|
|
980
982
|
set cells(value: (string | number[])[]);
|
package/dist/2d.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
2
|
import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
|
|
3
|
-
import { t as isWebGLAvailable } from "./webgl-unavailable-
|
|
4
|
-
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-
|
|
5
|
-
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-
|
|
6
|
-
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-
|
|
3
|
+
import { t as isWebGLAvailable } from "./webgl-unavailable-Z5_g8x9H.js";
|
|
4
|
+
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-CwxEDlCm.js";
|
|
5
|
+
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-DbVArsns.js";
|
|
6
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CF5yyjbj.js";
|
|
7
7
|
//#region src/2d/library-sprite.ts
|
|
8
8
|
/**
|
|
9
9
|
* What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
|
package/dist/3d.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-
|
|
2
|
-
import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-
|
|
1
|
+
import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-C0j2k-yB.js";
|
|
2
|
+
import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-rm8vVzu_.js";
|
|
3
3
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
|
|
4
4
|
import { i as SceneJson$1, s as JsonObject } from "./schema-B6ugCV1Q.js";
|
|
5
|
-
import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-
|
|
6
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
5
|
+
import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DNxDAb1w.js";
|
|
6
|
+
import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
|
|
7
7
|
import { n as PathGrid } from "./pathfinding-_fGrCFmH.js";
|
|
8
8
|
import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
|
|
9
9
|
import { VRM } from "@pixiv/three-vrm";
|
package/dist/3d.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-
|
|
1
|
+
import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-DZE7B07H.js";
|
|
2
2
|
import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
|
|
3
|
-
import { t as isWebGLAvailable } from "./webgl-unavailable-
|
|
4
|
-
import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-
|
|
5
|
-
import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-
|
|
6
|
-
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-
|
|
7
|
-
import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-
|
|
3
|
+
import { t as isWebGLAvailable } from "./webgl-unavailable-Z5_g8x9H.js";
|
|
4
|
+
import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-Dlq13Gyj.js";
|
|
5
|
+
import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-D1KixWK8.js";
|
|
6
|
+
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-BO-7XoFV.js";
|
|
7
|
+
import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-CvJ1wSRP.js";
|
|
8
8
|
import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
|
|
9
|
-
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-
|
|
9
|
+
import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DbYaEcZy.js";
|
|
10
10
|
//#region src/3d/model-verdict.ts
|
|
11
11
|
/** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
|
|
12
12
|
const MIXAMO = /^mixamorig[:_]?/i;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-
|
|
1
|
+
import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-rm8vVzu_.js";
|
|
2
2
|
import { a as Rng } from "./schema-B6ugCV1Q.js";
|
|
3
3
|
|
|
4
4
|
//#region src/core/boot-failure.d.ts
|
|
@@ -234,28 +234,47 @@ declare class AudioPlayer extends Node {
|
|
|
234
234
|
/** The `spatial` option for the SFX path, or undefined when not spatial. */
|
|
235
235
|
private spatialPlay;
|
|
236
236
|
/**
|
|
237
|
-
* Start the sound
|
|
237
|
+
* Start the sound, now.
|
|
238
|
+
*
|
|
239
|
+
* **Takes no arguments, on purpose.** This is the method scenes wire signals
|
|
240
|
+
* to — `incanto-audio.md` teaches
|
|
241
|
+
* `{"signal": "collected", "from": "Player/Collector", "to": "Coin",
|
|
242
|
+
* "handler": "play"}` — and a signal hands its handler whatever it carries.
|
|
243
|
+
* `collected` leads with a NUMBER. When this briefly took an optional
|
|
244
|
+
* scheduling time, that wiring became `play(10)`, which scheduled the pickup
|
|
245
|
+
* sound at ABSOLUTE audio-clock second 10: inaudible for the first ten
|
|
246
|
+
* seconds of the game and fine thereafter. The same optional parameter also
|
|
247
|
+
* made `Function.length` 1, so the engine's own arity checker cried wolf on
|
|
248
|
+
* the shipped `platformer-2d` template's own harness.
|
|
249
|
+
*
|
|
250
|
+
* Scheduling has its own name: {@link playAt}.
|
|
251
|
+
*/
|
|
252
|
+
/**
|
|
253
|
+
* Start the sound at `when` on the AUDIO clock (`engine.sfx.now` + a lead).
|
|
238
254
|
*
|
|
239
255
|
* A frame is a 16.67 ms grid at 60 Hz and 33.33 ms at 30, so a sound fired
|
|
240
256
|
* from `update()` cannot land closer than one frame to where a chart wants
|
|
241
|
-
* it — measured 0 of 64 notes
|
|
242
|
-
*
|
|
243
|
-
* ms:
|
|
257
|
+
* it — measured 0 of 64 notes given a scheduled start. Queue them a lead
|
|
258
|
+
* ahead and the same notes land at |mean| 0.000000 ms:
|
|
244
259
|
*
|
|
245
260
|
* ```ts
|
|
246
|
-
* const
|
|
247
|
-
* for (const note of dueSoon(engine.sfx.now +
|
|
248
|
-
* hit.
|
|
261
|
+
* const LEAD = 0.08;
|
|
262
|
+
* for (const note of dueSoon(engine.sfx.now + LEAD)) {
|
|
263
|
+
* hit.playAt(startedAt + note.atSec);
|
|
249
264
|
* }
|
|
250
265
|
* ```
|
|
251
266
|
*
|
|
252
267
|
* PRESETS only — a `src` clip goes through an `<audio>` element, which has no
|
|
253
|
-
* scheduling clock,
|
|
254
|
-
* approximated.
|
|
255
|
-
*
|
|
256
|
-
*
|
|
268
|
+
* scheduling clock, so `playAt` on one plays immediately rather than being
|
|
269
|
+
* silently approximated. A time already past plays now (Web Audio's own
|
|
270
|
+
* rule), so a late scheduler is late, not silent. The bus gain, the
|
|
271
|
+
* `engine.audio.recent()` entry and `muted` all still apply.
|
|
272
|
+
*
|
|
273
|
+
* Separate from {@link play} because `play` is what scenes wire signals to,
|
|
274
|
+
* and a signal hands its handler whatever it carries.
|
|
257
275
|
*/
|
|
258
|
-
|
|
276
|
+
playAt(when: number): void;
|
|
277
|
+
play(): void;
|
|
259
278
|
/**
|
|
260
279
|
* Procedural-SFX path: synthesize the preset and fire it through WebAudio
|
|
261
280
|
* (low-latency, overlap-friendly). Headless (no AudioContext) plays nothing —
|