incanto 0.41.0 → 0.42.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 (44) hide show
  1. package/bin/incanto-frame.mjs +144 -0
  2. package/dist/2d.d.ts +2 -2
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +76 -4
  5. package/dist/3d.js +5 -5
  6. package/dist/{behavior-TA8nySqb.d.ts → behavior-62q0HWBO.d.ts} +3 -1
  7. package/dist/{create-game-CZpENyin.js → create-game-9KPppR0L.js} +4 -4
  8. package/dist/{create-game-u7TDbvln.js → create-game-DItIRyXH.js} +158 -5
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{environment-presets-pGTHb_V4.js → environment-presets-Vl5xBsXp.js} +2 -2
  11. package/dist/{gameplay-By8mslMc.js → gameplay-DRi9524r.js} +13 -0
  12. package/dist/gameplay.d.ts +13 -1
  13. package/dist/gameplay.js +1 -1
  14. package/dist/index.d.ts +4 -4
  15. package/dist/index.js +3 -167
  16. package/dist/{loader-CvSDKC3v.d.ts → loader-CeyU_bm1.d.ts} +1 -1
  17. package/dist/net.d.ts +1 -1
  18. package/dist/net.js +2 -2
  19. package/dist/{pathfinding-_UlKUoox.d.ts → pathfinding-C49JSNNq.d.ts} +1 -1
  20. package/dist/{physics-2d-CEnZFcpM.js → physics-2d-BmgXBNDB.js} +1 -1
  21. package/dist/{physics-3d-CumBBvQo.js → physics-3d-C2G604O1.js} +2 -2
  22. package/dist/react.d.ts +1 -1
  23. package/dist/react.js +1 -1
  24. package/dist/{register-DuTlY56W.js → register-BSXV8T9F.js} +3 -1
  25. package/dist/{register-DnycZ91Q.js → register-D651it1J.js} +1 -1
  26. package/dist/{register-MSZvnHkp.js → register-R2JTnIMw.js} +1 -1
  27. package/dist/{replay-DN-kG4va.d.ts → replay-CAphXMyM.d.ts} +1 -1
  28. package/dist/{replay-DcWg4LT5.js → replay-DilbZgQI.js} +1 -1
  29. package/dist/src-cU57Uwdw.js +166 -0
  30. package/dist/{test-B4kSwQzq.js → test-DuOD1DO8.js} +22 -11
  31. package/dist/test.d.ts +3 -3
  32. package/dist/test.js +2 -2
  33. package/dist/vite.d.ts +61 -1
  34. package/dist/vite.js +149 -2
  35. package/editor/assets/{agent8-DCHkff44.js → agent8-Csw0T7jh.js} +1 -1
  36. package/editor/assets/{debug-uHHxqj0a.js → debug-C3qeBD4X.js} +1 -1
  37. package/editor/assets/{index-i1mIkyMA.js → index-B1rUkWxB.js} +49 -49
  38. package/editor/index.html +1 -1
  39. package/package.json +3 -2
  40. package/skills/incanto-audio.md +6 -2
  41. package/skills/incanto-verifying-your-game.md +41 -0
  42. package/templates-app/beacon-isle-3d/package.json +1 -1
  43. package/templates-app/tps-3d/package.json +1 -1
  44. package/templates-app/village-quest-3d/package.json +1 -1
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ /**
4
+ * incanto-frame — what is actually on screen, as numbers.
5
+ *
6
+ * bunx incanto-frame
7
+ *
8
+ * The rung above `incanto-check`. That one says the scene loads; `framing` says
9
+ * what the camera SHOULD see; this says what the GPU actually drew — the only
10
+ * one that can catch a black screen, a material that failed, or a shader
11
+ * drawing where it should not.
12
+ *
13
+ * It writes no files. An image is only worth producing when a human is going to
14
+ * look at one, and a report is what an agent can act on.
15
+ *
16
+ * Finding the dev server is this program's job, never the caller's: it reads
17
+ * the ports that are actually listening and asks each one whether it is an
18
+ * incanto dev server. You run one command; it works out the rest.
19
+ */
20
+ import { createRequire } from 'node:module';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath, pathToFileURL } from 'node:url';
23
+
24
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const { listeningPorts, parseProcNetTcp } = await import(
26
+ pathToFileURL(join(PKG, 'dist', 'vite.js')).href
27
+ );
28
+ const { frameText } = await import(pathToFileURL(join(PKG, 'dist', '3d.js')).href);
29
+
30
+ const args = process.argv.slice(2);
31
+ const asJson = args.includes('--json');
32
+ const gridArg = args[args.indexOf('--grid') + 1];
33
+ if (args.includes('--help') || args.includes('-h')) {
34
+ console.error(`Usage: incanto-frame [options]
35
+
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
39
+
40
+ Run your game's dev server and open the preview page first: the pixels live in
41
+ the browser, not in the dev server.`);
42
+ process.exit(0);
43
+ }
44
+
45
+ const read = (path) => {
46
+ try {
47
+ return readFileSync(path, 'utf-8');
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
52
+
53
+ /**
54
+ * Ports that are listening, on a machine with no `/proc` (macOS, BSD).
55
+ * The container this is built for is Linux, but a developer's laptop is not,
56
+ * and a tool that only works in CI teaches nobody anything.
57
+ */
58
+ function bsdListeningPorts() {
59
+ try {
60
+ const { execFileSync } = createRequire(import.meta.url)('node:child_process');
61
+ const out = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN'], {
62
+ encoding: 'utf-8',
63
+ timeout: 1500,
64
+ stdio: ['ignore', 'pipe', 'ignore'],
65
+ });
66
+ const ports = new Set();
67
+ for (const line of out.split('\n')) {
68
+ const m = line.match(/:(\d+)\s*\(LISTEN\)/);
69
+ if (m) ports.add(Number(m[1]));
70
+ }
71
+ return [...ports];
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ const explicit = args.includes('--port') ? Number(args[args.indexOf('--port') + 1]) : null;
78
+ const discovered = explicit ? [explicit] : await listeningPorts({ read });
79
+ const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
80
+
81
+ let found = null;
82
+ // BOTH hosts, deliberately: vite binds to ::1 on some machines and 127.0.0.1 on
83
+ // others, and a probe of the wrong one reports "no dev server" while the page
84
+ // is plainly on screen.
85
+ const HOSTS = ['localhost', '127.0.0.1'];
86
+ outer: for (const port of ports) {
87
+ for (const host of HOSTS) {
88
+ try {
89
+ const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
90
+ signal: AbortSignal.timeout(300),
91
+ });
92
+ if (!res.ok) continue;
93
+ const body = await res.json();
94
+ if (body?.incanto) {
95
+ found = { host, port, ...body };
96
+ break outer;
97
+ }
98
+ } catch {
99
+ // not listening, not ours, or not answering — either way, next
100
+ }
101
+ }
102
+ }
103
+
104
+ if (!found) {
105
+ console.error(
106
+ 'no incanto dev server found.\n' +
107
+ ' Start your game (the dev server) and try again — this reads the frame\n' +
108
+ ' from the running preview, so there has to be one.',
109
+ );
110
+ process.exit(1);
111
+ }
112
+
113
+ const res = await fetch(`http://${found.host}:${found.port}/__incanto/frame`, {
114
+ signal: AbortSignal.timeout(10_000),
115
+ });
116
+ const body = await res.json().catch(() => null);
117
+
118
+ if (res.status === 504 || body?.error === 'no-page-connected') {
119
+ console.error(
120
+ `the dev server is running on :${found.port}, but no page is connected.\n` +
121
+ ' Open the preview in a browser — the pixels are there, not in the server.',
122
+ );
123
+ process.exit(1);
124
+ }
125
+ if (!res.ok || !body?.report) {
126
+ console.error(`could not capture a frame: ${body?.error ?? res.status}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ const report = body.report;
131
+ if (report.error) {
132
+ console.error(`the page could not capture a frame: ${report.error}`);
133
+ process.exit(1);
134
+ }
135
+
136
+ if (asJson) {
137
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
138
+ } else {
139
+ process.stdout.write(`${frameText(report)}\n`);
140
+ }
141
+ process.exit(report.black ? 1 : 0);
142
+
143
+ void parseProcNetTcp; // re-exported for tests; referenced so bundlers keep it
144
+ void gridArg;
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-TA8nySqb.js";
1
+ import { Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-62q0HWBO.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-CvSDKC3v.js";
4
+ import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
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-CZpENyin.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-MSZvnHkp.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-CEnZFcpM.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-9KPppR0L.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-R2JTnIMw.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BmgXBNDB.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as QualityCaps, Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-TA8nySqb.js";
1
+ import { D as QualityCaps, Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-62q0HWBO.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DVwIGZdK.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-CvSDKC3v.js";
4
+ import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-_UlKUoox.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-C49JSNNq.js";
7
7
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
8
8
  import { VRM } from "@pixiv/three-vrm";
9
9
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -190,6 +190,45 @@ declare function keyboardIntensity(moving: boolean, sprinting: boolean): number;
190
190
  /** intensity → movement state (the animation driver). */
191
191
  declare function movementState(grounded: boolean, intensity: number): "idle" | "walk" | "run" | "fastRun" | "airborne";
192
192
  //#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
193
232
  //#region src/3d/nodes/node-3d.d.ts
194
233
  /**
195
234
  * Per-frame hand-off from `Renderer3D` to nodes that need the LIVE WebGL
@@ -643,6 +682,20 @@ interface Game3D {
643
682
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
644
683
  stats(): GameStats;
645
684
  /**
685
+ * What is actually on screen, as numbers.
686
+ *
687
+ * The rung above `framing()`: that one PREDICTS what the camera should see
688
+ * from the scene graph, this one measures what the GPU drew. The difference
689
+ * between the two is the whole class of bug a scene graph cannot express — a
690
+ * black screen, a material that failed, a shader drawing where it should not.
691
+ *
692
+ * Returns the report, never a file. An image is only worth writing when a
693
+ * human is going to look at it.
694
+ */
695
+ frame(opts?: {
696
+ grid?: [number, number];
697
+ }): Promise<FrameStats>;
698
+ /**
646
699
  * Assets that failed to load, with the reason.
647
700
  *
648
701
  * "The model just never appeared" used to be unanswerable from outside: the
@@ -3351,6 +3404,25 @@ declare class Renderer3D {
3351
3404
  warmUp(onProgress?: (fraction: number) => void): Promise<void>;
3352
3405
  private render;
3353
3406
  /**
3407
+ * A frame's pixels, read on the NEXT frame that draws.
3408
+ *
3409
+ * `preserveDrawingBuffer` is false by default — with good reason, it costs
3410
+ * bandwidth on every frame — so `canvas.toDataURL()` comes back blank, which
3411
+ * is the trap this repo has fallen into before. Reading with `gl.readPixels`
3412
+ * at the END of a render, in the same frame, needs no such flag and no
3413
+ * per-frame cost: nothing happens at all until somebody asks.
3414
+ *
3415
+ * That is why this lives in the engine rather than in a generic screenshot
3416
+ * tool. An outside tool cannot get inside the frame.
3417
+ */
3418
+ captureFrame(): Promise<{
3419
+ pixels: Uint8Array;
3420
+ width: number;
3421
+ height: number;
3422
+ }>;
3423
+ private pendingCaptures;
3424
+ private drainFrameCapture;
3425
+ /**
3354
3426
  * Bloom post pass (only when `environment.bloom` is declared). LDR and safe:
3355
3427
  * 1. scene → full-res offscreen (tone-mapped exactly like direct output)
3356
3428
  * 2. bright-pass at HALF res (keep pixels above threshold)
@@ -3575,4 +3647,4 @@ declare function createNavDebugNode(nav: TerrainNav, opts?: {
3575
3647
  name?: string;
3576
3648
  }): InstancedMesh3D;
3577
3649
  //#endregion
3578
- 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 Game3D, 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, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
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 };
package/dist/3d.js CHANGED
@@ -1,8 +1,8 @@
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-By8mslMc.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-pGTHb_V4.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-u7TDbvln.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-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";
4
4
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
5
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CumBBvQo.js";
5
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-C2G604O1.js";
6
6
  //#region src/3d/terrain-nav.ts
7
7
  function buildTerrainNav(terrain, opts) {
8
8
  const cellSize = opts?.cellSize ?? 2;
@@ -114,4 +114,4 @@ function createNavDebugNode(nav, opts) {
114
114
  return node;
115
115
  }
116
116
  //#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, horizonColorFromSky, keyboardIntensity, movementState, parseEnvironment3D, projectToRiver, registerNodes3D, resolveFlowerDensity, rigPose, riverCarveChannels, riverStepFor, setEnvironment3D, smoothCourse, splatWeights, sunDirectionFromElevationAzimuth, sunDirectionFromSky, syncTree, terrainThemeLayers, traceDownhillPath };
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 };
@@ -390,7 +390,9 @@ interface PlayMusicOptions {
390
390
  * frame to advance fades (the engine wires this to `updated`).
391
391
  *
392
392
  * ```ts
393
- * engine.music.play('incanto/assets/audio/theme.mp3'); // loop, full
393
+ * // NOTE: the bundle ships SFX only — no music track. A background track is
394
+ * // your game's own file under public/, or a URL.
395
+ * engine.music.play('/audio/theme.mp3'); // loop, full
394
396
  * engine.music.crossfadeTo('boss.mp3', 3); // 3s equal-power swap
395
397
  * engine.music.stop(2); // fade out over 2s
396
398
  * engine.music.setVolume(0.5); // per-manager volume
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { n as loadScene, o as computeViewport, s as resolveViewport, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-DuTlY56W.js";
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
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-By8mslMc.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-MSZvnHkp.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-DRi9524r.js";
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
- import { n as enablePhysics2D } from "./physics-2d-CEnZFcpM.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-BmgXBNDB.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { n as loadScene, v as diagnose, w as registerBehavior } from "./loader-r49nDwB4.js";
3
- import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-DuTlY56W.js";
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
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-By8mslMc.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-DRi9524r.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-pGTHb_V4.js";
10
- import { n as enablePhysics3D } from "./physics-3d-CumBBvQo.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";
11
11
  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
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -486,6 +486,76 @@ function setEnvironment3D(engine, patch) {
486
486
  scene.environment = merged;
487
487
  }
488
488
  //#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
489
559
  //#region src/3d/adaptive-resolution.ts
490
560
  const DEFAULTS = {
491
561
  slowMs: 20,
@@ -1869,6 +1939,50 @@ var Renderer3D = class {
1869
1939
  else if (clouds && !underwater) this.renderWithClouds(activeCamera, clouds, threeLight);
1870
1940
  else if ((bloom || post) && !underwater) this.renderWithBloom(activeCamera, bloom, post);
1871
1941
  else this.webgl.render(this.threeScene, activeCamera);
1942
+ this.drainFrameCapture();
1943
+ }
1944
+ /**
1945
+ * A frame's pixels, read on the NEXT frame that draws.
1946
+ *
1947
+ * `preserveDrawingBuffer` is false by default — with good reason, it costs
1948
+ * bandwidth on every frame — so `canvas.toDataURL()` comes back blank, which
1949
+ * is the trap this repo has fallen into before. Reading with `gl.readPixels`
1950
+ * at the END of a render, in the same frame, needs no such flag and no
1951
+ * per-frame cost: nothing happens at all until somebody asks.
1952
+ *
1953
+ * That is why this lives in the engine rather than in a generic screenshot
1954
+ * tool. An outside tool cannot get inside the frame.
1955
+ */
1956
+ captureFrame() {
1957
+ return new Promise((resolve, reject) => {
1958
+ this.pendingCaptures.push({
1959
+ resolve,
1960
+ reject
1961
+ });
1962
+ });
1963
+ }
1964
+ pendingCaptures = [];
1965
+ drainFrameCapture() {
1966
+ if (this.pendingCaptures.length === 0) return;
1967
+ const waiting = this.pendingCaptures;
1968
+ this.pendingCaptures = [];
1969
+ try {
1970
+ const gl = this.webgl.getContext();
1971
+ const width = this.webgl.domElement.width;
1972
+ const height = this.webgl.domElement.height;
1973
+ const pixels = new Uint8Array(width * height * 4);
1974
+ this.webgl.setRenderTarget(null);
1975
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
1976
+ flipRows(pixels, width, height);
1977
+ for (const w of waiting) w.resolve({
1978
+ pixels,
1979
+ width,
1980
+ height
1981
+ });
1982
+ } catch (error) {
1983
+ const e = error instanceof Error ? error : new Error(String(error));
1984
+ for (const w of waiting) w.reject(e);
1985
+ }
1872
1986
  }
1873
1987
  /**
1874
1988
  * Bloom post pass (only when `environment.bloom` is declared). LDR and safe:
@@ -2235,6 +2349,18 @@ const camPosScratch = new Vector3();
2235
2349
  /** Reused click-pick scratches (no per-pick allocation). */
2236
2350
  const pickRaycaster = new Raycaster();
2237
2351
  const pickNdc = new Vector2();
2352
+ /** Turn a bottom-left GL buffer into top-left screen order, in place. */
2353
+ function flipRows(pixels, width, height) {
2354
+ const stride = width * 4;
2355
+ const row = new Uint8Array(stride);
2356
+ for (let y = 0; y < Math.floor(height / 2); y++) {
2357
+ const top = y * stride;
2358
+ const bottom = (height - 1 - y) * stride;
2359
+ row.set(pixels.subarray(top, top + stride));
2360
+ pixels.copyWithin(top, bottom, bottom + stride);
2361
+ pixels.set(row, bottom);
2362
+ }
2363
+ }
2238
2364
  //#endregion
2239
2365
  //#region src/3d/create-game.ts
2240
2366
  var create_game_exports = /* @__PURE__ */ __exportAll({ createGame3D: () => createGame3D });
@@ -2378,6 +2504,27 @@ async function createGame3D(opts) {
2378
2504
  cleanups.push(engine.settings.bindRenderScale((scale) => {
2379
2505
  renderer.setRenderScale?.(scale);
2380
2506
  }));
2507
+ const captureReport = async (frameOpts) => {
2508
+ const capture = renderer.captureFrame;
2509
+ 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);
2512
+ };
2513
+ const hot = import.meta.hot;
2514
+ if (hot) {
2515
+ const onFrameRequest = (data) => {
2516
+ const id = data?.id;
2517
+ captureReport().then((frameReport) => hot.send("incanto:frame-report", {
2518
+ id,
2519
+ report: frameReport
2520
+ })).catch((error) => hot.send("incanto:frame-report", {
2521
+ id,
2522
+ report: { error: error instanceof Error ? error.message : String(error) }
2523
+ }));
2524
+ };
2525
+ hot.on("incanto:frame-request", onFrameRequest);
2526
+ cleanups.push(() => hot.off?.("incanto:frame-request", onFrameRequest));
2527
+ }
2381
2528
  await report(1, "ready");
2382
2529
  function disposeGame() {
2383
2530
  teardown([
@@ -2398,6 +2545,12 @@ async function createGame3D(opts) {
2398
2545
  assetErrors() {
2399
2546
  return renderer.assets?.errors() ?? [];
2400
2547
  },
2548
+ async frame(frameOpts) {
2549
+ const capture = renderer.captureFrame;
2550
+ if (!capture) throw new IncantoError("TREE_VIOLATION", "this renderer cannot capture frames");
2551
+ const shot = await capture.call(renderer);
2552
+ return frameStats(shot.pixels, shot.width, shot.height, frameOpts);
2553
+ },
2401
2554
  pick(x, y) {
2402
2555
  return engine.picker?.(x, y) ?? null;
2403
2556
  },
@@ -2464,4 +2617,4 @@ function wireAudioUnlock(engine, root, canvas) {
2464
2617
  return detachAll;
2465
2618
  }
2466
2619
  //#endregion
2467
- 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 };
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 };
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { At as LogLevel, T as RendererStats, b as Engine } from "./behavior-TA8nySqb.js";
1
+ import { At as LogLevel, T as RendererStats, b as Engine } from "./behavior-62q0HWBO.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-DuTlY56W.js";
1
+ import { t as registerCoreNodes } from "./register-BSXV8T9F.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-By8mslMc.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-DRi9524r.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";
@@ -5166,7 +5166,20 @@ var CameraShake = class extends Behavior {
5166
5166
  magnitude = 0;
5167
5167
  t = 0;
5168
5168
  prev = [];
5169
+ /**
5170
+ * Start a shake — unless the player asked for no motion.
5171
+ *
5172
+ * `settings.reduceMotion` is an accessibility switch, and camera movement the
5173
+ * player did not cause is the vestibular trigger it exists for. Checked HERE
5174
+ * rather than in `update` so a shake requested while it is on leaves nothing
5175
+ * queued to play the moment it is turned off.
5176
+ *
5177
+ * It deliberately does NOT touch gameplay timing (`hitStop`): freezing time
5178
+ * for 80 ms is feedback, not motion, and removing it would change what the
5179
+ * game is rather than how it moves.
5180
+ */
5169
5181
  shake(magnitude, seconds) {
5182
+ if (this.engine?.settings.get("reduceMotion") === true) return;
5170
5183
  this.magnitude = Math.max(this.magnitude, magnitude);
5171
5184
  this.t = seconds ?? this.falloff;
5172
5185
  }
@@ -1,4 +1,4 @@
1
- import { Et as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-TA8nySqb.js";
1
+ import { Et as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-62q0HWBO.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
@@ -685,6 +685,18 @@ declare class CameraShake extends Behavior {
685
685
  private magnitude;
686
686
  private t;
687
687
  private prev;
688
+ /**
689
+ * Start a shake — unless the player asked for no motion.
690
+ *
691
+ * `settings.reduceMotion` is an accessibility switch, and camera movement the
692
+ * player did not cause is the vestibular trigger it exists for. Checked HERE
693
+ * rather than in `update` so a shake requested while it is on leaves nothing
694
+ * queued to play the moment it is turned off.
695
+ *
696
+ * It deliberately does NOT touch gameplay timing (`hitStop`): freezing time
697
+ * for 80 ms is feedback, not motion, and removing it would change what the
698
+ * game is rather than how it moves.
699
+ */
688
700
  shake(magnitude: number, seconds?: number): void;
689
701
  override update(dt: number): void;
690
702
  }
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-By8mslMc.js";
1
+ import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-DRi9524r.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };