incanto 0.58.0 → 0.60.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.
Files changed (60) hide show
  1. package/bin/incanto-check.mjs +45 -4
  2. package/bin/incanto-multiplay.mjs +170 -0
  3. package/bin/incanto-verify.mjs +68 -4
  4. package/bin/incanto.mjs +1 -0
  5. package/dist/2d.d.ts +8 -2
  6. package/dist/2d.js +3 -3
  7. package/dist/3d.d.ts +9 -3
  8. package/dist/3d.js +4 -4
  9. package/dist/{behavior-l08AEbq9.d.ts → behavior-DWKTUzKI.d.ts} +10 -0
  10. package/dist/{create-game-D10bU5_J.js → create-game-BCm38FJV.js} +6 -6
  11. package/dist/{create-game-BZwWJIns.js → create-game-ClnIb_M5.js} +6 -6
  12. package/dist/debug.d.ts +1 -1
  13. package/dist/debug.js +2 -2
  14. package/dist/{duplicate-BPLZDZpd.js → duplicate-DJQd44CD.js} +1 -1
  15. package/dist/{environment-presets-CvvQr_bJ.js → environment-presets-8cjF3t6w.js} +12 -10
  16. package/dist/{gameplay-BVphcxmE.js → gameplay-BBEjPFsR.js} +62 -34
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +53 -8
  20. package/dist/index.js +8 -8
  21. package/dist/{json-BLk7H2Qa.js → json-CwwhxQgb.js} +7 -1
  22. package/dist/{loader-BcrRSjxB.js → loader-D8n7TU8W.js} +142 -5
  23. package/dist/{loader-BbEMTuWg.d.ts → loader-TvkRFbyL.d.ts} +1 -1
  24. package/dist/net.d.ts +2 -2
  25. package/dist/net.js +1 -1
  26. package/dist/{pathfinding-mEN4V1CU.d.ts → pathfinding-BqWBb0kh.d.ts} +1 -1
  27. package/dist/{physics-2d-EqA-jddf.js → physics-2d-DqdVp1bt.js} +12 -2
  28. package/dist/{physics-3d-Dnz4fsXX.js → physics-3d-BP0DZb_1.js} +13 -3
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.js +2 -2
  31. package/dist/{register-C6ZBFRjd.js → register-BSu2dWGC.js} +57 -27
  32. package/dist/{register-Ch70uByv.js → register-Da3hXh2H.js} +137 -46
  33. package/dist/{registry-C7u42TID.js → registry-WWcQcfMr.js} +1 -1
  34. package/dist/{replay-Dw6gMlYA.d.ts → replay-BCMK_VRP.d.ts} +15 -2
  35. package/dist/{replay-s7I2GstT.js → replay-BlNuIDdg.js} +41 -13
  36. package/dist/{split-screen-B0baBwxI.d.ts → split-screen-BQ3tAsf-.d.ts} +38 -1
  37. package/dist/{split-screen-DLsUrleX.js → split-screen-CL5Yvxse.js} +56 -13
  38. package/dist/{sprite-animation-C0wXLBZJ.js → sprite-animation-CY-mrr1L.js} +1 -1
  39. package/dist/{src-C1J09Op6.js → src-DFpXBMJN.js} +1 -1
  40. package/dist/{teardown-Cs113S9F.js → teardown-CTTwhWSe.js} +1 -1
  41. package/dist/{test-ZBga8kQ9.js → test-BeZ95pqw.js} +264 -25
  42. package/dist/test.d.ts +119 -5
  43. package/dist/test.js +3 -3
  44. package/dist/vite.js +2 -2
  45. package/editor/assets/{agent8-BQQjE9UQ.js → agent8-DSJries_.js} +1 -1
  46. package/editor/assets/{debug-C9UCsXBS.js → debug-D15Wi5TO.js} +1 -1
  47. package/editor/assets/{index-CBgfM3WD.js → index-BjC88k97.js} +91 -91
  48. package/editor/index.html +1 -1
  49. package/package.json +2 -1
  50. package/schemas/scene.schema.json +1174 -70
  51. package/skills/incanto-localization.md +64 -8
  52. package/skills/incanto-multiplayer.md +57 -4
  53. package/skills/incanto-node-reference.md +18 -0
  54. package/skills/incanto-physics-and-input.md +26 -0
  55. package/skills/incanto-verifying-your-game.md +22 -0
  56. package/templates-app/beacon-isle-3d/package.json +1 -1
  57. package/templates-app/platformer-2d/package.json +1 -1
  58. package/templates-app/star-survivor/package.json +1 -1
  59. package/templates-app/tps-3d/package.json +1 -1
  60. package/templates-app/village-quest-3d/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  * npx incanto-check src/game.scene.json → specific files (or directories)
