incanto 0.42.0 → 0.44.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 (41) hide show
  1. package/bin/incanto-feel.mjs +20 -1
  2. package/bin/incanto-frame.mjs +55 -11
  3. package/dist/2d.js +1 -1
  4. package/dist/3d.d.ts +2 -40
  5. package/dist/3d.js +6 -5
  6. package/dist/{create-game-DItIRyXH.js → create-game-D90pyPMx.js} +17 -81
  7. package/dist/{create-game-9KPppR0L.js → create-game-DFRbG0Bj.js} +3 -2
  8. package/dist/debug.d.ts +44 -3
  9. package/dist/debug.js +562 -4
  10. package/dist/{environment-presets-Vl5xBsXp.js → environment-presets-Ds5kXLoF.js} +1 -1
  11. package/dist/frame-report-Ct8XgsmV.d.ts +80 -0
  12. package/dist/frame-report-Lr3VO24R.js +197 -0
  13. package/dist/{gameplay-DRi9524r.js → gameplay-BQOeAid6.js} +37 -0
  14. package/dist/gameplay.d.ts +17 -0
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/{physics-3d-C2G604O1.js → physics-3d-CLPFv99o.js} +2 -2
  18. package/dist/react.js +1 -1
  19. package/dist/{src-cU57Uwdw.js → src-B3n06SsL.js} +1 -1
  20. package/dist/{teardown-CCtAMDLB.js → teardown-yePMOE1K.js} +32 -1
  21. package/dist/{test-DuOD1DO8.js → test-BMgiiD5i.js} +117 -5
  22. package/dist/test.d.ts +35 -1
  23. package/dist/test.js +2 -2
  24. package/dist/vite.d.ts +15 -1
  25. package/dist/vite.js +49 -6
  26. package/editor/assets/{agent8-Csw0T7jh.js → agent8-MgX6kYWH.js} +1 -1
  27. package/editor/assets/debug-Czri9cgX.js +3 -0
  28. package/editor/assets/{index-B1rUkWxB.js → index-D9MX3UHF.js} +60 -60
  29. package/editor/index.html +1 -1
  30. package/package.json +1 -1
  31. package/skills/incanto-3d-character.md +19 -0
  32. package/skills/incanto-gameplay-behaviors.md +27 -0
  33. package/skills/incanto-node-reference.md +2 -0
  34. package/skills/incanto-verifying-your-game.md +60 -4
  35. package/templates-app/beacon-isle-3d/package.json +1 -1
  36. package/templates-app/beacon-isle-3d/src/game.scene.json +47 -0
  37. package/templates-app/tps-3d/package.json +1 -1
  38. package/templates-app/tps-3d/src/game.scene.json +53 -0
  39. package/templates-app/village-quest-3d/package.json +1 -1
  40. package/templates-app/village-quest-3d/src/grove.scene.json +43 -0
  41. package/editor/assets/debug-C3qeBD4X.js +0 -3
@@ -52,7 +52,9 @@ if (args.help || !args.scene) {
52
52
  process.exit(args.help && args.scene !== undefined ? 0 : 1);
53
53
  }
54
54
 
55
- const { feelReport, feelText } = await import(pathToFileURL(join(PKG, 'dist', 'test.js')).href);
55
+ const { facingReport, facingText, feelReport, feelText } = await import(
56
+ pathToFileURL(join(PKG, 'dist', 'test.js')).href
57
+ );
56
58
  const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
57
59
 
58
60
  const behaviors = {};
@@ -90,4 +92,21 @@ try {
90
92
  }
91
93
 
92
94
  process.stdout.write(args.json ? `${JSON.stringify(report, null, 2)}\n` : `${feelText(report)}\n`);
95
+
96
+ // "It runs backwards" is this repo's most repeated 3D bug and the check is one
97
+ // dot product — which nobody runs by hand. It rides along here because this is
98
+ // already the command that drives the character.
99
+ if (!args.json) {
100
+ try {
101
+ const facing = await facingReport(sceneJson, {
102
+ behaviors,
103
+ stubMissingBehaviors: !args.behaviors,
104
+ resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
105
+ });
106
+ if (facing.skin) process.stdout.write(`${facingText(facing)}\n`);
107
+ } catch {
108
+ // a scene this probe cannot drive is not a failing scene
109
+ }
110
+ }
111
+
93
112
  process.exit(report.player ? 0 : 1);
