incanto 0.59.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 (34) hide show
  1. package/bin/incanto-check.mjs +45 -4
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.js +3 -3
  4. package/dist/{create-game-BiW8Men_.js → create-game-BCm38FJV.js} +4 -4
  5. package/dist/{create-game-CHDLDQsQ.js → create-game-ClnIb_M5.js} +4 -4
  6. package/dist/{environment-presets-DRAz5EV9.js → environment-presets-8cjF3t6w.js} +1 -1
  7. package/dist/index.d.ts +18 -1
  8. package/dist/index.js +3 -3
  9. package/dist/net.js +1 -1
  10. package/dist/{physics-2d-BaRSRrrZ.js → physics-2d-DqdVp1bt.js} +1 -1
  11. package/dist/{physics-3d-CYxjh-HW.js → physics-3d-BP0DZb_1.js} +1 -1
  12. package/dist/react.js +1 -1
  13. package/dist/{register-BpFcgdcL.js → register-BSu2dWGC.js} +1 -1
  14. package/dist/{register-CDrAQqPp.js → register-Da3hXh2H.js} +47 -5
  15. package/dist/{replay-O-yAGM76.d.ts → replay-BCMK_VRP.d.ts} +14 -1
  16. package/dist/{replay-CEPyQtF_.js → replay-BlNuIDdg.js} +13 -9
  17. package/dist/{split-screen-DDMZutQ6.js → split-screen-CL5Yvxse.js} +1 -1
  18. package/dist/{src-CY21B462.js → src-DFpXBMJN.js} +1 -1
  19. package/dist/{teardown-RApWnM1G.js → teardown-CTTwhWSe.js} +1 -1
  20. package/dist/{test-DHYuFyAu.js → test-BeZ95pqw.js} +14 -10
  21. package/dist/test.d.ts +17 -1
  22. package/dist/test.js +2 -2
  23. package/dist/vite.js +2 -2
  24. package/editor/assets/{agent8-BoRGtVxK.js → agent8-DSJries_.js} +1 -1
  25. package/editor/assets/{debug-CzdyCg75.js → debug-D15Wi5TO.js} +1 -1
  26. package/editor/assets/{index-VesuVEhe.js → index-BjC88k97.js} +3 -3
  27. package/editor/index.html +1 -1
  28. package/package.json +1 -1
  29. package/skills/incanto-localization.md +24 -0
  30. package/templates-app/beacon-isle-3d/package.json +1 -1
  31. package/templates-app/platformer-2d/package.json +1 -1
  32. package/templates-app/star-survivor/package.json +1 -1
  33. package/templates-app/tps-3d/package.json +1 -1
  34. 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;
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-CHDLDQsQ.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-BpFcgdcL.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BaRSRrrZ.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.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
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-DRAz5EV9.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-BiW8Men_.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-CYxjh-HW.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;
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
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-CDrAQqPp.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-RApWnM1G.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
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-DRAz5EV9.js";
11
- import { n as enablePhysics3D } from "./physics-3d-CYxjh-HW.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
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-CDrAQqPp.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-RApWnM1G.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
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-BpFcgdcL.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-BaRSRrrZ.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
  /**
@@ -1,5 +1,5 @@
1
1
  import { w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-CDrAQqPp.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
5
  import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { n as loadScene, t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
4
4
  import { i as resolveFrames, n as AnimationEntry, r as resolveAnimation, t as AnimationDef } from "./sprite-animation-CMr6f1K2.js";
5
5
  import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-C5OfBbmU.js";
6
6
  import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-BqWBb0kh.js";
7
- import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-O-yAGM76.js";
7
+ import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-BCMK_VRP.js";
8
8
 
9
9
  //#region src/core/audio/crossfade.d.ts
10
10
  /**
@@ -417,6 +417,18 @@ declare abstract class HudWidgetBase extends Node {
417
417
  * was empty at bake time and the English was permanent even for a game that
418
418
  * boots in another language.
419
419
  */