9
9
  * npx incanto-check --json → machine-readable report
10
10
  * npx incanto-check --strict-behaviors → unregistered script names fail too
11
+ * npx incanto-check --strict-warnings → a warning is a failure (CI, agents)
11
12
  *
12
13
  * Unregistered behaviors are STUBBED by default: structure-only validation
13
14
  * that needs no TypeScript. Sub-scene `instance` paths resolve relative to
@@ -25,6 +26,7 @@ function parseArgs(argv) {
25
26
  for (const a of argv) {
26
27
  if (a === '--json') args.json = true;
27
28
  else if (a === '--strict-behaviors') args.strict = true;
29
+ else if (a === '--strict-warnings') args.strictWarnings = true;
28
30
  else if (a === '--help' || a === '-h') args.help = true;
29
31
  else if (a.startsWith('--')) {
30
32
  console.error(`unknown flag: ${a}`);
@@ -36,7 +38,7 @@ function parseArgs(argv) {
36
38
 
37
39
  const args = parseArgs(process.argv.slice(2));
38
40
  if (args.help || args.invalid) {
39
- console.log(`Usage: npx incanto-check [files|dirs...] [--json] [--strict-behaviors]
41
+ console.log(`Usage: npx incanto check [files|dirs...] [--json] [--strict-behaviors] [--strict-warnings]
40
42
 
41
43
  Validates *.scene.json files headlessly with the installed engine.
42
44
  With no arguments, scans the current directory recursively
@@ -155,6 +157,28 @@ if (files.length === 0) {
155
157
 
156
158
  const { auditScene, validateScene } = await import(pathToFileURL(join(DIST, 'test.js')).href);
157
159
 
160
+ /**
161
+ * Every `strings` table in the corpus, unioned.
162
+ *
163
+ * Localization tables MERGE across scenes at runtime — a game's shared UI
164
+ * strings live in the scene it boots from — so auditing one file in isolation
165
+ * reported a key declared in the boot scene as "nothing declares" it, and told
166
+ * its author to add a string they already have. Checking a project means
167
+ * knowing what the project declares.
168
+ */
169
+ const declaredElsewhere = {};
170
+ for (const file of files) {
171
+ try {
172
+ const doc = JSON.parse(readFileSync(file, 'utf-8'));
173
+ for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
174
+ if (typeof entries !== 'object' || entries === null) continue;
175
+ declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
176
+ }
177
+ } catch {
178
+ /* a file that will not parse is reported below, on its own line */
179
+ }
180
+ }
181
+
158
182
  const results = files.map((file) => {
159
183
  let json;
160
184
  try {
@@ -166,7 +190,13 @@ const results = files.map((file) => {
166
190
  strictBehaviors: args.strict,
167
191
  resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
168
192
  });
169
- if (res.ok) return { file, ok: true, warnings: [...auditScene(json), ...missingArt(file, json)] };
193
+ if (res.ok) {
194
+ return {
195
+ file,
196
+ ok: true,
197
+ warnings: [...auditScene(json, { declaredElsewhere }), ...missingArt(file, json)],
198
+ };
199
+ }
170
200
  return {
171
201
  file,
172
202
  ok: false,
@@ -177,6 +207,7 @@ const results = files.map((file) => {
177
207
  });
178
208
 
179
209
  const failed = results.filter((r) => !r.ok);
210
+ const warned = results.reduce((n, r) => n + (r.warnings?.length ?? 0), 0);
180
211
  if (args.json) {
181
212
  console.log(JSON.stringify({ ok: failed.length === 0, results }, null, 2));
182
213
  } else {
@@ -186,10 +217,20 @@ if (args.json) {
186
217
  for (const w of r.warnings ?? []) console.log(` warn: ${w}`);
187
218
  } else console.log(`FAIL ${r.file}\n [${r.code}] ${r.message}`);
188
219
  }
189
- console.log(`\n${results.length - failed.length}/${results.length} scene(s) valid`);
220
+ // Say the warning count on the summary line. A warning printed under an `ok`
221
+ // and a summary that never mentions it is a finding an agent scrolls past.
222
+ console.log(
223
+ `\n${results.length - failed.length}/${results.length} scene(s) valid` +
224
+ (warned > 0
225
+ ? `, ${warned} warning(s)${args.strictWarnings ? '' : ' — `--strict-warnings` to fail on them'}`
226
+ : ''),
227
+ );
190
228
  }
191
229
  // `exitCode`, never `process.exit()`: stdout to a PIPE is written
192
230
  // asynchronously, and exiting discards whatever has not flushed. A --json
193
231
  // report read by another program came back truncated — silently, and only
194
232
  // when piped, which is the only way a program reads it.
195
- process.exitCode = failed.length === 0 ? 0 : 1;
233
+ // A warning is a finding, and a checker read by a CI step or an agent is read
234
+ // through its EXIT CODE. Opt-in, because turning it on by default would fail
235
+ // every project that has been living with one.
236
+ process.exitCode = failed.length === 0 && !(args.strictWarnings && warned > 0) ? 0 : 1;
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * incanto-multiplay — run a real match headlessly and check the clients AGREE.
4
+ *
5
+ * bunx incanto multiplay src/game.scene.json
6
+ * bunx incanto multiplay src/game.scene.json --players 3 --seconds 8 --json
7
+ *
8
+ * A desync is the purest silent failure this engine can have: both clients
9
+ * run, neither errors, and they are playing different games. Nothing checked
10
+ * for one — the harness measured room MEMBERSHIP, so "saw p2" meant "p2 is in
11
+ * the room", not "p2's data arrived", and a game whose replication was
12
+ * completely dead reported exactly what a working one did.
13
+ *
14
+ * WHAT THIS PROVES, AND WHAT IT DOES NOT. Every client runs against one
15
+ * in-memory server: no latency, no loss, no reordering. So this measures the
16
+ * APPLY path and the protocol shape, and says nothing about a live wire — a
17
+ * reconnect that never re-joins, a throttle that discards a payload, a batch
18
+ * dropped on a closed socket.
19
+ */
20
+ import { existsSync, readFileSync } from 'node:fs';
21
+ import { dirname, join, resolve } from 'node:path';
22
+ import { fileURLToPath, pathToFileURL } from 'node:url';
23
+
24
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+
26
+ const argv = process.argv.slice(2);
27
+ const flag = (name, fallback) => {
28
+ const i = argv.indexOf(name);
29
+ return i >= 0 ? argv[i + 1] : fallback;
30
+ };
31
+
32
+ if (argv.includes('--help') || argv.includes('-h')) {
33
+ console.log(`Usage: incanto multiplay [scene.json] [options]
34
+
35
+ --players N clients in the match (default 2)
36
+ --seconds N simulated seconds (default 5)
37
+ --seed N make the match reproducible (default 1)
38
+ --behaviors FILE your Behavior subclasses (.ts works on bun / node >= 23.6)
39
+ --scenes a=b.json a scene a NetworkSpawner names, repeatable
40
+ --no-late-join skip the late-joining client
41
+ --json machine-readable report
42
+
43
+ Checks that every client holds the SAME value for every replicated key, plus
44
+ one client that joins AFTER the match settles — the only observer that reads
45
+ the authoritative snapshot fresh, and so the only one that can see a value
46
+ erased from it.`);
47
+ process.exit(0);
48
+ }
49
+
50
+ const named = argv.find((a) => !a.startsWith('-') && a.endsWith('.json'));
51
+ if (!named) {
52
+ console.error('incanto-multiplay: name the scene, e.g. src/game.scene.json');
53
+ process.exit(2);
54
+ }
55
+ const scenePath = resolve(named);
56
+ if (!existsSync(scenePath)) {
57
+ console.error(`no such scene: ${scenePath}`);
58
+ process.exit(1);
59
+ }
60
+
61
+ const { playMultiplayer, multiplayText, registerAllNodes } = await import(
62
+ pathToFileURL(join(PKG, 'dist', 'test.js')).href
63
+ );
64
+ registerAllNodes();
65
+
66
+ const sceneJson = JSON.parse(readFileSync(scenePath, 'utf-8'));
67
+ const sceneDir = dirname(scenePath);
68
+
69
+ // Without the game's own behaviours a scene that names one throws
70
+ // `Unknown behavior` before a single frame runs — so load them when we are
71
+ // given them, and stub what is left. A stubbed behaviour still lets the
72
+ // SCENE's replication run, which is what this command measures.
73
+ const behaviorsFile = flag('--behaviors', null);
74
+ if (behaviorsFile) {
75
+ try {
76
+ const mod = await import(pathToFileURL(resolve(behaviorsFile)).href);
77
+ const { registerBehavior, Behavior } = await import(
78
+ pathToFileURL(join(PKG, 'dist', 'index.js')).href
79
+ );
80
+ for (const [name, value] of Object.entries(mod)) {
81
+ if (typeof value === 'function' && value.prototype instanceof Behavior) {
82
+ registerBehavior(name, value, { replace: true });
83
+ }
84
+ }
85
+ } catch (e) {
86
+ console.error(`could not load --behaviors '${behaviorsFile}' (${e?.message ?? e}).`);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Scenes a `NetworkSpawner` names.
93
+ *
94
+ * `--scenes remote-player=src/remote.scene.json` is explicit; otherwise every
95
+ * spawner's `scene` prop is looked for as a sibling file, which is where the
96
+ * templates put them. A spawner whose scene cannot be found materialises
97
+ * nothing, and the agreement check reports that as `missing` rather than
98
+ * letting it read as "this game has no remote players".
99
+ */
100
+ const scenes = {};
101
+ for (let i = 0; i < argv.length; i++) {
102
+ if (argv[i] !== '--scenes') continue;
103
+ const [key, file] = String(argv[i + 1] ?? '').split('=');
104
+ if (key && file) scenes[key] = JSON.parse(readFileSync(resolve(sceneDir, file), 'utf-8'));
105
+ }
106
+ const spawnerScenes = new Set();
107
+ const walk = (node) => {
108
+ if (node?.type === 'NetworkSpawner' && typeof node.props?.scene === 'string') {
109
+ spawnerScenes.add(node.props.scene);
110
+ }
111
+ for (const child of node?.children ?? []) walk(child);
112
+ };
113
+ walk(sceneJson.root);
114
+ for (const name of spawnerScenes) {
115
+ if (scenes[name]) continue;
116
+ for (const guess of [`${name}.scene.json`, `${name}.json`]) {
117
+ const at = resolve(sceneDir, guess);
118
+ if (existsSync(at)) {
119
+ scenes[name] = JSON.parse(readFileSync(at, 'utf-8'));
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ let report;
126
+ try {
127
+ report = await playMultiplayer({
128
+ scene: sceneJson,
129
+ scenes,
130
+ players: Number(flag('--players', 2)) || 2,
131
+ seconds: Number(flag('--seconds', 5)) || 5,
132
+ seed: Number(flag('--seed', 1)) || 1,
133
+ lateJoin: !argv.includes('--no-late-join'),
134
+ load: {
135
+ stubMissingBehaviors: true,
136
+ resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
137
+ },
138
+ // Move the owner, or every replicated key holds its authored value and two
139
+ // motionless clients agree about nothing in particular.
140
+ drive: (player, frame) => {
141
+ const root = player.engine.scene?.root;
142
+ const owner = root && findOwner(root);
143
+ if (!owner || !Array.isArray(owner.position)) return;
144
+ const spin = (player.account.charCodeAt(1) || 1) * 0.7;
145
+ owner.position = owner.position.map((v, i) =>
146
+ i === 1 && owner.position.length === 3 ? v : Math.sin(frame / 20 + spin + i) * 4,
147
+ );
148
+ },
149
+ });
150
+ } catch (e) {
151
+ console.error(`multiplay failed: ${e?.message ?? e}`);
152
+ process.exit(1);
153
+ }
154
+
155
+ /** The single `network.mode === 'owner'` node, the way the manager finds it. */
156
+ function findOwner(node) {
157
+ if (node.network?.mode === 'owner') return node;
158
+ for (const child of node.children ?? []) {
159
+ const found = findOwner(child);
160
+ if (found) return found;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ if (argv.includes('--json')) {
166
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
167
+ } else {
168
+ process.stdout.write(`${multiplayText(report)}\n`);
169
+ }
170
+ process.exit(report.ok ? 0 : 1);
@@ -19,7 +19,7 @@
19
19
  * what to arrange, and says so in the summary.
20
20
  */
21
21
  import { spawnSync } from 'node:child_process';
22
- import { existsSync, readdirSync, statSync } from 'node:fs';
22
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
23
23
  import { dirname, join, relative } from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
 
@@ -35,18 +35,21 @@ const flag = (name) => {
35
35
  return i >= 0 ? argv[i + 1] : null;
36
36
  };
37
37
  if (argv.includes('--help') || argv.includes('-h')) {
38
- console.log(`Usage: incanto-verify [scene.json] [--behaviors FILE] [--json]
38
+ console.log(`Usage: incanto verify [scene.json] [--behaviors FILE] [--json]
39
39
 
40
40
  Walks the whole ladder and tells you the ONE thing to do next:
41
41
 
42
42
  loads the scene is legal and every asset resolves (incanto-check)
43
43
  plays a seeded run can actually finish it (incanto-playtest)
44
+ feels the sounds and effects it declares actually fired (engine.audio/effects)
45
+ agrees two clients hold the same replicated values (incanto-multiplay)
44
46
  draws the GPU drew something and the subject is in shot (incanto-frame)
45
47
  says the running game logs no errors and lost no assets (incanto-logs)
46
48
 
47
49
  Without a scene it looks for one *.scene.json under the current directory.
48
- "draws" needs a dev server with the page open; without one it is reported as
49
- NOT MEASURED rather than failed a missing measurement is not a broken game.`);
50
+ "draws" needs a dev server with the page open, and "agrees" needs a scene with
51
+ a "multiplayer" header; without either they are reported as NOT MEASURED rather
52
+ than failed — a missing measurement is not a broken game.`);
50
53
  process.exit(0);
51
54
  }
52
55
 
@@ -274,6 +277,67 @@ if (rungs[0].status === 'pass') {
274
277
  }
275
278
  }
276
279
 
280
+ // ---- agrees --------------------------------------------------------------
281
+ // A desync is the purest silent failure this engine has: both clients run,
282
+ // neither errors, and they are playing different games. Skipped outright for a
283
+ // single-player game, because a rung that always passes is decoration.
284
+ {
285
+ const isMultiplayer = (() => {
286
+ try {
287
+ const doc = JSON.parse(readFileSync(scene, 'utf-8'));
288
+ return Boolean(doc?.multiplayer);
289
+ } catch {
290
+ return false;
291
+ }
292
+ })();
293
+ if (!isMultiplayer) {
294
+ rungs.push({
295
+ name: 'agrees',
296
+ status: 'skipped',
297
+ summary: 'not run — this scene has no `multiplayer` header',
298
+ });
299
+ } else {
300
+ const args = [scene, '--json', '--seconds', '4'];
301
+ if (behaviors) args.push('--behaviors', behaviors);
302
+ const r = run('multiplay', args);
303
+ const out = safeJson(r.stdout);
304
+ if (!out) {
305
+ rungs.push({
306
+ name: 'agrees',
307
+ status: 'unmeasured',
308
+ summary: `the match could not run — ${firstLine(r.stderr) || `exit ${r.status}`}`,
309
+ fix: `see it in full: \`incanto multiplay ${scene}\``,
310
+ });
311
+ } else if (out.keysCompared === 0) {
312
+ // Nothing moved, so nothing was compared. Saying "pass" here is how a
313
+ // rung becomes decorative.
314
+ rungs.push({
315
+ name: 'agrees',
316
+ status: 'unmeasured',
317
+ summary: 'no owner node declared any sync keys, so agreement was not tested',
318
+ fix: 'give the player node `network: { mode: "owner", sync: ["position"] }`',
319
+ });
320
+ } else if (out.ok) {
321
+ rungs.push({
322
+ name: 'agrees',
323
+ status: 'pass',
324
+ summary:
325
+ `${out.keysCompared} replicated value(s) match across clients` +
326
+ `${out.lateJoinChecked ? ' and a late joiner' : ''}` +
327
+ ' — over one in-memory server, not a live wire',
328
+ });
329
+ } else {
330
+ const first = out.disagreements?.[0];
331
+ rungs.push({
332
+ name: 'agrees',
333
+ status: 'fail',
334
+ summary: first ? first.summary : (out.problems?.[0] ?? 'the clients disagreed'),
335
+ fix: `every disagreement: \`incanto multiplay ${scene}\``,
336
+ });
337
+ }
338
+ }
339
+ }
340
+
277
341
  // ---- draws ---------------------------------------------------------------
278
342
  {
279
343
  const r = run('frame', ['--json']);
package/bin/incanto.mjs CHANGED
@@ -35,6 +35,7 @@ const COMMANDS = {
35
35
  check: ['incanto-check.mjs', 'validate scene JSON and the assets it names'],
36
36
  verify: ['incanto-verify.mjs', 'the whole ladder: loads · plays · feels · draws · says'],
37
37
  playtest: ['incanto-playtest.mjs', 'drive the game headlessly and report what happened'],
38
+ multiplay: ['incanto-multiplay.mjs', 'run a real match and check the clients agree'],
38
39
  play: ['incanto-play.mjs', 'run a scene headlessly for a fixed span'],
39
40
  feel: ['incanto-feel.mjs', 'measure jump height, run speed, facing'],
40
41
  frame: ['incanto-frame.mjs', 'what the running page is drawing right now'],
package/dist/2d.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { At as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-l08AEbq9.js";
1
+ import { At as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DWKTUzKI.js";
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 { r as FrameStats } from "./frame-report-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-BbEMTuWg.js";
5
+ import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
6
6
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
7
7
  import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
8
8
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
@@ -107,6 +107,9 @@ declare class PhysicsBody2D extends Node2D {
107
107
  private _collider;
108
108
  /** @internal Physics resync flag — raised when `collider` is REPLACED. */
109
109
  _colliderChanged: boolean;
110
+ private _enabled;
111
+ /** @internal Physics resync flag — raised when `enabled` is written. */
112
+ _enabledChanged: boolean;
110
113
  /**
111
114
  * Collider shape prop. Change it by REPLACING the object
112
115
  * (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
@@ -114,6 +117,9 @@ declare class PhysicsBody2D extends Node2D {
114
117
  */
115
118
  get collider(): JsonObject;
116
119
  set collider(value: JsonObject);
120
+ /** See the `enabled` prop. Writing it arms or disarms the existing collider. */
121
+ get enabled(): boolean;
122
+ set enabled(value: boolean);
117
123
  /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
118
124
  static validateJson(node: Node): void;
119
125
  /** @internal Set by Physics2D when the body is created. */
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-BZwWJIns.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-C6ZBFRjd.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-EqA-jddf.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-ClnIb_M5.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-BSu2dWGC.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-DqdVp1bt.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
package/dist/3d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { At as Node, D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-l08AEbq9.js";
1
+ import { At as Node, D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-DWKTUzKI.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  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-DCnHFmto.js";
5
- import { t as LoadSceneOptions } from "./loader-BbEMTuWg.js";
5
+ import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
6
6
  import { r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
7
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-mEN4V1CU.js";
7
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-BqWBb0kh.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";
10
10
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -333,6 +333,9 @@ declare class PhysicsBody3D extends Node3D {
333
333
  private _collider;
334
334
  /** @internal Physics resync flag — raised when `collider` is REPLACED. */
335
335
  _colliderChanged: boolean;
336
+ private _enabled;
337
+ /** @internal Physics resync flag — raised when `enabled` is written. */
338
+ _enabledChanged: boolean;
336
339
  /**
337
340
  * Collider shape prop. Change it by REPLACING the object
338
341
  * (`body.collider = { shape: 'box', size: [...] }`) — physics rebuilds the
@@ -340,6 +343,9 @@ declare class PhysicsBody3D extends Node3D {
340
343
  */
341
344
  get collider(): JsonObject;
342
345
  set collider(value: JsonObject);
346
+ /** See the `enabled` prop. Writing it arms or disarms the existing collider. */
347
+ get enabled(): boolean;
348
+ set enabled(value: boolean);
343
349
  /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
344
350
  static validateJson(node: Node): void;
345
351
  /** @internal Set by Physics3D when the body is created. */
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-BSMny7oe.js";
2
- import { B as StaticBody3D, F as WaterCutout3D, I as Area3D, L as CharacterBody3D, N as Water3D, P as WATER_CUTOUT_MAX, R as PhysicsBody3D, V as Node3D, W as WATER_MAX_RIPPLES, z as RigidBody3D } from "./gameplay-BVphcxmE.js";
3
- 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-CvvQr_bJ.js";
4
- 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-D10bU5_J.js";
2
+ import { B as StaticBody3D, F as WaterCutout3D, I as Area3D, L as CharacterBody3D, N as Water3D, P as WATER_CUTOUT_MAX, R as PhysicsBody3D, V as Node3D, W as WATER_MAX_RIPPLES, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
3
+ 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-8cjF3t6w.js";
4
+ 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-BCm38FJV.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-Dnz4fsXX.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BP0DZb_1.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;
@@ -1033,6 +1033,16 @@ declare class Localization {
1033
1033
  * `startsWith` per widget per frame and nothing else.
1034
1034
  */
1035
1035
  resolve(value: string, params?: Record<string, JsonValue>): string;
1036
+ /**
1037
+ * Whether the ACTIVE locale or the base declares this key.
1038
+ *
1039
+ * This is the runtime question, and it is not the same as `has`: a key only
1040
+ * `ko` declares is "declared", and an English player still gets the raw key
1041
+ * back from `t()`, because the fallback chain is current → base → the key.
1042
+ * Widgets that ask before translating asked the wrong one and painted
1043
+ * `settings.quality.low` into an English menu.
1044
+ */
1045
+ declares(key: string): boolean;
1036
1046
  /** Whether any locale declares this key — for the audit, not for the runtime. */
1037
1047
  has(key: string): boolean;
1038
1048
  }
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as registerBehavior, C as diagnose, n as loadScene } from "./loader-BcrRSjxB.js";
3
- import { _ as Engine, g as AudioPlayer, x as qualityRendering, y as qualityCaps } from "./register-Ch70uByv.js";
2
+ import { j as registerBehavior, n as loadScene, w as diagnose } from "./loader-D8n7TU8W.js";
3
+ import { S as qualityRendering, _ as Engine, b as qualityCaps, g as AudioPlayer } from "./register-Da3hXh2H.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-Cs113S9F.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CTTwhWSe.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { R as PhysicsBody3D, U as createCausticsQuad, V as Node3D, n as registerGameplayBehaviors } from "./gameplay-BVphcxmE.js";
8
+ import { R as PhysicsBody3D, U as createCausticsQuad, V as Node3D, n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.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-CvvQr_bJ.js";
11
- import { n as enablePhysics3D } from "./physics-3d-Dnz4fsXX.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-8cjF3t6w.js";
11
+ import { n as enablePhysics3D } from "./physics-3d-BP0DZb_1.js";
12
12
  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";
13
13
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
14
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as registerBehavior, C as diagnose, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-BcrRSjxB.js";
3
- import { _ as Engine, g as AudioPlayer } from "./register-Ch70uByv.js";
2
+ import { c as resolveViewport, j as registerBehavior, n as loadScene, s as computeViewport, w as diagnose } from "./loader-D8n7TU8W.js";
3
+ import { _ as Engine, g as AudioPlayer } from "./register-Da3hXh2H.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-Cs113S9F.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CTTwhWSe.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { n as registerGameplayBehaviors } from "./gameplay-BVphcxmE.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-C6ZBFRjd.js";
8
+ import { n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
9
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BSu2dWGC.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { n as enablePhysics2D } from "./physics-2d-EqA-jddf.js";
11
+ import { n as enablePhysics2D } from "./physics-2d-DqdVp1bt.js";
12
12
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
13
13
  //#region src/2d/assets.ts
14
14
  /**
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Pt as LogLevel, T as RendererStats, b as Engine } from "./behavior-l08AEbq9.js";
1
+ import { Pt as LogLevel, T as RendererStats, b as Engine } from "./behavior-DWKTUzKI.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/debug.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as jsonClone } from "./json-BLk7H2Qa.js";
2
- import { s as mergeStaticProps } from "./registry-C7u42TID.js";
1
+ import { t as jsonClone } from "./json-CwwhxQgb.js";
2
+ import { s as mergeStaticProps } from "./registry-WWcQcfMr.js";
3
3
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
4
  //#region src/debug/capture.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-BcrRSjxB.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-D8n7TU8W.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
@@ -1,11 +1,11 @@
1
- import { C as diagnose } from "./loader-BcrRSjxB.js";
2
- import { t as registerCoreNodes } from "./register-Ch70uByv.js";
1
+ import { w as diagnose } from "./loader-D8n7TU8W.js";
2
+ import { I as translateOn, t as registerCoreNodes } from "./register-Da3hXh2H.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-C7u42TID.js";
5
+ import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
6
6
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
7
- import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-C0wXLBZJ.js";
8
- import { B as StaticBody3D, F as WaterCutout3D, G as colliderFootDrop, I as Area3D, L as CharacterBody3D, N as Water3D, R as PhysicsBody3D, V as Node3D, w as tolerateUnknownAction, z as RigidBody3D } from "./gameplay-BVphcxmE.js";
7
+ import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
8
+ import { B as StaticBody3D, F as WaterCutout3D, G as colliderFootDrop, I as Area3D, L as CharacterBody3D, N as Water3D, R as PhysicsBody3D, V as Node3D, w as tolerateUnknownAction, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
9
9
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
10
10
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
11
11
  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";
@@ -5837,8 +5837,9 @@ var Label3D = class extends Sprite3D {
5837
5837
  canvasTexture = null;
5838
5838
  lastKey = "";
5839
5839
  resolveTexture(_assets) {
5840
- if (this.text === "" || typeof document === "undefined") return null;
5841
- const key = `${this.text}\0${this.height}\0${this.color}\0${this.font}\0${this.outline}`;
5840
+ const text = translateOn(this, this.text);
5841
+ if (text === "" || typeof document === "undefined") return null;
5842
+ const key = `${text}\0${this.height}\0${this.color}\0${this.font}\0${this.outline}`;
5842
5843
  if (key !== this.lastKey) {
5843
5844
  this.lastKey = key;
5844
5845
  this.rasterize();
@@ -5847,6 +5848,7 @@ var Label3D = class extends Sprite3D {
5847
5848
  }
5848
5849
  /** Draw the text once, and size the quad to what it measured. */
5849
5850
  rasterize() {
5851
+ const text = translateOn(this, this.text);
5850
5852
  const fontPx = Math.max(8, Math.round(this.height * 256));
5851
5853
  this.canvas ??= document.createElement("canvas");
5852
5854
  const ctx = this.canvas.getContext("2d");
@@ -5854,7 +5856,7 @@ var Label3D = class extends Sprite3D {
5854
5856
  const stroke = Math.max(1, Math.round(fontPx * .14));
5855
5857
  const fontSpec = `bold ${fontPx}px ${this.font}`;
5856
5858
  ctx.font = fontSpec;
5857
- const width = Math.max(1, Math.ceil(ctx.measureText(this.text).width) + stroke * 2);
5859
+ const width = Math.max(1, Math.ceil(ctx.measureText(text).width) + stroke * 2);
5858
5860
  const lineHeight = Math.ceil(fontPx * 1.35) + stroke * 2;
5859
5861
  this.canvas.width = width;
5860
5862
  this.canvas.height = lineHeight;
@@ -5865,10 +5867,10 @@ var Label3D = class extends Sprite3D {
5865
5867
  ctx.lineWidth = stroke;
5866
5868
  ctx.lineJoin = "round";
5867
5869
  ctx.strokeStyle = this.outline;
5868
- ctx.strokeText(this.text, width / 2, lineHeight / 2);
5870
+ ctx.strokeText(text, width / 2, lineHeight / 2);
5869
5871
  }
5870
5872
  ctx.fillStyle = this.color;
5871
- ctx.fillText(this.text, width / 2, lineHeight / 2);
5873
+ ctx.fillText(text, width / 2, lineHeight / 2);
5872
5874
  this.canvasTexture?.dispose();
5873
5875
  const texture = new CanvasTexture(this.canvas);
5874
5876
  texture.colorSpace = SRGBColorSpace;