@@ -25,20 +25,46 @@ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
25
25
  const { listeningPorts, parseProcNetTcp } = await import(
26
26
  pathToFileURL(join(PKG, 'dist', 'vite.js')).href
27
27
  );
28
- const { frameText } = await import(pathToFileURL(join(PKG, 'dist', '3d.js')).href);
28
+ const { frameText, diffText } = await import(pathToFileURL(join(PKG, 'dist', '3d.js')).href);
29
29
 
30
30
  const args = process.argv.slice(2);
31
31
  const asJson = args.includes('--json');
32
- const gridArg = args[args.indexOf('--grid') + 1];
32
+ /** `--flag` with an optional value: `--remember` and `--remember shader` both. */
33
+ const flag = (name) => {
34
+ const i = args.indexOf(name);
35
+ if (i < 0) return null;
36
+ const next = args[i + 1];
37
+ return next && !next.startsWith('-') ? next : '';
38
+ };
39
+ const gridArg = flag('--grid');
40
+ const remember = flag('--remember');
41
+ const diff = flag('--diff');
42
+ const threshold = flag('--threshold');
33
43
  if (args.includes('--help') || args.includes('-h')) {
34
44
  console.error(`Usage: incanto-frame [options]
35
45
 
36
- --json the full report as JSON instead of prose
37
- --grid WxH cells across and down (default 16x9)
38
- --port N skip discovery and use this port
46
+ --json the full report as JSON instead of prose
47
+ --grid WxH cells across and down (default 16x9)
48
+ --port N skip discovery and use this port
49
+
50
+ --remember [L] keep this frame as the baseline named L (default "last")
51
+ --diff [L] say what changed since baseline L
52
+ --threshold N a cell has changed when it moves this far, 0..255 (default 3)
39
53
 
40
54
  Run your game's dev server and open the preview page first: the pixels live in
41
- the browser, not in the dev server.`);
55
+ the browser, not in the dev server.
56
+
57
+ The report of ONE frame has a floor: a one-pixel seam is a fraction of a level
58
+ once a cell is averaged, and nothing can tell it apart from a thin rope in the
59
+ scene. Two frames of the SAME view can:
60
+
61
+ incanto-frame --remember # before your change
62
+ ...edit the shader...
63
+ incanto-frame --diff # "a vertical band at x 640..660"
64
+
65
+ Baselines live in the dev server's memory and die with it — no file is written.
66
+ Pause the game first (debug ☰ → Time → Pause) or you are measuring time
67
+ passing, not your change.`);
42
68
  process.exit(0);
43
69
  }
44
70
 
@@ -110,7 +136,13 @@ if (!found) {
110
136
  process.exit(1);
111
137
  }
112
138
 
113
- const res = await fetch(`http://${found.host}:${found.port}/__incanto/frame`, {
139
+ const params = new URLSearchParams();
140
+ if (gridArg !== null) params.set('grid', gridArg);
141
+ if (remember !== null) params.set('remember', remember);
142
+ if (diff !== null) params.set('diff', diff);
143
+ if (threshold) params.set('threshold', threshold);
144
+ const q = params.toString();
145
+ const res = await fetch(`http://${found.host}:${found.port}/__incanto/frame${q ? `?${q}` : ''}`, {
114
146
  signal: AbortSignal.timeout(10_000),
115
147
  });
116
148
  const body = await res.json().catch(() => null);
@@ -134,11 +166,23 @@ if (report.error) {
134
166
  }
135
167
 
136
168
  if (asJson) {
137
- process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
169
+ process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
138
170
  } else {
139
- process.stdout.write(`${frameText(report)}\n`);
171
+ const lines = [frameText(report)];
172
+ if (body.diffError === 'no-baseline') {
173
+ lines.push(
174
+ `no baseline named "${diff || 'last'}" — run \`incanto-frame --remember${diff ? ` ${diff}` : ''}\` first,\n` +
175
+ ' then again with --diff after your change. The baseline lives in the dev\n' +
176
+ ' server, so restarting it forgets.',
177
+ );
178
+ } else if (body.diffError) {
179
+ lines.push(`cannot compare: ${body.diffError}`);
180
+ }
181
+ if (body.diff) lines.push(diffText(body.diff));
182
+ if (body.remembered) lines.push(`remembered as "${body.remembered}"`);
183
+ process.stdout.write(`${lines.join('\n')}\n`);
140
184
  }
141
- process.exit(report.black ? 1 : 0);
185
+ // A missing baseline is a usage error, not a picture: fail so a script notices.
186
+ process.exit(report.black || body.diffError ? 1 : 0);
142
187
 
143
188
  void parseProcNetTcp; // re-exported for tests; referenced so bundlers keep it
144
- void gridArg;
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-9KPppR0L.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DFRbG0Bj.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-R2JTnIMw.js";
4
4
  import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BmgXBNDB.js";
5
5
  //#region src/2d/library-sprite.ts
package/dist/3d.d.ts CHANGED
@@ -3,6 +3,7 @@ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-D
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
4
  import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
+ 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-Ct8XgsmV.js";
6
7
  import { n as PathGrid, s as SpatialPose } from "./pathfinding-C49JSNNq.js";
7
8
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
8
9
  import { VRM } from "@pixiv/three-vrm";
@@ -190,45 +191,6 @@ declare function keyboardIntensity(moving: boolean, sprinting: boolean): number;
190
191
  /** intensity → movement state (the animation driver). */
191
192
  declare function movementState(grounded: boolean, intensity: number): "idle" | "walk" | "run" | "fastRun" | "airborne";
192
193
  //#endregion
193
- //#region src/3d/frame-report.d.ts
194
- /**
195
- * What the frame looks like, as something worth saying.
196
- *
197
- * The pixels are not the deliverable. A 1280x720 frame is 2.8 million numbers:
198
- * unreadable by a person, unaffordable for a model (roughly a thousand tokens
199
- * as an image, roughly a million as text). The deliverable is the JUDGEMENT —
200
- * black screen, one flat colour, washed out — and the numbers behind it, small
201
- * enough to print and compare.
202
- *
203
- * Same principle as `framing`'s "lit by: NOTHING": a report is a sentence
204
- * before it is a number, because a number needs a reader who already knows what
205
- * to compare it against.
206
- *
207
- * Pure arithmetic over an RGBA buffer — no three, no DOM, no GPU. Whoever has
208
- * the pixels calls this; in practice that is the browser holding the canvas.
209
- */
210
- /** Per-cell mean colour, `[r, g, b]` 0..255. */
211
- type GridCell = [number, number, number];
212
- interface FrameStats {
213
- width: number;
214
- height: number;
215
- /** Every pixel below the black threshold — the render produced nothing. */
216
- black: boolean;
217
- /** One colour everywhere: a clear-colour fill, or a camera inside geometry. */
218
- uniform: boolean;
219
- /** 0..1 mean relative luminance. */
220
- meanLuma: number;
221
- /** Coarse mean colour per cell, rows top to bottom. */
222
- grid: GridCell[][];
223
- }
224
- interface FrameStatsOptions {
225
- /** Cells across and down. Default 16x9 — 144 cells, printable. */
226
- grid?: [number, number];
227
- }
228
- declare function frameStats(pixels: Uint8Array | Uint8ClampedArray, width: number, height: number, opts?: FrameStatsOptions): FrameStats;
229
- /** The report a person or an agent reads. Verdict first, numbers after. */
230
- declare function frameText(stats: FrameStats): string;
231
- //#endregion
232
194
  //#region src/3d/nodes/node-3d.d.ts
233
195
  /**
234
196
  * Per-frame hand-off from `Renderer3D` to nodes that need the LIVE WebGL
@@ -3647,4 +3609,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
3647
3609
  name?: string;
3648
3610
  }): InstancedMesh3D;
3649
3611
  //#endregion
3650
- export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, enablePhysics3D, findRiverCoverageGaps, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
3612
+ export { Area3D, AssetStore3D, type BedSampler, Billboard3D, type BillboardGroupMode, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, type CreateGame3DOptions, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, type DownhillTraceOptions, Environment3D, type Environment3DConfig, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, type FlowerVariety, Flowers3D, type FogEnvironment, Foliage3D, type FoliageKind, type FoliageStyle, type FrameDiff, type FrameSignature, type FrameStats, type FrameStatsOptions, type Game3D, type GridCell, type HeightSampler, type Heightmap, type HeightmapOptions, InstancedMesh3D, Joint3D, type JointType3D, LoftMesh3D, type LoftSection, MeshInstance3D, type MeshKind, type MeshMaterialProps, type ModelEntry, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, type Physics3DOptions, PhysicsBody3D, QUARTER_PITCH, type RenderContext3D, type RenderHook3D, Renderer3D, type Renderer3DOptions, type RigView, RigidBody3D, type Ripple, River3D, type RiverCarveOptions, type RiverCoverageGap, type RiverHit, type RiverRing, type RiverRingOptions, SIGNATURE_GRID, type ShadowsEnvironment, type SkyEnvironment, StaticBody3D, type SunConsumer3D, type SyncOptions, type SyncResult, TERRAIN_THEMES, Terrain3D, type TerrainLayer, type TerrainNav, type TerrainNavOptions, type TerrainTheme, TextureCache3D, type TextureSpec, Trail3D, Tree3D, type TreeTier, type TreeType, VOXEL_PALETTE, type VoxelBlock, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
package/dist/3d.js CHANGED
@@ -1,8 +1,9 @@
1
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-DRi9524r.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-Vl5xBsXp.js";
3
- import { a as Environment3D, c as setEnvironment3D, d as sunDirectionFromElevationAzimuth, f as sunDirectionFromSky, i as syncTree, l as horizonColorFromSky, o as frameStats, p as AssetStore3D, r as Renderer3D, s as frameText, t as createGame3D, u as parseEnvironment3D } from "./create-game-DItIRyXH.js";
1
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BQOeAid6.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-Ds5kXLoF.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-D90pyPMx.js";
4
+ import { a as frameStats, i as frameSignature, n as diffSignatures, o as frameText, r as diffText, t as SIGNATURE_GRID } from "./frame-report-Lr3VO24R.js";
4
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
5
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-C2G604O1.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CLPFv99o.js";
6
7
  //#region src/3d/terrain-nav.ts
7
8
  function buildTerrainNav(terrain, opts) {
8
9
  const cellSize = opts?.cellSize ?? 2;
@@ -114,4 +115,4 @@ function createNavDebugNode(nav, opts) {
114
115
  return node;
115
116
  }
116
117
  //#endregion
117
- export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, enablePhysics3D, findRiverCoverageGaps, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
118
+ export { Area3D, AssetStore3D, Billboard3D, BoneAttachment3D, BoneLookAt3D, Camera3D, CharacterBody3D, CharacterController3D, DEFAULT_TERRAIN_TEXTURE_BASE, DirectionalLight3D, Environment3D, DENSITY_PRESETS as FLOWER_DENSITY_PRESETS, FLOWER_VARIETIES, Flowers3D, Foliage3D, InstancedMesh3D, Joint3D, LoftMesh3D, MeshInstance3D, ModelInstance3D, Node3D, OmniLight3D, Particles3D, Physics3D, PhysicsBody3D, QUARTER_PITCH, Renderer3D, RigidBody3D, River3D, SIGNATURE_GRID, StaticBody3D, TERRAIN_THEMES, Terrain3D, TextureCache3D, Trail3D, Tree3D, VOXEL_PALETTE, VoxelGrid3D, WATER_CUTOUT_MAX, WATER_MAX_RIPPLES, Water3D, WaterCutout3D, acquireOwnTexture, acquireTexture, buildHeightmap, buildRiverRings, buildTerrainNav, cameraRelative, createGame3D, createNavDebugNode, diffSignatures, diffText, enablePhysics3D, findRiverCoverageGaps, frameSignature, frameStats, frameText, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
@@ -3,11 +3,12 @@ import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-r
3
3
  import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-BSXV8T9F.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
- import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-CCtAMDLB.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-DRi9524r.js";
6
+ import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-yePMOE1K.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
8
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-Vl5xBsXp.js";
10
- import { n as enablePhysics3D } from "./physics-3d-C2G604O1.js";
9
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-Ds5kXLoF.js";
10
+ import { a as frameStats, i as frameSignature } from "./frame-report-Lr3VO24R.js";
11
+ import { n as enablePhysics3D } from "./physics-3d-CLPFv99o.js";
11
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";
12
13
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
14
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -486,76 +487,6 @@ function setEnvironment3D(engine, patch) {
486
487
  scene.environment = merged;
487
488
  }
488
489
  //#endregion
489
- //#region src/3d/frame-report.ts
490
- /** Rec. 709 relative luminance of an 8-bit triple, 0..1. */
491
- function luma(r, g, b) {
492
- return (.2126 * r + .7152 * g + .0722 * b) / 255;
493
- }
494
- /**
495
- * A pixel this dark is indistinguishable from "nothing was drawn". Not zero:
496
- * a real render of a night scene still lands a few levels above black, and
497
- * calling that a failure would make the check useless where it matters most.
498
- */
499
- const BLACK_LEVEL = .012;
500
- /** Cell-to-cell luminance range below this: the frame is one flat colour. */
501
- const UNIFORM_EPSILON = .004;
502
- function frameStats(pixels, width, height, opts = {}) {
503
- const [cols, rows] = opts.grid ?? [16, 9];
504
- const grid = [];
505
- let total = 0;
506
- let maxLuma = 0;
507
- let minLuma = 1;
508
- for (let row = 0; row < rows; row++) {
509
- const line = [];
510
- for (let col = 0; col < cols; col++) {
511
- const x0 = Math.floor(col * width / cols);
512
- const x1 = Math.max(x0 + 1, Math.floor((col + 1) * width / cols));
513
- const y0 = Math.floor(row * height / rows);
514
- const y1 = Math.max(y0 + 1, Math.floor((row + 1) * height / rows));
515
- let r = 0;
516
- let g = 0;
517
- let b = 0;
518
- let n = 0;
519
- for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
520
- const i = (y * width + x) * 4;
521
- r += pixels[i] ?? 0;
522
- g += pixels[i + 1] ?? 0;
523
- b += pixels[i + 2] ?? 0;
524
- n += 1;
525
- }
526
- const cell = [
527
- Math.round(r / n),
528
- Math.round(g / n),
529
- Math.round(b / n)
530
- ];
531
- const cellLuma = luma(cell[0], cell[1], cell[2]);
532
- total += cellLuma;
533
- maxLuma = Math.max(maxLuma, cellLuma);
534
- minLuma = Math.min(minLuma, cellLuma);
535
- line.push(cell);
536
- }
537
- grid.push(line);
538
- }
539
- const cells = cols * rows;
540
- const meanLuma = total / cells;
541
- return {
542
- width,
543
- height,
544
- black: maxLuma <= BLACK_LEVEL,
545
- uniform: maxLuma - minLuma < UNIFORM_EPSILON,
546
- meanLuma: Math.round(meanLuma * 1e3) / 1e3,
547
- grid
548
- };
549
- }
550
- /** The report a person or an agent reads. Verdict first, numbers after. */
551
- function frameText(stats) {
552
- const lines = [];
553
- if (stats.black) lines.push("BLACK SCREEN — nothing was drawn (no light, no camera, or nothing in view)");
554
- else if (stats.uniform) lines.push("one flat colour — the camera may be inside geometry, or only the sky is drawn");
555
- lines.push(`frame ${stats.width}×${stats.height} · luminance ${stats.meanLuma.toFixed(2)}`);
556
- return lines.join("\n");
557
- }
558
- //#endregion
559
490
  //#region src/3d/adaptive-resolution.ts
