incanto 0.37.0 → 0.39.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/dist/2d.d.ts +2 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +87 -3
  4. package/dist/3d.js +4 -32
  5. package/dist/{behavior-5o1EkbLD.d.ts → behavior-BNNSIXS5.d.ts} +135 -7
  6. package/dist/{create-game-DmGc-Mrn.js → create-game-DEtU2PRg.js} +4 -4
  7. package/dist/{create-game-QqrrK3yr.js → create-game-R_8_gTCU.js} +790 -689
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/editor.js +21 -0
  10. package/dist/{environment-presets-DWHxLHM5.js → environment-presets-bokhdw_w.js} +2 -2
  11. package/dist/{gameplay-CsJDUQh_.js → gameplay-BPGomkHR.js} +83 -13
  12. package/dist/gameplay.d.ts +1 -1
  13. package/dist/gameplay.js +1 -1
  14. package/dist/index.d.ts +4 -4
  15. package/dist/index.js +3 -3
  16. package/dist/{loader-iGq2PT_k.d.ts → loader-mSL65Djg.d.ts} +1 -1
  17. package/dist/net.d.ts +1 -1
  18. package/dist/net.js +2 -2
  19. package/dist/{pathfinding-pPrqtlWS.d.ts → pathfinding-D7W8uqm1.d.ts} +1 -1
  20. package/dist/{physics-2d-C8Hi40qb.js → physics-2d-B9ktSWrr.js} +1 -1
  21. package/dist/{physics-3d-C0mWoWXC.js → physics-3d-Cpgc5ZM0.js} +2 -2
  22. package/dist/react.d.ts +1 -1
  23. package/dist/react.js +1 -1
  24. package/dist/{register-DFSW2Y30.js → register-C2iO_yLv.js} +1 -1
  25. package/dist/{register-DOWGnxe1.js → register-CGf5NKrc.js} +321 -33
  26. package/dist/{register-t15rydSm.js → register-CiXP3-GB.js} +1 -1
  27. package/dist/{replay-DGzhZWrM.d.ts → replay-CwdWgz0f.d.ts} +1 -1
  28. package/dist/{replay-BuOTl5rM.js → replay-YQ9FW7Yh.js} +1 -1
  29. package/dist/{test-BiIO9ULW.js → test-Bf-lUxr2.js} +10 -10
  30. package/dist/test.d.ts +3 -3
  31. package/dist/test.js +2 -2
  32. package/dist/vite.js +1 -1
  33. package/editor/assets/{agent8-CeeI7P_0.js → agent8-DfIcNKb4.js} +1 -1
  34. package/editor/assets/{debug-DN9X9G6z.js → debug-ex82CxIM.js} +1 -1
  35. package/editor/assets/{index-C833FBzB.js → index-eVTVArHd.js} +91 -91
  36. package/editor/index.html +1 -1
  37. package/package.json +1 -1
  38. package/schemas/scene.schema.json +294 -0
  39. package/skills/incanto-environment.md +21 -0
  40. package/skills/incanto-node-reference.md +45 -0
  41. package/skills/incanto-performance.md +219 -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
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-5o1EkbLD.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-BNNSIXS5.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-iGq2PT_k.js";
4
+ import { t as LoadSceneOptions } from "./loader-mSL65Djg.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-DmGc-Mrn.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-DFSW2Y30.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-C8Hi40qb.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DEtU2PRg.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-C2iO_yLv.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-B9ktSWrr.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 { N as Scene$1, S as Scheduler, T as RendererStats, Tt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-5o1EkbLD.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-BNNSIXS5.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-iGq2PT_k.js";
4
+ import { t as LoadSceneOptions } from "./loader-mSL65Djg.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-pPrqtlWS.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-D7W8uqm1.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";
@@ -3006,6 +3006,48 @@ declare class Water3D extends Node3D implements RenderHook3D, SunConsumer3D {
3006
3006
  underwaterAt(camX: number, camY: number, camZ: number): (UnderwaterConfig & {
3007
3007
  surfaceY: number;
3008
3008
  }) | null;
3009
+ /**
3010
+ * Give the GPU allocations back.
3011
+ *
3012
+ * A fancy surface holds a cube target, a half-res scene pre-pass (colour +
3013
+ * depth) and — with `mirror` on — a mirror target at half the drawing buffer,
3014
+ * 4x MSAA, half-float. Tens of megabytes, PER WATER NODE.
3015
+ *
3016
+ * Until now the only thing that released any of it was `free()`, so every way
3017
+ * of stopping using one kept all of it: turning the mirror off zeroed a
3018
+ * uniform, and water off camera or under the player simply returned early.
3019
+ * Survivable while quality was decided once at load; not survivable now that
3020
+ * a settings menu turns the mirror off on a running game.
3021
+ */
3022
+ private releasePassTargets;
3023
+ /**
3024
+ * Frames in a row that took no pass at all (submerged, or off camera).
3025
+ *
3026
+ * Releasing on the FIRST idle frame would be worse than the leak: a camera
3027
+ * panning past a lake would allocate and free a mirror target every other
3028
+ * frame, and allocation is not free either. Only a surface nobody has looked
3029
+ * at for a couple of seconds hands its memory back.
3030
+ */
3031
+ private idleFrames;
3032
+ /**
3033
+ * The tier's ceiling, or null when nothing has told us (headless, tests, a
3034
+ * renderer that does not push caps). Null means "no ceiling" — the authored
3035
+ * props stand.
3036
+ */
3037
+ private caps;
3038
+ /** @internal See {@link QualityConsumer3D} — a ceiling, never an upgrade. */
3039
+ _applyQualityCaps(caps: QualityCaps): void;
3040
+ /**
3041
+ * What this surface actually draws, once the tier has had its say.
3042
+ *
3043
+ * ONE-DIRECTIONAL: a cap can turn fancy into simple, never simple into fancy.
3044
+ * And it never writes `this.quality` — a tier is a runtime ceiling, not an
3045
+ * edit, so serializing a game running at low still writes what the author
3046
+ * chose.
3047
+ */
3048
+ private effectiveQuality;
3049
+ /** One frame that took no pass; release everything once it has been a while. */
3050
+ private noteIdleFrame;
3009
3051
  override free(): void;
3010
3052
  /** Swap geometry+material only when a surface-shaping prop changed. */
3011
3053
  private rebuildSurfaceIfNeeded;
@@ -3125,6 +3167,19 @@ interface Renderer3DOptions {
3125
3167
  /** Defaults to `window.devicePixelRatio` capped at 2. */
3126
3168
  pixelRatio?: number;
3127
3169
  /**
3170
+ * Starting point for the rendering knobs, from the player's quality tier.
3171
+ *
3172
+ * DEFAULTS, not overrides: a scene's own `environment.rendering` is a decision
3173
+ * about that scene and still wins, and an explicit `pixelRatio` option wins
3174
+ * over both. This only decides what a device that has said nothing gets —
3175
+ * which for `antialias` is the only chance there is, since it is a WebGL
3176
+ * context attribute and cannot change after the context exists.
3177
+ */
3178
+ qualityDefaults?: {
3179
+ antialias: boolean;
3180
+ pixelRatio: number;
3181
+ };
3182
+ /**
3128
3183
  * Keep the frame rate by rendering at fewer pixels when frames run slow, and
3129
3184
  * hand them back when they recover (default ON). A retina display asks for
3130
3185
  * 4× the pixels; shaded water is fragment-heavy; the honest answer is fewer
@@ -3222,6 +3277,8 @@ declare class Renderer3D {
3222
3277
  /** The pixel ratio the scene ASKED for — the governor scales this, never replaces it. */
3223
3278
  private readonly basePixelRatio;
3224
3279
  private readonly adaptive;
3280
+ private renderScale;
3281
+ private adaptiveScale;
3225
3282
  private lastFrameAt;
3226
3283
  private readonly debugLines;
3227
3284
  private readonly selectionLines;
@@ -3236,6 +3293,21 @@ declare class Renderer3D {
3236
3293
  */
3237
3294
  private governResolution;
3238
3295
  /**
3296
+ * The player's resolution multiplier, on top of whatever the scene and the
3297
+ * quality tier resolved to.
3298
+ *
3299
+ * The cheapest lever there is on a weak GPU: at 0.66 the fragment shaders run
3300
+ * over 44% of the pixels for a soft image and nothing else, where dropping a
3301
+ * quality tier costs the shadows outright. Live — a settings menu writes it
3302
+ * mid-game — and it multiplies with the adaptive governor rather than
3303
+ * fighting it, so a player who has already scaled down does not get scaled
3304
+ * down twice as hard.
3305
+ */
3306
+ setRenderScale(scale: number): void;
3307
+ /** Judge frames against `fps` — see AdaptiveResolution.setTarget. */
3308
+ setFrameTarget(fps: number): void;
3309
+ private applyPixelRatio;
3310
+ /**
3239
3311
  * Compile the scene's GPU programs off the critical path.
3240
3312
  *
3241
3313
  * The compile itself is unavoidable — a dressed outdoor scene is 40+ programs
@@ -3378,6 +3450,17 @@ interface SyncOptions {
3378
3450
  /** Render interpolation factor (0..1) for fixed-step physics bodies; see
3379
3451
  * Engine.interpolationAlpha. Defaults to 1 (render the current transform). */
3380
3452
  alpha?: number;
3453
+ /**
3454
+ * The quality tier's ceiling, pushed to every `_applyQualityCaps` node
3455
+ * (Water3D) BEFORE its own sync — a node that rebuilds because the cap
3456
+ * changed must do it in the same frame, not the next one.
3457
+ *
3458
+ * The same route as `sunDirection` and for the same reason: the walk already
3459
+ * touches every node, so nothing has to subscribe or unsubscribe. A `static`
3460
+ * subtree is pruned before this point and never receives caps — which is
3461
+ * correct, since it is not being synced at all.
3462
+ */
3463
+ quality?: QualityCaps | null;
3381
3464
  }
3382
3465
  /**
3383
3466
  * Mirror an Incanto node tree onto a three.js scene (dirty-push, once per frame):
@@ -3404,6 +3487,7 @@ interface WalkState {
3404
3487
  sunLight: DirectionalLight3D | null;
3405
3488
  alpha: number;
3406
3489
  ignoreStatic: boolean;
3490
+ quality: QualityCaps | null;
3407
3491
  }
3408
3492
  //#endregion
3409
3493
  //#region src/3d/terrain-nav.d.ts
package/dist/3d.js CHANGED
@@ -1,36 +1,8 @@
1
- import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- 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-CsJDUQh_.js";
3
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-DWHxLHM5.js";
4
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-QqrrK3yr.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-BPGomkHR.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-bokhdw_w.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-R_8_gTCU.js";
5
4
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-C0mWoWXC.js";
7
- //#region src/3d/environment-runtime.ts
8
- /**
9
- * Live environment editing — the renderer re-applies `scene.environment`
10
- * every frame (cheap when unchanged), so changing the LOOK at runtime is
11
- * just writing that object. This helper does it safely:
12
- *
13
- * setEnvironment3D(engine, { sky: { elevationDeg: 4 }, exposure: 0.8 });
14
- * setEnvironment3D(engine, { fog: null }); // null DELETES a key
15
- *
16
- * - one-level deep merge (patching `sky.elevationDeg` keeps the other sky keys)
17
- * - hard-validates the merged result NOW (in your call stack), so a bad patch
18
- * throws where you wrote it — never inside the render loop
19
- */
20
- function setEnvironment3D(engine, patch) {
21
- const scene = engine.scene;
22
- if (!scene) throw new IncantoError("BAD_FORMAT", "setEnvironment3D: the engine has no scene.");
23
- const merged = { ...scene.environment ?? {} };
24
- for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
25
- else if (typeof value === "object" && !Array.isArray(value) && typeof merged[key] === "object" && merged[key] !== null && !Array.isArray(merged[key])) merged[key] = {
26
- ...merged[key],
27
- ...value
28
- };
29
- else merged[key] = value;
30
- parseEnvironment3D(merged);
31
- scene.environment = merged;
32
- }
33
- //#endregion
5
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-Cpgc5ZM0.js";
34
6
  //#region src/3d/terrain-nav.ts
35
7
  function buildTerrainNav(terrain, opts) {
36
8
  const cellSize = opts?.cellSize ?? 2;
@@ -1091,6 +1091,21 @@ interface SettingsValues {
1091
1091
  * does not translate falls back to English, silently and on purpose.
1092
1092
  */
1093
1093
  language: string;
1094
+ /**
1095
+ * Has a HUMAN chosen the quality tier?
1096
+ *
1097
+ * Without this, a first boot cannot tell "the default happens to be high" from
1098
+ * "the player picked high", so device auto-detection would either never run or
1099
+ * would overwrite a deliberate choice on every launch.
1100
+ */
1101
+ qualityChosen: boolean;
1102
+ /** Frames per second to draw at most. 0 = uncapped (draw every rAF). */
1103
+ maxFps: number;
1104
+ /**
1105
+ * Extra multiplier on the render resolution, on top of the tier's pixel ratio.
1106
+ * 1 = as the tier says. Below 1 trades sharpness for frame rate.
1107
+ */
1108
+ renderScale: number;
1094
1109
  }
1095
1110
  /** What a device probe can see without a GPU context. */
1096
1111
  interface DeviceHints {
@@ -1122,6 +1137,43 @@ declare class Settings {
1122
1137
  /** Back to defaults, on disk too. */
1123
1138
  reset(): void;
1124
1139
  /**
1140
+ * Choose a starting tier from the device, ONCE.
1141
+ *
1142
+ * Only on a first visit: after someone has picked a tier, re-detecting on
1143
+ * every launch would silently undo their choice — which is the behaviour every
1144
+ * player experiences as "the settings do not save".
1145
+ */
1146
+ autoQuality(hints?: DeviceHints): QualityTier;
1147
+ /** Record that the tier came from a person, not from the device probe. */
1148
+ chooseQuality(tier: QualityTier): void;
1149
+ /**
1150
+ * Apply the saved quality tier to a live scene, and keep applying it.
1151
+ *
1152
+ * The same shape as `bindAudio` and `bindLocale`. Until 0.38 this did not
1153
+ * exist: `settings.quality` was written to disk and read by NOBODY, and
1154
+ * `qualityEnvironment` — the function that turns a tier into something the
1155
+ * renderer understands — emitted patches the environment parser rejects, for
1156
+ * all three tiers. Both were public, documented and never once called.
1157
+ */
1158
+ bindQuality(apply: (patch: Record<string, JsonValue>) => void): () => void;
1159
+ /**
1160
+ * Apply the saved frame cap, and again whenever it changes.
1161
+ *
1162
+ * Separate from `bindQuality` because it is not a tier: a player caps the
1163
+ * frame rate to keep a phone cool or a battery alive, on the highest visual
1164
+ * settings the device can hold, and those two choices do not belong on one
1165
+ * slider. 0 means uncapped. Returns the unsubscribe.
1166
+ */
1167
+ bindFrameCap(apply: (fps: number) => void): () => void;
1168
+ /**
1169
+ * Apply the saved resolution multiplier, and again whenever it changes.
1170
+ *
1171
+ * The lever that buys the most frames per unit of ugliness on a weak GPU —
1172
+ * shading 44% of the pixels at 0.66 costs a soft image and nothing else,
1173
+ * while dropping a quality tier costs shadows entirely.
1174
+ */
1175
+ bindRenderScale(apply: (scale: number) => void): () => void;
1176
+ /**
1125
1177
  * Apply the saved language, and keep it in step both ways.
1126
1178
  *
1127
1179
  * The same shape as `bindAudio`: a game that writes
@@ -1139,15 +1191,63 @@ declare class Settings {
1139
1191
  bindAudio(audio: AudioBuses): () => void;
1140
1192
  }
1141
1193
  /**
1142
- * What a quality tier MEANS, as an environment patch.
1194
+ * What a quality tier MEANS, as a LIVE environment patch.
1195
+ *
1196
+ * Only the levers that actually cost: shadows, post, bloom and clouds.
1197
+ * Deliberately an `environment` patch, because `setEnvironment3D(engine, patch)`
1198
+ * already applies one live and validated — this is not a second rendering
1199
+ * pipeline, it is three presets for the one that exists.
1200
+ *
1201
+ * The `off` spelling matters and got this wrong for three releases: every tier
1202
+ * emitted `{ enabled: false }`, which the environment parser rejects
1203
+ * (`environment.bloom has unknown key 'enabled'`). Nothing called this function,
1204
+ * so nothing found out. In a live patch `null` DELETES a key — that is how a
1205
+ * stage is turned off — and `shadows` alone takes a real `false`.
1143
1206
  *
1144
- * Only the levers that actually cost: shadows, post, clouds, and the resolution
1145
- * the renderer draws at. Deliberately an `environment` patch, because
1146
- * `setEnvironment3D(engine, patch)` already applies one live and validated —
1147
- * this is not a second rendering pipeline, it is three presets for the one that
1148
- * exists.
1207
+ * Antialiasing and pixel ratio are NOT here: `antialias` is a WebGL context
1208
+ * attribute and cannot change without rebuilding the renderer. See
1209
+ * `qualityRendering`.
1149
1210
  */
1150
1211
  declare function qualityEnvironment(tier: QualityTier): Record<string, JsonValue>;
1212
+ /**
1213
+ * What a quality tier means for the levers that can only be set at BOOT.
1214
+ *
1215
+ * `antialias` is a context attribute: three fixes it when the WebGL context is
1216
+ * created, so a settings menu that changes it is choosing what the NEXT launch
1217
+ * gets. Every engine has this constraint and every game's options screen says
1218
+ * "requires restart" next to it — `createGame3D` reads the saved tier at boot,
1219
+ * so the restart is all it takes.
1220
+ *
1221
+ * `pixelRatio` CAN change live (adaptive resolution already does it), but it
1222
+ * belongs with antialias as "how many pixels do we draw" rather than "what is in
1223
+ * the scene".
1224
+ */
1225
+ /**
1226
+ * The per-tier CEILING for nodes that are expensive on their own.
1227
+ *
1228
+ * The quality tier travels as an environment patch, and an environment patch
1229
+ * can only reach environment stages — shadows, bloom, post, clouds. Water is
1230
+ * the single most expensive thing this engine draws and it is a NODE, so no
1231
+ * tier could touch it.
1232
+ *
1233
+ * These caps ride the render sync walk instead (see `SyncOptions.quality`). The
1234
+ * contract is one-directional and everything rests on it: **a cap clamps DOWN,
1235
+ * never up.** A tier may take away what the scene asked for; it may never give
1236
+ * a scene something it did not ask for, or `high` on a phone would silently
1237
+ * upgrade a deliberately cheap pond into a mirror.
1238
+ */
1239
+ interface QualityCaps {
1240
+ /**
1241
+ * Whether a water surface may run its multi-pass "fancy" path at all.
1242
+ *
1243
+ * False collapses it to the single-pass lake shader: no planar mirror, no
1244
+ * reflection cube, no refraction grab, no depth completion — 8.4 scene
1245
+ * submissions a frame become 3.5. It is the largest single saving available
1246
+ * on a low-end device, and it is not free: the simple shader has no samplers,
1247
+ * so the shoreline dissolve, contact foam and depth absorption go with it.
1248
+ */
1249
+ fancyWater: boolean;
1250
+ }
1151
1251
  //#endregion
1152
1252
  //#region src/core/stats.d.ts
1153
1253
  /**
@@ -1195,6 +1295,8 @@ interface EngineOptions {
1195
1295
  scheduler?: Scheduler;
1196
1296
  /** Seed for `engine.rng` — set it to make a run reproducible (default: random). */
1197
1297
  seed?: number;
1298
+ /** Frame cap in fps; 0 (default) runs at whatever the display offers. */
1299
+ maxFps?: number;
1198
1300
  }
1199
1301
  /**
1200
1302
  * The game loop: drives a Scene's tree with fixed-timestep `fixedUpdate`
@@ -1306,6 +1408,32 @@ declare class Engine {
1306
1408
  setScene(scene: Scene): void;
1307
1409
  start(): void;
1308
1410
  /**
1411
+ * The frame cap: fps, or 0 for "as fast as the display goes" (the default).
1412
+ *
1413
+ * Live — a settings menu writes it mid-game. It gates the whole frame, not
1414
+ * just the render: an uncapped loop on a 144 Hz phone burns battery running
1415
+ * game logic nobody sees.
1416
+ *
1417
+ * `tick()` is deliberately NOT gated. A headless harness driving its own
1418
+ * clock asked for that frame; the cap is about the real-time loop.
1419
+ */
1420
+ maxFps: number;
1421
+ private nextFrameAt;
1422
+ private lastOfferedAt;
1423
+ private displayPeriodMs;
1424
+ /**
1425
+ * Should this display frame run?
1426
+ *
1427
+ * The obvious `if (now - last < interval) return` is wrong in a way that only
1428
+ * shows up between the round numbers: a 45 fps cap on a 60 Hz display misses
1429
+ * every other deadline by a fraction of a millisecond, waits a whole display
1430
+ * frame more, and delivers 30 — asking for 45 makes the game slower than not
1431
+ * capping at all. The fix is to allow a frame that is within half a display
1432
+ * period of its deadline, and to advance the deadline by the interval rather
1433
+ * than from `now`, so the error does not accumulate.
1434
+ */
1435
+ private frameIsDue;
1436
+ /**
1309
1437
  * A frame threw somewhere outside a single node (a renderer, physics, a
1310
1438
  * signal handler). Report the first one and count the rest: a broken frame
1311
1439
  * repeats sixty times a second, and sixty identical traces a second is how
@@ -1531,4 +1659,4 @@ declare function registeredBehaviors(): string[];
1531
1659
  /** Test isolation helper. */
1532
1660
  declare function clearBehaviors(): void;
1533
1661
  //#endregion
1534
- export { translationKey as $, qualityEnvironment as A, LogManager as At, restoreBehaviors as B, EngineStats as C, AudioBuses as Ct, QualityTier as D, SceneTree as Dt, DeviceHints as E, NodeLifecycle as Et, RestoreReport as F, OrderGroupTable as G, createSaveStore as H, SaveSlot as I, BASE_LOCALE as J, effectiveOrder as K, SaveSlots as L, suggestQuality as M, SignalListener as Mt, Scene as N, Settings as O, LogEntry as Ot, BehaviorState as P, suggestLocale as Q, behaviorsWithoutSave as R, Scheduler as S, PlayMusicOptions as St, RendererStats as T, Node as Tt, ORDER_GROUP_BASE as U, SaveStore as V, OrderGroup as W, Localization as X, LocaleTables as Y, T_PREFIX as Z, mergeStaticSignals as _, SynthOptions as _t, clearBehaviors as a, VoicePreset as at, Engine as b, MusicManager as bt, registeredBehaviors as c, RolloffModel as ct, PropSchema as d, spatialGain as dt, InputMap as et, clearRegistry as f, spatialPan as ft, getNodeType as g, SfxWave as gt, getNodeSignals as h, SfxParams as ht, behaviorSignals as i, Voice as it, readDeviceHints as j, Signal as jt, SettingsValues as k, LogLevel as kt, NodeCtor as l, SpatialParams as lt, getNodeSchema as m, SFX_PRESET_NAMES as mt, BehaviorCtor as n, SfxPlayOptions as nt, getBehavior as o, Listener as ot, createNode as p, SFX_PRESETS as pt, resolveOrderGroups as q, behaviorSchema as r, isAudioContextAvailable as rt, registerBehavior as s, ROLLOFF_MODELS as st, Behavior as t, SfxEngine as tt, PropDef as u, Vec3 as ut, registerNode as v, synthSfx as vt, GameStats as w, BusName as wt, EngineOptions as x, MusicTrack as xt, registeredTypes as y, MusicBackend as yt, captureBehaviors as z };
1662
+ export { suggestLocale as $, SettingsValues as A, LogLevel as At, captureBehaviors as B, EngineStats as C, PlayMusicOptions as Ct, QualityCaps as D, NodeLifecycle as Dt, DeviceHints as E, Node as Et, BehaviorState as F, OrderGroup as G, SaveStore as H, RestoreReport as I, resolveOrderGroups as J, OrderGroupTable as K, SaveSlot as L, readDeviceHints as M, Signal as Mt, suggestQuality as N, SignalListener as Nt, QualityTier as O, SceneTree as Ot, Scene as P, T_PREFIX as Q, SaveSlots as R, Scheduler as S, MusicTrack as St, RendererStats as T, BusName as Tt, createSaveStore as U, restoreBehaviors as V, ORDER_GROUP_BASE as W, LocaleTables as X, BASE_LOCALE as Y, Localization as Z, mergeStaticSignals as _, SfxWave as _t, clearBehaviors as a, Voice as at, Engine as b, MusicBackend as bt, registeredBehaviors as c, ROLLOFF_MODELS as ct, PropSchema as d, Vec3 as dt, translationKey as et, clearRegistry as f, spatialGain as ft, getNodeType as g, SfxParams as gt, getNodeSignals as h, SFX_PRESET_NAMES as ht, behaviorSignals as i, isAudioContextAvailable as it, qualityEnvironment as j, LogManager as jt, Settings as k, LogEntry as kt, NodeCtor as l, RolloffModel as lt, getNodeSchema as m, SFX_PRESETS as mt, BehaviorCtor as n, SfxEngine as nt, getBehavior as o, VoicePreset as ot, createNode as p, spatialPan as pt, effectiveOrder as q, behaviorSchema as r, SfxPlayOptions as rt, registerBehavior as s, Listener as st, Behavior as t, InputMap as tt, PropDef as u, SpatialParams as ut, registerNode as v, SynthOptions as vt, GameStats as w, AudioBuses as wt, EngineOptions as x, MusicManager as xt, registeredTypes as y, synthSfx as yt, behaviorsWithoutSave as z };
@@ -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-DOWGnxe1.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-CGf5NKrc.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-CsJDUQh_.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DFSW2Y30.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BPGomkHR.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-C2iO_yLv.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-C8Hi40qb.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-B9ktSWrr.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
  /**