incanto 0.54.0 → 0.56.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 (33) hide show
  1. package/bin/incanto-verify.mjs +46 -0
  2. package/dist/2d.js +1 -1
  3. package/dist/3d.js +4 -4
  4. package/dist/{create-game-Czzp6ZuE.js → create-game-CqJkfMzm.js} +3 -3
  5. package/dist/{create-game-BRt6XKmP.js → create-game-YvaNWhe6.js} +1 -1
  6. package/dist/debug.js +3 -1
  7. package/dist/editor.js +7 -0
  8. package/dist/{environment-presets-BQ_QsIBY.js → environment-presets-DLHqgNAF.js} +117 -1
  9. package/dist/{gameplay-CZ2yq37J.js → gameplay-C_SPzmUe.js} +75 -2
  10. package/dist/gameplay.d.ts +55 -1
  11. package/dist/gameplay.js +2 -2
  12. package/dist/index.js +1 -1
  13. package/dist/{physics-3d-Bes-GIuR.js → physics-3d-NzDwWPrF.js} +2 -2
  14. package/dist/react.js +1 -1
  15. package/dist/{src-vNPQeQ1W.js → src-51GA-9m-.js} +1 -1
  16. package/dist/{test-Tnf1nQl3.js → test-Bu6SPNsV.js} +29 -6
  17. package/dist/test.d.ts +16 -0
  18. package/dist/test.js +1 -1
  19. package/dist/vite.js +2 -2
  20. package/editor/assets/{agent8-BWaW-D85.js → agent8-X0Nesj-k.js} +1 -1
  21. package/editor/assets/{debug-DLF8rUtr.js → debug-TXqu-ftX.js} +2 -2
  22. package/editor/assets/{index-BYfiwbQx.js → index-DTdwKKIv.js} +51 -51
  23. package/editor/index.html +1 -1
  24. package/package.json +1 -1
  25. package/schemas/scene.schema.json +177 -0
  26. package/skills/incanto-building-3d-games.md +38 -2
  27. package/skills/incanto-gameplay-behaviors.md +19 -0
  28. package/skills/incanto-node-reference.md +29 -0
  29. package/skills/incanto-performance.md +4 -0
  30. package/skills/incanto-verifying-your-game.md +7 -0
  31. package/templates-app/beacon-isle-3d/package.json +1 -1
  32. package/templates-app/tps-3d/package.json +1 -1
  33. package/templates-app/village-quest-3d/package.json +1 -1
@@ -122,12 +122,16 @@ const rungs = [];
122
122
  );
123
123
  }
124
124
 
125
+ /** The playtest's JSON, shared by the `plays` and `feels` rungs. */
126
+ let playtestReport = null;
127
+
125
128
  // ---- plays ---------------------------------------------------------------