560
491
  const DEFAULTS = {
561
492
  slowMs: 20,
@@ -2475,6 +2406,7 @@ async function createGame3D(opts) {
2475
2406
  const overlay = attachDebugOverlay(engine, {
2476
2407
  container: host,
2477
2408
  statsSource: () => renderer.stats?.() ?? {},
2409
+ ...renderer.captureFrame ? { frameSource: () => captureFramePixels() } : {},
2478
2410
  ...editorSwitch ? { actions: [editorAction] } : {},
2479
2411
  ...opts._debugDoc ? { doc: opts._debugDoc } : {}
2480
2412
  });
@@ -2504,19 +2436,22 @@ async function createGame3D(opts) {
2504
2436
  cleanups.push(engine.settings.bindRenderScale((scale) => {
2505
2437
  renderer.setRenderScale?.(scale);
2506
2438
  }));
2507
- const captureReport = async (frameOpts) => {
2439
+ const captureFramePixels = () => {
2508
2440
  const capture = renderer.captureFrame;
2509
2441
  if (!capture) throw new IncantoError("TREE_VIOLATION", "this renderer cannot capture frames");
2510
- const shot = await capture.call(renderer);
2511
- return frameStats(shot.pixels, shot.width, shot.height, frameOpts);
2442
+ return capture.call(renderer);
2512
2443
  };
2513
2444
  const hot = import.meta.hot;
2514
2445
  if (hot) {
2515
2446
  const onFrameRequest = (data) => {
2516
- const id = data?.id;
2517
- captureReport().then((frameReport) => hot.send("incanto:frame-report", {
2447
+ const request = data;
2448
+ const id = request?.id;
2449
+ captureFramePixels().then((shot) => hot.send("incanto:frame-report", {
2518
2450
  id,
2519
- report: frameReport
2451
+ report: {
2452
+ ...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
2453
+ signature: frameSignature(shot.pixels, shot.width, shot.height)
2454
+ }
2520
2455
  })).catch((error) => hot.send("incanto:frame-report", {
2521
2456
  id,
2522
2457
  report: { error: error instanceof Error ? error.message : String(error) }
@@ -2525,6 +2460,7 @@ async function createGame3D(opts) {
2525
2460
  hot.on("incanto:frame-request", onFrameRequest);
2526
2461
  cleanups.push(() => hot.off?.("incanto:frame-request", onFrameRequest));
2527
2462
  }
2463
+ cleanups.push(pauseWhenHidden(engine));
2528
2464
  await report(1, "ready");
2529
2465
  function disposeGame() {
2530
2466
  teardown([
@@ -2617,4 +2553,4 @@ function wireAudioUnlock(engine, root, canvas) {
2617
2553
  return detachAll;
2618
2554
  }
2619
2555
  //#endregion
2620
- export { Environment3D as a, setEnvironment3D as c, sunDirectionFromElevationAzimuth as d, sunDirectionFromSky as f, syncTree as i, horizonColorFromSky as l, create_game_exports as n, frameStats as o, AssetStore3D as p, Renderer3D as r, frameText as s, createGame3D as t, parseEnvironment3D as u };
2556
+ export { Environment3D as a, parseEnvironment3D as c, AssetStore3D as d, syncTree as i, sunDirectionFromElevationAzimuth as l, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, horizonColorFromSky as s, createGame3D as t, sunDirectionFromSky as u };
@@ -3,8 +3,8 @@ import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagno
3
3
  import { h as Engine, m as AudioPlayer } from "./register-BSXV8T9F.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
- import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-CCtAMDLB.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-DRi9524r.js";
6
+ import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, t as teardown } from "./teardown-yePMOE1K.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
8
  import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-R2JTnIMw.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
10
  import { n as enablePhysics2D } from "./physics-2d-BmgXBNDB.js";
@@ -545,6 +545,7 @@ async function createGame2D(opts) {
545
545
  if (keyboard) engine.input.attachKeyboard(keyboard);
546
546
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
547
547
  cleanups.push(engine.settings.bindAudio(engine.audio));
548
+ cleanups.push(pauseWhenHidden(engine));
548
549
  cleanups.push(engine.settings.bindLocale(engine.locale));
549
550
  const touchMode = opts.touch ?? "auto";
550
551
  if (touchMode !== false) {
package/dist/debug.d.ts CHANGED
@@ -3,16 +3,21 @@ import { At as LogLevel, T as RendererStats, b as Engine } from "./behavior-62q0
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
5
5
  interface DocumentLike {
6
+ /** Optional: present on a real document, absent in the test double. */
7
+ addEventListener?(type: string, cb: (event: unknown) => void): void;
8
+ removeEventListener?(type: string, cb: (event: unknown) => void): void;
6
9
  createElement(tag: string): HTMLElement;
7
10
  }
8
11
  //#endregion
9
12
  //#region src/debug/index.d.ts
10
13
  type DebugPanelId = "explorer" | "inspector" | "logs" | "stats" | "time";
14
+ /** Menu entries that DO something once rather than opening a panel. */
15
+ type DebugCommandId = "copyScene" | "captureRegion";
11
16
  /** Menu entries that are switches rather than panels. */
12
17
  type DebugSwitchId = "colliders";
13
18
  /** `all` is every collider in the world; `selected` follows the Explorer. */
14
19
  type ColliderMode = "off" | "all" | "selected";
15
- type DebugMenuId = DebugPanelId | DebugSwitchId;
20
+ type DebugMenuId = DebugPanelId | DebugSwitchId | DebugCommandId;
16
21
  /** An extra ☰ menu entry the host app supplies (e.g. "edit this scene"). */
17
22
  interface DebugAction {
18
23
  /** Menu label. Keep it a verb — the menu is a list of things you can do. */
@@ -37,6 +42,19 @@ interface DebugOverlayOptions {
37
42
  * renderers): the chip shows engine stats only.
38
43
  */
39
44
  statsSource?: () => Partial<RendererStats>;
45
+ /**
46
+ * The renderer's pixels, for the region capture. Wired by createGame like
47
+ * `statsSource`, because the overlay holds the engine and not the renderer.
48
+ *
49
+ * It must be `captureFrame()` and not `canvas.toDataURL()`: the drawing
50
+ * buffer is gone by the time anything outside a render can read it, and the
51
+ * image comes back black. That trap has been hit here before.
52
+ */
53
+ frameSource?: () => Promise<{
54
+ pixels: Uint8Array;
55
+ width: number;
56
+ height: number;
57
+ }>;
40
58
  /** @internal Test seam — replaces `document`. */
41
59
  doc?: DocumentLike;
42
60
  }
@@ -48,6 +66,7 @@ declare class DebugOverlay {
48
66
  private readonly doc;
49
67
  private readonly statsSource?;
50
68
  private readonly actions;
69
+ private readonly frameSource?;
51
70
  private readonly panels;
52
71
  private readonly cleanups;
53
72
  private readonly menuButton;
@@ -75,7 +94,7 @@ declare class DebugOverlay {
75
94
  private timeEls;
76
95
  /** Pointer inside the inspector — it stops refreshing under your hand. */
77
96
  private hovering;
78
- constructor(engine: Engine, container: HTMLElement, doc: DocumentLike, statsSource?: (() => Partial<RendererStats>) | undefined, actions?: DebugAction[]);
97
+ constructor(engine: Engine, container: HTMLElement, doc: DocumentLike, statsSource?: (() => Partial<RendererStats>) | undefined, actions?: DebugAction[], frameSource?: DebugOverlayOptions["frameSource"] | undefined);
79
98
  isOpen(id: DebugMenuId): boolean;
80
99
  /** How much of the physics world the wireframes show. */
81
100
  colliderMode: ColliderMode;
@@ -117,6 +136,28 @@ declare class DebugOverlay {
117
136
  private trackEditing;
118
137
  private pushLog;
119
138
  /**
139
+ * The scene, on the clipboard, ready to paste into a conversation.
140
+ *
141
+ * This is half of "here is what I am looking at" — the half a screenshot
142
+ * cannot carry. The other half is the region capture below.
143
+ */
144
+ copyScene(): Promise<boolean>;
145
+ /**
146
+ * Drag a rectangle over the game; the selection copies as an image.
147
+ *
148
+ * The pixels come from `frameSource` — the renderer's own end-of-frame read —
149
+ * and never from `canvas.toDataURL()`, which returns black once the frame has
150
+ * composited unless `preserveDrawingBuffer` is on (it is not, because it costs
151
+ * bandwidth on every frame).
152
+ */
153
+ captureRegion(): void;
154
+ private capture;
155
+ private captureScale;
156
+ /** Crop the last frame to `drag` and put the image on the clipboard. */
157
+ private copyRegion;
158
+ /** A line that says what happened and gets out of the way. */
159
+ private toast;
160
+ /**
120
161
  * Set game time, defensively. `timeScale` multiplies every dt in the engine,
121
162
  * so a NaN from a text field would poison physics, timers and animation in
122
163
  * one frame, and a negative would run the simulation backwards through code
@@ -144,4 +185,4 @@ declare class DebugOverlay {
144
185
  private renderLogs;
145
186
  }
146
187
  //#endregion
147
- export { ColliderMode, DebugAction, DebugMenuId, DebugOverlay, DebugOverlayOptions, DebugPanelId, DebugSwitchId, attachDebugOverlay };
188
+ export { ColliderMode, DebugAction, DebugCommandId, DebugMenuId, DebugOverlay, DebugOverlayOptions, DebugPanelId, DebugSwitchId, attachDebugOverlay };