420
+ /**
421
+ * What this widget actually PAINTS, when that differs from its props.
422
+ *
423
+ * A headless capture printed `text="@t:menu.start"` — byte-identical in
424
+ * every language — so a passing capture proved nothing about localization
425
+ * and a broken translation was invisible to every check the engine has.
426
+ * `incanto-frame` reads the WebGL buffer and cannot read DOM text at all.
427
+ *
428
+ * `undefined` when the painted words ARE the prop: recording it twice would
429
+ * be noise on every widget in a game that never localizes anything.
430
+ */
431
+ _paintedText(): string | undefined;
420
432
  protected _labelText(): string;
421
433
  protected _tOr(key: string, fallback: string): string;
422
434
  }
@@ -456,6 +468,7 @@ declare class UiText extends HudWidgetBase {
456
468
  * that starts blank looks broken rather than empty.
457
469
  */
458
470
  get shown(): string;
471
+ override _paintedText(): string | undefined;
459
472
  protected _build(): HTMLElement;
460
473
  /** The whole appearance as one string, so re-applying is one comparison. */
461
474
  private look;
@@ -604,6 +617,10 @@ declare class UiDialogue extends HudWidgetBase {
604
617
  clear(): void;
605
618
  private next;
606
619
  protected _build(): HTMLElement;
620
+ /** The locale `current` was resolved in, so a switch is one compare away. */
621
+ private resolvedIn;
622
+ /** Re-read the live line from its untouched source. */
623
+ private resolveCurrent;
607
624
  override update(dt: number): void;
608
625
  }
609
626
  //#endregion
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { A as getBehavior, C as Node, D as behaviorSchema, E as Behavior, M as registeredBehaviors, N as Signal, O as behaviorSignals, S as InputMap, T as parseNodePath, _ as savesWithoutUid, a as serializeNode, b as effectiveOrder, c as resolveViewport, d as isConstRef, f as resolveConstants, g as restoreBehaviors, h as captureBehaviors, i as Scene, j as registerBehavior, k as clearBehaviors, l as SceneTree, m as behaviorsWithoutSave, n as loadScene, o as SCENE_FORMAT, p as SaveSlots, s as computeViewport, u as CONST_REF_KEY, v as createSaveStore, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-D8n7TU8W.js";
2
- import { A as UiBar, B as ROLLOFF_MODELS, C as readDeviceHints, D as HudLayer, E as EffectLog, F as suggestLocale, G as synthSfx, H as spatialPan, J as crossfadeGains, K as MusicManager, L as translationKey, M as BASE_LOCALE, N as Localization, P as T_PREFIX, R as SfxEngine, T as LogManager, U as SFX_PRESETS, V as spatialGain, W as SFX_PRESET_NAMES, X as AudioBuses, Y as fadeGain, _ as Engine, a as UiMuteToggle, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as AudioPlayer, h as UiDialogue, i as UiLanguageSelect, j as UiText, k as UiBanner, l as UiSelect, m as UiButton, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as WebAudioMusicBackend, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, w as suggestQuality, x as qualityEnvironment, y as Settings, z as isAudioContextAvailable } from "./register-CDrAQqPp.js";
2
+ import { A as UiBar, B as ROLLOFF_MODELS, C as readDeviceHints, D as HudLayer, E as EffectLog, F as suggestLocale, G as synthSfx, H as spatialPan, J as crossfadeGains, K as MusicManager, L as translationKey, M as BASE_LOCALE, N as Localization, P as T_PREFIX, R as SfxEngine, T as LogManager, U as SFX_PRESETS, V as spatialGain, W as SFX_PRESET_NAMES, X as AudioBuses, Y as fadeGain, _ as Engine, a as UiMuteToggle, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as AudioPlayer, h as UiDialogue, i as UiLanguageSelect, j as UiText, k as UiBanner, l as UiSelect, m as UiButton, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as WebAudioMusicBackend, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, w as suggestQuality, x as qualityEnvironment, y as Settings, z as isAudioContextAvailable } 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
5
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-CwwhxQgb.js";
6
6
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-WWcQcfMr.js";
7
- import { a as nodeRefWarnings, i as describeRefProblem, n as startRecording, o as resolveRefInJson, r as auditScene, t as replay } from "./replay-CEPyQtF_.js";
7
+ import { a as nodeRefWarnings, i as describeRefProblem, n as startRecording, o as resolveRefInJson, r as auditScene, t as replay } from "./replay-BlNuIDdg.js";
8
8
  import { a as logReport, i as resolveRendering, n as attachTouchControls, o as logText, r as joystickVector, s as parseDrive, t as TouchControls } from "./touch-DESwnpOc.js";
9
9
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
10
10
  import { a as PARTICLE_PRESETS, i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
11
- import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-CY21B462.js";
11
+ import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-DFpXBMJN.js";
12
12
  import { t as duplicateNode } from "./duplicate-DJQd44CD.js";
13
13
  export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
package/dist/net.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
2
- import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-DDMZutQ6.js";
2
+ import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-CL5Yvxse.js";
3
3
  export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { w as diagnose } from "./loader-D8n7TU8W.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-BpFcgdcL.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-BSu2dWGC.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
7
  //#region src/2d/physics/physics-2d.ts
@@ -4,7 +4,7 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { H as validateCollider3D, I as Area3D, L as CharacterBody3D, R as PhysicsBody3D, V as Node3D, z as RigidBody3D } from "./gameplay-BBEjPFsR.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-DRAz5EV9.js";
7
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-8cjF3t6w.js";
8
8
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
9
9
  //#region src/3d/physics/collider-lines.ts
10
10
  /**
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BiW8Men_.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CHDLDQsQ.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BCm38FJV.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-ClnIb_M5.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,5 +1,5 @@
1
1
  import { C as Node, b as effectiveOrder, w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-CDrAQqPp.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
5
  import { i as getNodeSchema, l as registerNode } from "./registry-WWcQcfMr.js";
@@ -1584,6 +1584,18 @@ var HudWidgetBase = class extends Node {
1584
1584
  * was empty at bake time and the English was permanent even for a game that
1585
1585
  * boots in another language.
1586
1586
  */
1587
+ /**
1588
+ * What this widget actually PAINTS, when that differs from its props.
1589
+ *
1590
+ * A headless capture printed `text="@t:menu.start"` — byte-identical in
1591
+ * every language — so a passing capture proved nothing about localization
1592
+ * and a broken translation was invisible to every check the engine has.
1593
+ * `incanto-frame` reads the WebGL buffer and cannot read DOM text at all.
1594
+ *
1595
+ * `undefined` when the painted words ARE the prop: recording it twice would
1596
+ * be noise on every widget in a game that never localizes anything.
1597
+ */
1598
+ _paintedText() {}
1587
1599
  _labelText() {
1588
1600
  const label = this.label;
1589
1601
  return typeof label === "string" ? this._t(label) : "";
@@ -1642,6 +1654,10 @@ var UiText = class extends HudWidgetBase {
1642
1654
  if (this.format && this.slot !== null) return this._t(this.format).replace("{}", this.slot);
1643
1655
  return this._t(this.text);
1644
1656
  }
1657
+ _paintedText() {
1658
+ const painted = this.shown;
1659
+ return painted === this.text ? void 0 : painted;
1660
+ }
1645
1661
  _build() {
1646
1662
  const el = document.createElement("div");
1647
1663
  el.style.cssText = this.look();
@@ -1812,6 +1828,7 @@ var UiBanner = class extends HudWidgetBase {
1812
1828
  show(text, opts) {
1813
1829
  this.queue.push({
1814
1830
  text,
1831
+ source: text,
1815
1832
  color: opts?.color ?? "#ffffff",
1816
1833
  seconds: opts?.seconds ?? this.seconds
1817
1834
  });
@@ -1844,6 +1861,11 @@ var UiBanner = class extends HudWidgetBase {
1844
1861
  this._element.style.opacity = opacity;
1845
1862
  }
1846
1863
  if (this.current) {
1864
+ const next = this._t(this.current.source);
1865
+ if (next !== this.current.text) {
1866
+ this.current.text = next;
1867
+ if (this._element) this._element.textContent = next;
1868
+ }
1847
1869
  if (this.current.seconds > 0) {
1848
1870
  this.remaining -= dt;
1849
1871
  if (this.remaining <= 0) {
@@ -1854,7 +1876,7 @@ var UiBanner = class extends HudWidgetBase {
1854
1876
  }
1855
1877
  if (!this.current && this.queue.length > 0) {
1856
1878
  this.current = this.queue.shift();
1857
- this.current.text = this._t(this.current.text);
1879
+ this.current.text = this._t(this.current.source);
1858
1880
  this.remaining = this.current.seconds;
1859
1881
  if (this._element) {
1860
1882
  this._element.textContent = this.current.text;
@@ -3107,10 +3129,14 @@ var UiDialogue = class extends HudWidgetBase {
3107
3129
  }
3108
3130
  /** Queue a line; optional `choices` renders buttons after the line types out. */
3109
3131
  say(speaker, text, choices) {
3110
- this.queue.push({
3132
+ const source = {
3111
3133
  speaker,
3112
3134
  text,
3113
3135
  choices: choices ?? null
3136
+ };
3137
+ this.queue.push({
3138
+ ...source,
3139
+ source
3114
3140
  });
3115
3141
  }
3116
3142
  /**
@@ -3164,13 +3190,22 @@ var UiDialogue = class extends HudWidgetBase {
3164
3190
  box.addEventListener("click", () => this.advance());
3165
3191
  return box;
3166
3192
  }
3193
+ /** The locale `current` was resolved in, so a switch is one compare away. */
3194
+ resolvedIn = "\0";
3195
+ /** Re-read the live line from its untouched source. */
3196
+ resolveCurrent() {
3197
+ const line = this.current;
3198
+ if (!line) return;
3199
+ line.text = this._t(line.source.text);
3200
+ line.speaker = this._t(line.source.speaker);
3201
+ line.choices = line.source.choices ? line.source.choices.map((c) => this._t(c)) : null;
3202
+ this.resolvedIn = this.tree?.engine?.locale.locale ?? "";
3203
+ }
3167
3204
  update(dt) {
3168
3205
  super.update(dt);
3169
3206
  if (!this.current && this.queue.length > 0) {
3170
3207
  this.current = this.queue.shift();
3171
- this.current.text = this._t(this.current.text);
3172
- this.current.speaker = this._t(this.current.speaker);
3173
- if (this.current.choices) this.current.choices = this.current.choices.map((c) => this._t(c));
3208
+ this.resolveCurrent();
3174
3209
  this.revealed = this.charsPerSecond <= 0 ? this.current.text.length : 0;
3175
3210
  this.emit("lineShown", this.current.text);
3176
3211
  if (this._element) {
@@ -3180,6 +3215,13 @@ var UiDialogue = class extends HudWidgetBase {
3180
3215
  }
3181
3216
  if (this._element) this._element.style.display = this.visible && this.current ? "block" : "none";
3182
3217
  if (!this.current) return;
3218
+ if ((this.tree?.engine?.locale.locale ?? "") !== this.resolvedIn) {
3219
+ const before = this.current.text.length;
3220
+ const progress = before > 0 ? this.revealed / before : 1;
3221
+ this.resolveCurrent();
3222
+ this.revealed = Math.round(progress * this.current.text.length);
3223
+ if (this.speakerEl) this.speakerEl.textContent = this.current.speaker;
3224
+ }
3183
3225
  const wasTyping = this.revealed < this.current.text.length;
3184
3226
  if (wasTyping) this.revealed = Math.min(this.current.text.length, this.revealed + this.charsPerSecond * dt);
3185
3227
  const doneTyping = this.revealed >= this.current.text.length;
@@ -3,7 +3,20 @@ import { s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
5
5
  /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
6
- declare function auditScene(scene: JsonObject): string[];
6
+ interface AuditOptions {
7
+ /**
8
+ * Every locale table the whole PROJECT declares, unioned.
9
+ *
10
+ * Localization tables MERGE across scenes — a game's shared UI strings live
11
+ * in the scene it boots from, and a level that adds three lines of its own
12
+ * does not wipe them. Auditing one file in isolation therefore reported a key
13
+ * declared in the boot scene as "nothing declares" it, and told its author to
14
+ * add a string they already have. The checker passes the corpus in when it
15
+ * has one.
16
+ */
17
+ declaredElsewhere?: Record<string, Record<string, string>>;
18
+ }
19
+ declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
7
20
  //#endregion
8
21
  //#region src/core/errors.d.ts
9
22
  /**
@@ -1,5 +1,5 @@
1
1
  import { D as behaviorSchema, T as parseNodePath } from "./loader-D8n7TU8W.js";
2
- import { L as translationKey, O as HudWidgetBase } from "./register-CDrAQqPp.js";
2
+ import { L as translationKey, O as HudWidgetBase } from "./register-Da3hXh2H.js";
3
3
  import { i as getNodeSchema, o as getNodeType } from "./registry-WWcQcfMr.js";
4
4
  //#region src/core/node-refs.ts
5
5
  /**
@@ -266,8 +266,7 @@ function isLit(environment) {
266
266
  const ambient = environment.ambient;
267
267
  return typeof ambient?.intensity === "number" && ambient.intensity > 0;
268
268
  }
269
- /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
270
- function auditScene(scene) {
269
+ function auditScene(scene, opts = {}) {
271
270
  const warnings = [];
272
271
  const root = scene.root;
273
272
  if (!root) return warnings;
@@ -301,7 +300,7 @@ function auditScene(scene) {
301
300
  if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
302
301
  if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
303
302
  for (const warning of nodeRefWarnings(scene)) warnings.push(warning);
304
- for (const warning of auditStrings(scene)) warnings.push(warning);
303
+ for (const warning of auditStrings(scene, opts.declaredElsewhere)) warnings.push(warning);
305
304
  return warnings;
306
305
  }
307
306
  /**
@@ -323,11 +322,16 @@ function auditScene(scene) {
323
322
  * not trip it.
324
323
  */
325
324
  const NEAR_MISS = /^\s*@\s*t\s*[:.]/i;
326
- function auditStrings(scene) {
327
- const tables = scene.strings;
325
+ function auditStrings(scene, elsewhere) {
326
+ const own = scene.strings;
327
+ const tables = {};
328
+ for (const source of [elsewhere, own]) for (const [locale, entries] of Object.entries(source ?? {})) tables[locale] = {
329
+ ...tables[locale] ?? {},
330
+ ...entries
331
+ };
328
332
  const declared = /* @__PURE__ */ new Set();
329
- for (const entries of Object.values(tables ?? {})) for (const key of Object.keys(entries)) declared.add(key);
330
- const inBase = new Set(Object.keys(tables?.["en"] ?? {}));
333
+ for (const entries of Object.values(tables)) for (const key of Object.keys(entries)) declared.add(key);
334
+ const inBase = new Set(Object.keys(tables["en"] ?? {}));
331
335
  const missing = /* @__PURE__ */ new Set();
332
336
  const notInEnglish = /* @__PURE__ */ new Set();
333
337
  const nearMiss = /* @__PURE__ */ new Set();
@@ -337,7 +341,7 @@ function auditStrings(scene) {
337
341
  const key = translationKey(value);
338
342
  if (key !== null) {
339
343
  if (!declared.has(key)) missing.add(key);
340
- else if (tables && !inBase.has(key)) notInEnglish.add(key);
344
+ else if (declared.size > 0 && !inBase.has(key)) notInEnglish.add(key);
341
345
  }
342
346
  return;
343
347
  }
@@ -1,5 +1,5 @@
1
1
  import { C as Node, N as Signal, n as loadScene, t as buildNodeJson, w as diagnose } from "./loader-D8n7TU8W.js";
2
- import { _ as Engine, t as registerCoreNodes } from "./register-CDrAQqPp.js";
2
+ import { _ as Engine, t as registerCoreNodes } from "./register-Da3hXh2H.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { l as registerNode } from "./registry-WWcQcfMr.js";
@@ -214,6 +214,6 @@ function newUid() {
214
214
  //#endregion
215
215
  //#region src/index.ts
216
216
  /** Engine version. Kept in sync with package.json by the release pipeline. */
217
- const VERSION = "0.59.0";
217
+ const VERSION = "0.60.0";
218
218
  //#endregion
219
219
  export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
@@ -1,4 +1,4 @@
1
- import { g as AudioPlayer } from "./register-CDrAQqPp.js";
1
+ import { g as AudioPlayer } from "./register-Da3hXh2H.js";
2
2
  import { a as logReport, s as parseDrive } from "./touch-DESwnpOc.js";
3
3
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
4
4
  //#region src/core/audio-errors.ts
@@ -1,13 +1,13 @@
1
1
  import { c as resolveViewport, j as registerBehavior, n as loadScene } from "./loader-D8n7TU8W.js";
2
- import { _ as Engine } from "./register-CDrAQqPp.js";
2
+ import { _ as Engine } from "./register-Da3hXh2H.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-WWcQcfMr.js";
6
- import { n as startRecording } from "./replay-CEPyQtF_.js";
6
+ import { n as startRecording } from "./replay-BlNuIDdg.js";
7
7
  import { n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
8
- import { t as registerNodes2D } from "./register-BpFcgdcL.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-DRAz5EV9.js";
10
- import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-DDMZutQ6.js";
8
+ import { t as registerNodes2D } from "./register-BSu2dWGC.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-8cjF3t6w.js";
10
+ import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-CL5Yvxse.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
13
13
  /**
@@ -1669,7 +1669,7 @@ async function playMultiplayer(opts) {
1669
1669
  */
1670
1670
  async function joinLate(server, first, opts) {
1671
1671
  try {
1672
- const { Engine } = await import("./register-CDrAQqPp.js").then((n) => n.v);
1672
+ const { Engine } = await import("./register-Da3hXh2H.js").then((n) => n.v);
1673
1673
  const { loadScene } = await import("./loader-D8n7TU8W.js").then((n) => n.r);
1674
1674
  const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
1675
1675
  const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
@@ -1925,6 +1925,8 @@ function captureScene(scene) {
1925
1925
  type: ctor.typeName,
1926
1926
  props
1927
1927
  };
1928
+ const painted = node._paintedText?.();
1929
+ if (typeof painted === "string") capture.text = painted;
1928
1930
  if (node.uid) capture.uid = node.uid;
1929
1931
  if (node.groups.size > 0) capture.groups = [...node.groups];
1930
1932
  if (Object.keys(node.tags).length > 0) capture.tags = jsonClone(node.tags);
@@ -1951,6 +1953,7 @@ function describeCapture(capture) {
1951
1953
  const schema = defaultsFor(node.type);
1952
1954
  const deltas = Object.entries(node.props).filter(([key, value]) => !schema || !jsonEquals(value, schema[key]?.default ?? null)).map(([key, value]) => `${key}=${JSON.stringify(value)}`);
1953
1955
  const extras = [
1956
+ node.text !== void 0 ? `paints=${JSON.stringify(node.text)}` : "",
1954
1957
  node.script ? `script=${node.script}` : "",
1955
1958
  node.groups ? `groups=${node.groups.join(",")}` : "",
1956
1959
  ...deltas
@@ -2021,12 +2024,13 @@ async function runScript(json, opts) {
2021
2024
  });
2022
2025
  const scene = loadScene(structuredClone(json), { resolveScene: opts.resolveScene });
2023
2026
  engine.setScene(scene);
2027
+ if (opts.locale) engine.locale.locale = opts.locale;
2024
2028
  const physics = opts.physics ?? "auto";
2025
2029
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2026
- const { enablePhysics2D } = await import("./physics-2d-BaRSRrrZ.js").then((n) => n.r);
2030
+ const { enablePhysics2D } = await import("./physics-2d-DqdVp1bt.js").then((n) => n.r);
2027
2031
  await enablePhysics2D(engine);
2028
2032
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2029
- const { enablePhysics3D } = await import("./physics-3d-CYxjh-HW.js").then((n) => n.r);
2033
+ const { enablePhysics3D } = await import("./physics-3d-BP0DZb_1.js").then((n) => n.r);
2030
2034
  await enablePhysics3D(engine);
2031
2035
  }
2032
2036
  const failures = [];
@@ -2139,10 +2143,10 @@ async function createPlaySession(json, opts = {}) {
2139
2143
  engine.setScene(scene);
2140
2144
  const physics = opts.physics ?? "auto";
2141
2145
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2142
- const { enablePhysics2D } = await import("./physics-2d-BaRSRrrZ.js").then((n) => n.r);
2146
+ const { enablePhysics2D } = await import("./physics-2d-DqdVp1bt.js").then((n) => n.r);
2143
2147
  await enablePhysics2D(engine);
2144
2148
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2145
- const { enablePhysics3D } = await import("./physics-3d-CYxjh-HW.js").then((n) => n.r);
2149
+ const { enablePhysics3D } = await import("./physics-3d-BP0DZb_1.js").then((n) => n.r);
2146
2150
  await enablePhysics3D(engine);
2147
2151
  }
2148
2152
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
package/dist/test.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { At as Node, Nt as LogEntry, P as Scene, b as Engine, n as BehaviorCtor } from "./behavior-DWKTUzKI.js";
2
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
  import { t as LoadSceneOptions } from "./loader-TvkRFbyL.js";
4
- import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-O-yAGM76.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-BCMK_VRP.js";
5
5
  import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-BQ3tAsf-.js";
6
6
 
7
7
  //#region src/test/facing.d.ts
@@ -490,6 +490,14 @@ interface NodeCapture {
490
490
  script?: string;
491
491
  /** CURRENT value of every schema prop (not delta — captures runtime state). */
492
492
  props: Record<string, JsonValue>;
493
+ /**
494
+ * What this node actually PAINTS, when that differs from its props.
495
+ *
496
+ * A capture printed `text="@t:menu.start"` — byte-identical in every
497
+ * language — so it proved nothing about localization. Absent unless the
498
+ * painted words differ from the authored ones.
499
+ */
500
+ text?: string;
493
501
  }
494
502
  interface SceneCapture {
495
503
  name: string;
@@ -564,6 +572,14 @@ interface RunScriptOptions {
564
572
  physics?: "2d" | "3d" | "auto" | false;
565
573
  /** Collect a snapshot every N simulated ms. */
566
574
  snapshotEveryMs?: number;
575
+ /**
576
+ * Run the whole script in this language.
577
+ *
578
+ * Without it every headless check ran in English, so a translation could
579
+ * only ever be verified by opening a browser and reading — which is exactly
580
+ * the loop the ladder exists to replace.
581
+ */
582
+ locale?: string;
567
583
  resolveScene?: LoadSceneOptions["resolveScene"];
568
584
  }
569
585
  interface RunResult {