126
129
  if (rungs[0].status === 'pass') {
127
130
  const args = [scene, '--json', '--runs', '8'];
128
131
  if (behaviors) args.push('--behaviors', behaviors);
129
132
  const r = run('playtest', args);
130
133
  const out = safeJson(r.stdout);
134
+ playtestReport = out;
131
135
  const won = out?.runs?.filter((x) => x.outcome === 'won').length ?? 0;
132
136
  const total = out?.runs?.length ?? 0;
133
137
  // A scene that declares no win is not a scene that cannot be won: a
@@ -158,6 +162,48 @@ if (rungs[0].status === 'pass') {
158
162
  rungs.push({ name: 'plays', status: 'skipped', summary: 'not run — the scene does not load' });
159
163
  }
160
164
 
165
+ // ---- feels ---------------------------------------------------------------
166
+ // Sound and effects, from the same playtest that just ran. A game whose
167
+ // feedback is wired but never triggered plays perfectly and feels dead, and
168
+ // every other rung here calls it healthy.
169
+ {
170
+ const fb = playtestReport?.feedback;
171
+ const declared = (fb?.declaredAudio?.length ?? 0) + (fb?.declaredEffects?.length ?? 0);
172
+ const fired = new Set([...(fb?.heard ?? []), ...(fb?.shown ?? [])]);
173
+ if (!fb) {
174
+ rungs.push({
175
+ name: 'feels',
176
+ status: 'skipped',
177
+ summary: 'not run — the playtest did not report',
178
+ });
179
+ } else if (declared === 0) {
180
+ rungs.push({
181
+ name: 'feels',
182
+ status: 'skipped',
183
+ summary: 'this game declares no sound and no effects',
184
+ });
185
+ } else if (fired.size === 0) {
186
+ rungs.push({
187
+ name: 'feels',
188
+ status: 'fail',
189
+ summary: `${declared} sound/effect node(s) declared, and not one fired in any run`,
190
+ fix: 'connect them — a signal to `play` on an AudioPlayer, or `replay()` on a one-shot; `engine.audio.recent()` and `engine.effects.recent()` say what did fire',
191
+ });
192
+ } else {
193
+ const silent = [...(fb.declaredAudio ?? []), ...(fb.declaredEffects ?? [])].filter(
194
+ (path) => !fired.has(path),
195
+ );
196
+ rungs.push({
197
+ name: 'feels',
198
+ status: 'pass',
199
+ summary:
200
+ silent.length === 0
201
+ ? `every one of the ${declared} sound/effect nodes fired`
202
+ : `${fired.size} of ${declared} fired — silent: ${silent.slice(0, 3).join(', ')}${silent.length > 3 ? ` +${silent.length - 3}` : ''}`,
203
+ });
204
+ }
205
+ }
206
+
161
207
  // ---- draws ---------------------------------------------------------------
162
208
  {
163
209
  const r = run('frame', ['--json']);
package/dist/2d.js CHANGED
@@ -1,5 +1,5 @@
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-BRt6XKmP.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-YvaNWhe6.js";
3
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-en63AEZO.js";
4
4
  import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CfWAggJ1.js";
5
5
  //#region src/2d/library-sprite.ts
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
- import { F as CharacterBody3D, H as WATER_MAX_RIPPLES, I as PhysicsBody3D, L as RigidBody3D, M as WATER_CUTOUT_MAX, N as WaterCutout3D, P as Area3D, R as StaticBody3D, j as Water3D, z as Node3D } from "./gameplay-CZ2yq37J.js";
2
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-BQ_QsIBY.js";
3
- import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-Czzp6ZuE.js";
1
+ import { B as Node3D, F as Area3D, I as CharacterBody3D, L as PhysicsBody3D, M as Water3D, N as WATER_CUTOUT_MAX, P as WaterCutout3D, R as RigidBody3D, U as WATER_MAX_RIPPLES, z as StaticBody3D } from "./gameplay-C_SPzmUe.js";
2
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-DLHqgNAF.js";
3
+ import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-CqJkfMzm.js";
4
4
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-Bes-GIuR.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-NzDwWPrF.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;
@@ -5,11 +5,11 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { r as parseDrive, t as logReport } from "./log-report-CPFm4OXf.js";
6
6
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BnMyy9tr.js";
7
7
  import { a as openBundledEditor, c as audioErrors, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as claimCanvasGestures, t as teardown } from "./teardown-C7uVSJvx.js";
8
- import { I as PhysicsBody3D, V as createCausticsQuad, n as registerGameplayBehaviors, z as Node3D } from "./gameplay-CZ2yq37J.js";
8
+ import { B as Node3D, H as createCausticsQuad, L as PhysicsBody3D, n as registerGameplayBehaviors } from "./gameplay-C_SPzmUe.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-BQ_QsIBY.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-DLHqgNAF.js";
11
11
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
12
- import { n as enablePhysics3D } from "./physics-3d-Bes-GIuR.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-NzDwWPrF.js";
13
13
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
14
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
15
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -4,7 +4,7 @@ import { b as AudioPlayer, x as Engine } from "./register-p48lHE2o.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BnMyy9tr.js";
6
6
  import { a as openBundledEditor, c as audioErrors, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, s as claimCanvasGestures, t as teardown } from "./teardown-C7uVSJvx.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-CZ2yq37J.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-C_SPzmUe.js";
8
8
  import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-en63AEZO.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
10
  import { n as enablePhysics2D } from "./physics-2d-CfWAggJ1.js";
package/dist/debug.js CHANGED
@@ -1164,7 +1164,9 @@ var DebugOverlay = class {
1164
1164
  ...gpu.triangles !== void 0 ? [`tris ${compactCount(gpu.triangles)}`] : [],
1165
1165
  ...gpu.drawCalls !== void 0 ? [`calls ${gpu.drawCalls}`] : []
1166
1166
  ].join(" · ");
1167
- chip.textContent = `${Math.round(stats.fps)} fps · ${stats.frameMs.toFixed(1)} ms\n${line2}`;
1167
+ const p = stats.phases;
1168
+ const line3 = `\nphys ${p.fixedMs.toFixed(1)} · logic ${p.updateMs.toFixed(1)} · draw ${p.renderMs.toFixed(1)} · other ${p.otherMs.toFixed(1)}`;
1169
+ chip.textContent = `${Math.round(stats.fps)} fps · ${stats.frameMs.toFixed(1)} ms\n${line2}${line3}`;
1168
1170
  }
1169
1171
  renderExplorer() {
1170
1172
  const panel = this.panels.get("explorer");
package/dist/editor.js CHANGED
@@ -1181,6 +1181,13 @@ var kt = {
1181
1181
  en: "Options are the locales the scene declares in \"strings\", each labeled with its own endonym. Picking one switches the game live and persists it. English is the base: any key a locale omits falls back to it, on purpose.",
1182
1182
  ko: "옵션은 씬의 \"strings\"가 선언한 로케일이며, 각 언어의 자기 이름으로 표시됩니다. 선택하면 즉시 바뀌고 저장됩니다. 영어가 기준이라 로케일이 빠뜨린 키는 의도적으로 영어로 표시됩니다."
1183
1183
  }),
1184
+ L("Label3D", {
1185
+ en: "Text standing in the world — a nameplate, a sign, a damage number.",
1186
+ ko: "월드에 서 있는 텍스트 — 이름표, 표지판, 데미지 숫자."
1187
+ }, {
1188
+ en: "height is the cap height in METRES and the quad follows what the text measures, so a sign is as wide as its words. Faces the camera by default (a damage number read from behind is a bug) and carries a dark outline so it stays readable over anything. Everything else a sprite knows — anchor, tint, opacity, renderOrder — it inherits. For a damage number: set text, then float it up and fade opacity to 0 and queueFree().",
1189
+ ko: "height는 글자 높이(미터)이고 가로 폭은 텍스트 실측을 따라갑니다. 기본으로 카메라를 향하며(뒤에서 읽히는 데미지 숫자는 버그니까요) 어두운 외곽선이 있어 어떤 배경에서도 읽힙니다. 앵커·틴트·투명도·renderOrder 등 스프라이트의 나머지 기능은 그대로 상속합니다. 데미지 숫자는 text를 넣고 위로 띄우며 opacity를 0으로 보낸 뒤 queueFree() 하면 됩니다."
1190
+ }),
1184
1191
  L("UiVolumeSlider", {
1185
1192
  en: "A volume slider, already wired to a bus.",
1186
1193
  ko: "볼륨 슬라이더 — 버스에 이미 연결되어 있습니다."
@@ -4,7 +4,7 @@ import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-C7u42TID.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-7qvUxF6Z.js";
7
- import { F as CharacterBody3D, I as PhysicsBody3D, L as RigidBody3D, N as WaterCutout3D, P as Area3D, R as StaticBody3D, U as colliderFootDrop, j as Water3D, z as Node3D } from "./gameplay-CZ2yq37J.js";
7
+ import { B as Node3D, F as Area3D, I as CharacterBody3D, L as PhysicsBody3D, M as Water3D, P as WaterCutout3D, R as RigidBody3D, W as colliderFootDrop, z as StaticBody3D } from "./gameplay-C_SPzmUe.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -5685,6 +5685,121 @@ const colorScratchB$1 = new Color();
5685
5685
  const colorScratchC$1 = new Color();
5686
5686
  const tuftDryScratch = new Color();
5687
5687
  //#endregion
5688
+ //#region src/3d/nodes/label-3d.ts
5689
+ /**
5690
+ * Text standing in the world — a damage number, a nameplate, a sign.
5691
+ *
5692
+ * Every action game has these, and the answer used to be a sentence in the 3D
5693
+ * skill: *"There is no 3D text node — a nameplate or damage number is a
5694
+ * canvas-rendered texture on a child Sprite3D."* True, and a real afternoon of
5695
+ * work per game: measure the text, size the canvas, keep the device pixel ratio
5696
+ * honest, build the texture, work out the quad's aspect, re-rasterise when the
5697
+ * number changes, and dispose the old texture so a damage number does not leak
5698
+ * one per hit.
5699
+ *
5700
+ * It IS a canvas-rendered texture on a Sprite3D — which is exactly why it
5701
+ * should be a node. Everything a sprite already knows (billboarding, anchor,
5702
+ * tint, opacity, depth, render order) it inherits, so this only adds the text.
5703
+ *
5704
+ * ```json
5705
+ * { "name": "Name", "type": "Label3D",
5706
+ * "props": { "text": "Goblin", "height": 0.25, "position": [0, 2, 0] } }
5707
+ * ```
5708
+ *
5709
+ * A damage number is this plus two lines of behavior: set `text`, then move it
5710
+ * up and fade `opacity` to 0 over ~0.6 s and `queueFree()`.
5711
+ */
5712
+ var Label3D = class extends Sprite3D {
5713
+ static typeName = "Label3D";
5714
+ static props = {
5715
+ ...Sprite3D.props,
5716
+ text: { default: "" },
5717
+ /** Cap height of the text in METRES — the one number a sign is sized by. */
5718
+ height: { default: .3 },
5719
+ color: { default: "#ffffff" },
5720
+ font: { default: "sans-serif" },
5721
+ /** Outline colour; `""` turns it off. On by default: white text over a
5722
+ * bright wall is unreadable, and a damage number lands anywhere. */
5723
+ outline: { default: "#000000" },
5724
+ /** A damage number read from behind is a bug, so these face the camera. */
5725
+ billboard: {
5726
+ default: "full",
5727
+ options: [
5728
+ "y",
5729
+ "full",
5730
+ "none"
5731
+ ]
5732
+ }
5733
+ };
5734
+ text = "";
5735
+ height = .3;
5736
+ color = "#ffffff";
5737
+ font = "sans-serif";
5738
+ outline = "#000000";
5739
+ billboard = "full";
5740
+ /**
5741
+ * A `texture` on a label is a mistake, and a loud one.
5742
+ *
5743
+ * The prop is inherited from `Sprite3D` and cannot be un-inherited (schemas
5744
+ * merge up the chain), so a URL set here would be silently ignored — this
5745
+ * node draws its own text. Silent is the failure mode these rounds keep
5746
+ * finding; naming it costs one check at load.
5747
+ */
5748
+ static validateJson(node) {
5749
+ const label = node;
5750
+ if (label.texture) throw new IncantoError("BAD_FORMAT", `Label3D '${label.name}' has a "texture" — a label draws its own text, so the image would never be used. Set "text" instead, or use a Sprite3D.`, { prop: "texture" });
5751
+ }
5752
+ canvas = null;
5753
+ /** The rasterised text. Named apart from Sprite3D's `texture` URL prop, which
5754
+ * this node ignores — the text IS the picture. */
5755
+ canvasTexture = null;
5756
+ lastKey = "";
5757
+ resolveTexture(_assets) {
5758
+ if (this.text === "" || typeof document === "undefined") return null;
5759
+ const key = `${this.text}\0${this.height}\0${this.color}\0${this.font}\0${this.outline}`;
5760
+ if (key !== this.lastKey) {
5761
+ this.lastKey = key;
5762
+ this.rasterize();
5763
+ }
5764
+ return this.canvasTexture;
5765
+ }
5766
+ /** Draw the text once, and size the quad to what it measured. */
5767
+ rasterize() {
5768
+ const fontPx = Math.max(8, Math.round(this.height * 256));
5769
+ this.canvas ??= document.createElement("canvas");
5770
+ const ctx = this.canvas.getContext("2d");
5771
+ if (!ctx) return;
5772
+ const stroke = Math.max(1, Math.round(fontPx * .14));
5773
+ const fontSpec = `bold ${fontPx}px ${this.font}`;
5774
+ ctx.font = fontSpec;
5775
+ const width = Math.max(1, Math.ceil(ctx.measureText(this.text).width) + stroke * 2);
5776
+ const lineHeight = Math.ceil(fontPx * 1.35) + stroke * 2;
5777
+ this.canvas.width = width;
5778
+ this.canvas.height = lineHeight;
5779
+ ctx.font = fontSpec;
5780
+ ctx.textBaseline = "middle";
5781
+ ctx.textAlign = "center";
5782
+ if (this.outline !== "") {
5783
+ ctx.lineWidth = stroke;
5784
+ ctx.lineJoin = "round";
5785
+ ctx.strokeStyle = this.outline;
5786
+ ctx.strokeText(this.text, width / 2, lineHeight / 2);
5787
+ }
5788
+ ctx.fillStyle = this.color;
5789
+ ctx.fillText(this.text, width / 2, lineHeight / 2);
5790
+ this.canvasTexture?.dispose();
5791
+ const texture = new CanvasTexture(this.canvas);
5792
+ texture.colorSpace = SRGBColorSpace;
5793
+ this.canvasTexture = texture;
5794
+ this.size = [width / lineHeight * this.height * 1.35, this.height * 1.35];
5795
+ }
5796
+ free() {
5797
+ this.canvasTexture?.dispose();
5798
+ this.canvasTexture = null;
5799
+ super.free();
5800
+ }
5801
+ };
5802
+ //#endregion
5688
5803
  //#region src/3d/nodes/lights-3d.ts
5689
5804
  /** Sun-style light. Direction comes from the node's rotation. */
5690
5805
  var DirectionalLight3D = class extends Node3D {
@@ -12436,6 +12551,7 @@ function registerNodes3D() {
12436
12551
  registerNode(MeshInstance3D);
12437
12552
  registerNode(LoftMesh3D);
12438
12553
  registerNode(Sprite3D);
12554
+ registerNode(Label3D);
12439
12555
  registerNode(AnimatedSprite3D);
12440
12556
  registerNode(Billboard3D);
12441
12557
  registerNode(CharacterController3D);
@@ -4891,6 +4891,78 @@ function phaseOf(hour) {
4891
4891
  return "dusk";
4892
4892
  }
4893
4893
  //#endregion
4894
+ //#region src/gameplay/float-away.ts
4895
+ /**
4896
+ * Rise, fade, and be gone — the second half of a damage number.
4897
+ *
4898
+ * `Label3D` gave a game text standing in the world; every game then writes the
4899
+ * same eight lines to move it up and fade it out, and the interesting part is
4900
+ * not the eight lines, it is the three things they all get wrong: the node is
4901
+ * freed on the frame it becomes invisible rather than a second later, the fade
4902
+ * fights whatever else writes `opacity`, and a hundred numbers a fight each
4903
+ * leave a node behind because nothing ever frees them.
4904
+ *
4905
+ * ```json
4906
+ * { "name": "Hit", "type": "Label3D",
4907
+ * "props": { "text": "12", "height": 0.35, "color": "#ff5a5a" },
4908
+ * "script": { "name": "FloatAway", "props": { "rise": 1.2, "seconds": 0.7 } } }
4909
+ * ```
4910
+ *
4911
+ * Spawning one is then `duplicateNode` + set `text` + `position` — no per-frame
4912
+ * code of your own, and no cleanup to forget.
4913
+ *
4914
+ * It writes `opacity` and the node's own `position`, so it composes with
4915
+ * anything that does not. Works on any node with those props: a `Label3D`, a
4916
+ * `Sprite3D` pickup puff, a 2D `Label`.
4917
+ */
4918
+ var FloatAway = class extends Behavior {
4919
+ static props = {
4920
+ /** World units travelled over the whole life (negative sinks). */
4921
+ rise: { default: 1 },
4922
+ /** How long the whole thing takes. */
4923
+ seconds: { default: .7 },
4924
+ /** Fraction of the life spent at full opacity before the fade starts. */
4925
+ hold: { default: .3 },
4926
+ /** Sideways drift, so ten numbers at once do not stack into one. */
4927
+ drift: { default: 0 },
4928
+ /** Free the node when it finishes. Off for something you re-use. */
4929
+ freeOnEnd: { default: true }
4930
+ };
4931
+ rise = 1;
4932
+ seconds = .7;
4933
+ hold = .3;
4934
+ drift = 0;
4935
+ freeOnEnd = true;
4936
+ elapsed = 0;
4937
+ start = [];
4938
+ done = false;
4939
+ onReady() {
4940
+ const node = this.node;
4941
+ this.start = [...node.position ?? []];
4942
+ }
4943
+ update(dt) {
4944
+ if (this.done) return;
4945
+ const node = this.node;
4946
+ if (!node.position) return;
4947
+ this.elapsed += dt;
4948
+ const life = Math.max(.001, this.seconds);
4949
+ const t = Math.min(1, this.elapsed / life);
4950
+ const next = [...this.start];
4951
+ const eased = 1 - (1 - t) * (1 - t);
4952
+ next[1] = (this.start[1] ?? 0) + this.rise * eased;
4953
+ if (this.drift !== 0) next[0] = (this.start[0] ?? 0) + this.drift * eased;
4954
+ node.position = next;
4955
+ if (node.opacity !== void 0) {
4956
+ const holdFor = Math.min(.99, Math.max(0, this.hold));
4957
+ node.opacity = t <= holdFor ? 1 : 1 - (t - holdFor) / (1 - holdFor);
4958
+ }
4959
+ if (t >= 1) {
4960
+ this.done = true;
4961
+ if (this.freeOnEnd) this.node.queueFree();
4962
+ }
4963
+ }
4964
+ };
4965
+ //#endregion
4894
4966
  //#region src/gameplay/follow-camera.ts
4895
4967
  /**
4896
4968
  * Make the node it sits on chase a target's position — THE camera-follow
@@ -6636,7 +6708,8 @@ const GAMEPLAY_BEHAVIORS = {
6636
6708
  Spawner,
6637
6709
  WaveSpawner,
6638
6710
  Projectile,
6639
- Buoyancy
6711
+ Buoyancy,
6712
+ FloatAway
6640
6713
  };
6641
6714
  /**
6642
6715
  * Register all built-in gameplay behaviors. Idempotent and hot-reload tolerant
@@ -6648,4 +6721,4 @@ function registerGameplayBehaviors(opts) {
6648
6721
  for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
6649
6722
  }
6650
6723
  //#endregion
6651
- export { Chase as A, validateCollider3D as B, restartScene as C, DamageOnContact as D, phaseOf as E, CharacterBody3D as F, WATER_MAX_RIPPLES as H, PhysicsBody3D as I, RigidBody3D as L, WATER_CUTOUT_MAX as M, WaterCutout3D as N, Health as O, Area3D as P, StaticBody3D as R, goToScene as S, DayNight as T, colliderFootDrop as U, createCausticsQuad as V, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Water3D as j, Collector as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, FollowCamera as w, GameFlow as x, screenFlash as y, Node3D as z };
6724
+ export { Collector as A, Node3D as B, restartScene as C, phaseOf as D, DayNight as E, Area3D as F, createCausticsQuad as H, CharacterBody3D as I, PhysicsBody3D as L, Water3D as M, WATER_CUTOUT_MAX as N, DamageOnContact as O, WaterCutout3D as P, RigidBody3D as R, goToScene as S, FloatAway as T, WATER_MAX_RIPPLES as U, validateCollider3D as V, colliderFootDrop as W, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Chase as j, Health as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, FollowCamera as w, GameFlow as x, screenFlash as y, StaticBody3D as z };
@@ -678,6 +678,60 @@ declare class DayNight extends Behavior {
678
678
  }
679
679
  declare function phaseOf(hour: number): DayPhase;
680
680
  //#endregion
681
+ //#region src/gameplay/float-away.d.ts
682
+ /**
683
+ * Rise, fade, and be gone — the second half of a damage number.
684
+ *
685
+ * `Label3D` gave a game text standing in the world; every game then writes the
686
+ * same eight lines to move it up and fade it out, and the interesting part is
687
+ * not the eight lines, it is the three things they all get wrong: the node is
688
+ * freed on the frame it becomes invisible rather than a second later, the fade
689
+ * fights whatever else writes `opacity`, and a hundred numbers a fight each
690
+ * leave a node behind because nothing ever frees them.
691
+ *
692
+ * ```json
693
+ * { "name": "Hit", "type": "Label3D",
694
+ * "props": { "text": "12", "height": 0.35, "color": "#ff5a5a" },
695
+ * "script": { "name": "FloatAway", "props": { "rise": 1.2, "seconds": 0.7 } } }
696
+ * ```
697
+ *
698
+ * Spawning one is then `duplicateNode` + set `text` + `position` — no per-frame
699
+ * code of your own, and no cleanup to forget.
700
+ *
701
+ * It writes `opacity` and the node's own `position`, so it composes with
702
+ * anything that does not. Works on any node with those props: a `Label3D`, a
703
+ * `Sprite3D` pickup puff, a 2D `Label`.
704
+ */
705
+ declare class FloatAway extends Behavior {
706
+ static override readonly props: {
707
+ /** World units travelled over the whole life (negative sinks). */rise: {
708
+ default: number;
709
+ }; /** How long the whole thing takes. */
710
+ seconds: {
711
+ default: number;
712
+ }; /** Fraction of the life spent at full opacity before the fade starts. */
713
+ hold: {
714
+ default: number;
715
+ }; /** Sideways drift, so ten numbers at once do not stack into one. */
716
+ drift: {
717
+ default: number;
718
+ }; /** Free the node when it finishes. Off for something you re-use. */
719
+ freeOnEnd: {
720
+ default: boolean;
721
+ };
722
+ };
723
+ rise: number;
724
+ seconds: number;
725
+ hold: number;
726
+ drift: number;
727
+ freeOnEnd: boolean;
728
+ private elapsed;
729
+ private start;
730
+ private done;
731
+ override onReady(): void;
732
+ override update(dt: number): void;
733
+ }
734
+ //#endregion
681
735
  //#region src/gameplay/game-flow.d.ts
682
736
  /**
683
737
  * Reload the CURRENT scene from its source JSON — fresh nodes, reset physics,
@@ -869,4 +923,4 @@ declare function registerGameplayBehaviors(opts?: {
869
923
  replace?: boolean;
870
924
  }): void;
871
925
  //#endregion
872
- export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, type DayPhase, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
926
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, type DayPhase, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Chase, C as restartScene, D as DamageOnContact, E as phaseOf, O as Health, S as goToScene, T as DayNight, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, k as Collector, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, w as FollowCamera, x as GameFlow, y as screenFlash } from "./gameplay-CZ2yq37J.js";
2
- export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
1
+ import { A as Collector, C as restartScene, D as phaseOf, E as DayNight, O as DamageOnContact, S as goToScene, T as FloatAway, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Chase, k as Health, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, w as FollowCamera, x as GameFlow, y as screenFlash } from "./gameplay-C_SPzmUe.js";
2
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { a as nodeRefWarnings, i as describeRefProblem, n as startRecording, o a
8
8
  import { n as logText, r as parseDrive, t as logReport } from "./log-report-CPFm4OXf.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-7qvUxF6Z.js";
11
- import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-vNPQeQ1W.js";
11
+ import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-51GA-9m-.js";
12
12
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BnMyy9tr.js";
13
13
  import { t as duplicateNode } from "./duplicate-MNLMAcbz.js";
14
14
  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, 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 };
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { C as diagnose } from "./loader-BTkHYrQn.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { B as validateCollider3D, F as CharacterBody3D, I as PhysicsBody3D, L as RigidBody3D, P as Area3D, z as Node3D } from "./gameplay-CZ2yq37J.js";
4
+ import { B as Node3D, F as Area3D, I as CharacterBody3D, L as PhysicsBody3D, R as RigidBody3D, V as validateCollider3D } from "./gameplay-C_SPzmUe.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-BQ_QsIBY.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-DLHqgNAF.js";
7
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
8
8
  //#region src/3d/physics/collider-lines.ts
9
9
  /**
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-Czzp6ZuE.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-BRt6XKmP.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-CqJkfMzm.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-YvaNWhe6.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -161,6 +161,6 @@ function newUid() {
161
161
  //#endregion
162
162
  //#region src/index.ts
163
163
  /** Engine version. Kept in sync with package.json by the release pipeline. */
164
- const VERSION = "0.54.0";
164
+ const VERSION = "0.56.0";
165
165
  //#endregion
166
166
  export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
@@ -4,9 +4,9 @@ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-C7u42TID.js";
6
6
  import { n as startRecording } from "./replay-t1pP0gQg.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-CZ2yq37J.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-C_SPzmUe.js";
8
8
  import { t as registerNodes2D } from "./register-en63AEZO.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BQ_QsIBY.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-DLHqgNAF.js";
10
10
  import { n as registerNodesNet, t as createSplitScreen } from "./split-screen-CSb_uZ6W.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
@@ -752,6 +752,8 @@ async function runOnce(json, seed, opts) {
752
752
  }
753
753
  }
754
754
  driver.release();
755
+ const heard = engine.audio.recent().map((e) => e.from);
756
+ const shown = engine.effects.recent().map((e) => e.from);
755
757
  const bad = outcome !== "won";
756
758
  const run = {
757
759
  seed,
@@ -762,7 +764,9 @@ async function runOnce(json, seed, opts) {
762
764
  damageTaken: oracle.damage(),
763
765
  errors: engine.stats().errors,
764
766
  replay: bad ? recorder.stop() : null,
765
- endedAt: at
767
+ endedAt: at,
768
+ heard: [...new Set(heard)],
769
+ shown: [...new Set(shown)]
766
770
  };
767
771
  if (!bad) recorder.stop();
768
772
  for (const off of offs) off();
@@ -796,6 +800,19 @@ async function playtest(json, opts = {}) {
796
800
  ...opts.resolveScene ? { resolveScene: opts.resolveScene } : {}
797
801
  });
798
802
  const actions = probe.engine.input.declaredActions().map((a) => a.name);
803
+ const feedbackNodes = {
804
+ audio: [],
805
+ effects: []
806
+ };
807
+ const walkFeedback = (node) => {
808
+ const type = node.constructor.typeName ?? "";
809
+ const path = node.getPath();
810
+ if (type === "AudioPlayer") feedbackNodes.audio.push(path);
811
+ if (type === "Particles2D" || type === "Particles3D") feedbackNodes.effects.push(path);
812
+ for (const child of node.children) walkFeedback(child);
813
+ };
814
+ const probeRoot = probe.scene.tree.root;
815
+ if (probeRoot) walkFeedback(probeRoot);
799
816
  probe.dispose();
800
817
  return {
801
818
  runs,
@@ -804,7 +821,13 @@ async function playtest(json, opts = {}) {
804
821
  actions,
805
822
  inertActions: [],
806
823
  declaresWin,
807
- seconds: opts.seconds ?? 60
824
+ seconds: opts.seconds ?? 60,
825
+ feedback: {
826
+ declaredAudio: feedbackNodes.audio,
827
+ declaredEffects: feedbackNodes.effects,
828
+ heard: [...new Set(runs.flatMap((r) => r.heard))],
829
+ shown: [...new Set(runs.flatMap((r) => r.shown))]
830
+ }
808
831
  };
809
832
  }
810
833
  function pct(n, total) {
@@ -1733,7 +1756,7 @@ async function runScript(json, opts) {
1733
1756
  const { enablePhysics2D } = await import("./physics-2d-CfWAggJ1.js").then((n) => n.r);
1734
1757
  await enablePhysics2D(engine);
1735
1758
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1736
- const { enablePhysics3D } = await import("./physics-3d-Bes-GIuR.js").then((n) => n.r);
1759
+ const { enablePhysics3D } = await import("./physics-3d-NzDwWPrF.js").then((n) => n.r);
1737
1760
  await enablePhysics3D(engine);
1738
1761
  }
1739
1762
  const failures = [];
@@ -1849,7 +1872,7 @@ async function createPlaySession(json, opts = {}) {
1849
1872
  const { enablePhysics2D } = await import("./physics-2d-CfWAggJ1.js").then((n) => n.r);
1850
1873
  await enablePhysics2D(engine);
1851
1874
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1852
- const { enablePhysics3D } = await import("./physics-3d-Bes-GIuR.js").then((n) => n.r);
1875
+ const { enablePhysics3D } = await import("./physics-3d-NzDwWPrF.js").then((n) => n.r);
1853
1876
  await enablePhysics3D(engine);
1854
1877
  }
1855
1878
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
package/dist/test.d.ts CHANGED
@@ -232,6 +232,10 @@ interface PlaytestRun {
232
232
  replay: ReplayJson | null;
233
233
  /** Where the player ended up, for the fall report. */
234
234
  endedAt: [number, number, number];
235
+ /** Node paths that made a sound during the run. */
236
+ heard: string[];
237
+ /** Node paths that fired a visible effect during the run. */
238
+ shown: string[];
235
239
  }
236
240
  interface PlaytestReport {
237
241
  runs: PlaytestRun[];
@@ -261,6 +265,18 @@ interface PlaytestReport {
261
265
  * up to 1s", hiding the very fact it was reporting.
262
266
  */
263
267
  seconds: number;
268
+ /**
269
+ * Sound and effects: what the scene declares, and what actually fired.
270
+ *
271
+ * A game whose feedback is wired but never triggered plays perfectly and
272
+ * feels dead — invisible in every other number in this report.
273
+ */
274
+ feedback: {
275
+ declaredAudio: string[];
276
+ declaredEffects: string[];
277
+ heard: string[];
278
+ shown: string[];
279
+ };
264
280
  }
265
281
  /**
266
282
  * Who the driver is playing as.
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { r as auditScene } from "./replay-t1pP0gQg.js";
2
- import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-Tnf1nQl3.js";
2
+ import { S as framingText, _ as failingReplays, a as registerAllNodes, b as playtestText, c as ladderText, d as multiplayText, f as playMultiplayer, g as facingText, h as facingReport, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as findPlayer, x as describeFraming, y as playtest } from "./test-Bu6SPNsV.js";
3
3
  export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, validateScene };
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as VERSION } from "./src-vNPQeQ1W.js";
1
+ import { t as VERSION } from "./src-51GA-9m-.js";
2
2
  import { n as diffSignatures } from "./frame-report-njybhZon.js";
3
- import { s as validateScene } from "./test-Tnf1nQl3.js";
3
+ import { s as validateScene } from "./test-Bu6SPNsV.js";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
6
6
  //#region src/vite/frame-endpoint.ts