incanto 0.37.0 → 0.38.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/dist/2d.d.ts +2 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +33 -3
  4. package/dist/3d.js +3 -31
  5. package/dist/{behavior-5o1EkbLD.d.ts → behavior-B5PJ32eu.d.ts} +95 -6
  6. package/dist/{create-game-DmGc-Mrn.js → create-game-C4t5gVhc.js} +3 -3
  7. package/dist/{create-game-QqrrK3yr.js → create-game-DqY2lMBc.js} +781 -687
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/editor.js +21 -0
  10. package/dist/{environment-presets-DWHxLHM5.js → environment-presets-C0Axpfth.js} +1 -1
  11. package/dist/gameplay.d.ts +1 -1
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +3 -3
  14. package/dist/{loader-iGq2PT_k.d.ts → loader-DSMdqasu.d.ts} +1 -1
  15. package/dist/net.d.ts +1 -1
  16. package/dist/net.js +2 -2
  17. package/dist/{pathfinding-pPrqtlWS.d.ts → pathfinding-BrE5Uec6.d.ts} +1 -1
  18. package/dist/{physics-2d-C8Hi40qb.js → physics-2d-OfKb0PV_.js} +1 -1
  19. package/dist/{physics-3d-C0mWoWXC.js → physics-3d-C8242nXr.js} +1 -1
  20. package/dist/react.d.ts +1 -1
  21. package/dist/react.js +1 -1
  22. package/dist/{register-t15rydSm.js → register-B4RRQbyq.js} +1 -1
  23. package/dist/{register-DOWGnxe1.js → register-Dm7nrd-E.js} +321 -33
  24. package/dist/{register-DFSW2Y30.js → register-DnB9m7Rl.js} +1 -1
  25. package/dist/{replay-DGzhZWrM.d.ts → replay-Cxrr-ApH.d.ts} +1 -1
  26. package/dist/{replay-BuOTl5rM.js → replay-DH0UOuZ-.js} +1 -1
  27. package/dist/{test-BiIO9ULW.js → test-CoDPWx-m.js} +9 -9
  28. package/dist/test.d.ts +3 -3
  29. package/dist/test.js +2 -2
  30. package/dist/vite.js +1 -1
  31. package/editor/assets/{agent8-CeeI7P_0.js → agent8-BtIlblPm.js} +1 -1
  32. package/editor/assets/{debug-DN9X9G6z.js → debug-BvTmHXI7.js} +1 -1
  33. package/editor/assets/{index-C833FBzB.js → index-BS01WWgM.js} +91 -91
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +294 -0
  37. package/skills/incanto-node-reference.md +45 -0
  38. package/skills/incanto-performance.md +212 -0
  39. package/templates-app/beacon-isle-3d/package.json +1 -1
  40. package/templates-app/tps-3d/package.json +1 -1
  41. package/templates-app/village-quest-3d/package.json +1 -1
package/editor/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Incanto Scene Editor</title>
7
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><rect width='16' height='16' rx='3' fill='%236ee7dc'/><text x='8' y='12' text-anchor='middle' font-size='11' font-family='monospace' fill='%230e1018'>i</text></svg>" />
8
- <script type="module" crossorigin src="./assets/index-C833FBzB.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-BS01WWgM.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="./assets/GameServer-C56iOUgF.js">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incanto",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -343,6 +343,9 @@
343
343
  {
344
344
  "$ref": "#/$defs/UiDialogue"
345
345
  },
346
+ {
347
+ "$ref": "#/$defs/UiFrameCapSelect"
348
+ },
346
349
  {
347
350
  "$ref": "#/$defs/UiImage"
348
351
  },
@@ -352,6 +355,12 @@
352
355
  {
353
356
  "$ref": "#/$defs/UiPanel"
354
357
  },
358
+ {
359
+ "$ref": "#/$defs/UiQualitySelect"
360
+ },
361
+ {
362
+ "$ref": "#/$defs/UiRenderScaleSelect"
363
+ },
355
364
  {
356
365
  "$ref": "#/$defs/UiSelect"
357
366
  },
@@ -6560,6 +6569,101 @@
6560
6569
  },
6561
6570
  "required": ["name", "type"]
6562
6571
  },
6572
+ "UiFrameCapSelect": {
6573
+ "type": "object",
6574
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6575
+ "properties": {
6576
+ "name": {
6577
+ "type": "string"
6578
+ },
6579
+ "uid": {
6580
+ "type": "string"
6581
+ },
6582
+ "type": {
6583
+ "const": "UiFrameCapSelect"
6584
+ },
6585
+ "groups": {
6586
+ "type": "array",
6587
+ "items": {
6588
+ "type": "string"
6589
+ }
6590
+ },
6591
+ "tags": {
6592
+ "type": "object"
6593
+ },
6594
+ "props": {
6595
+ "type": "object",
6596
+ "properties": {
6597
+ "anchor": {
6598
+ "type": "string",
6599
+ "enum": [
6600
+ "topLeft",
6601
+ "top",
6602
+ "topRight",
6603
+ "left",
6604
+ "center",
6605
+ "right",
6606
+ "bottomLeft",
6607
+ "bottom",
6608
+ "bottomRight"
6609
+ ],
6610
+ "default": "topLeft"
6611
+ },
6612
+ "visible": {
6613
+ "type": "boolean",
6614
+ "default": true
6615
+ },
6616
+ "focusable": {
6617
+ "type": "boolean",
6618
+ "default": true
6619
+ },
6620
+ "draggable": {
6621
+ "type": "boolean",
6622
+ "default": false
6623
+ },
6624
+ "dropTarget": {
6625
+ "type": "boolean",
6626
+ "default": false
6627
+ },
6628
+ "label": {
6629
+ "type": "string",
6630
+ "default": "Frame rate"
6631
+ },
6632
+ "options": {
6633
+ "type": "string",
6634
+ "default": "30,60,120,0"
6635
+ },
6636
+ "value": {
6637
+ "type": "string",
6638
+ "default": ""
6639
+ }
6640
+ },
6641
+ "additionalProperties": false
6642
+ },
6643
+ "script": {
6644
+ "type": "object",
6645
+ "properties": {
6646
+ "name": {
6647
+ "type": "string"
6648
+ },
6649
+ "props": {
6650
+ "type": "object"
6651
+ }
6652
+ },
6653
+ "required": ["name"]
6654
+ },
6655
+ "network": {
6656
+ "type": "object"
6657
+ },
6658
+ "children": {
6659
+ "type": "array",
6660
+ "items": {
6661
+ "$ref": "#/$defs/node"
6662
+ }
6663
+ }
6664
+ },
6665
+ "required": ["name", "type"]
6666
+ },
6563
6667
  "UiImage": {
6564
6668
  "type": "object",
6565
6669
  "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped"],
@@ -6883,6 +6987,196 @@
6883
6987
  },
6884
6988
  "required": ["name", "type"]
6885
6989
  },
6990
+ "UiQualitySelect": {
6991
+ "type": "object",
6992
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
6993
+ "properties": {
6994
+ "name": {
6995
+ "type": "string"
6996
+ },
6997
+ "uid": {
6998
+ "type": "string"
6999
+ },
7000
+ "type": {
7001
+ "const": "UiQualitySelect"
7002
+ },
7003
+ "groups": {
7004
+ "type": "array",
7005
+ "items": {
7006
+ "type": "string"
7007
+ }
7008
+ },
7009
+ "tags": {
7010
+ "type": "object"
7011
+ },
7012
+ "props": {
7013
+ "type": "object",
7014
+ "properties": {
7015
+ "anchor": {
7016
+ "type": "string",
7017
+ "enum": [
7018
+ "topLeft",
7019
+ "top",
7020
+ "topRight",
7021
+ "left",
7022
+ "center",
7023
+ "right",
7024
+ "bottomLeft",
7025
+ "bottom",
7026
+ "bottomRight"
7027
+ ],
7028
+ "default": "topLeft"
7029
+ },
7030
+ "visible": {
7031
+ "type": "boolean",
7032
+ "default": true
7033
+ },
7034
+ "focusable": {
7035
+ "type": "boolean",
7036
+ "default": true
7037
+ },
7038
+ "draggable": {
7039
+ "type": "boolean",
7040
+ "default": false
7041
+ },
7042
+ "dropTarget": {
7043
+ "type": "boolean",
7044
+ "default": false
7045
+ },
7046
+ "label": {
7047
+ "type": "string",
7048
+ "default": "Quality"
7049
+ },
7050
+ "options": {
7051
+ "type": "string",
7052
+ "default": "low,medium,high"
7053
+ },
7054
+ "value": {
7055
+ "type": "string",
7056
+ "default": ""
7057
+ }
7058
+ },
7059
+ "additionalProperties": false
7060
+ },
7061
+ "script": {
7062
+ "type": "object",
7063
+ "properties": {
7064
+ "name": {
7065
+ "type": "string"
7066
+ },
7067
+ "props": {
7068
+ "type": "object"
7069
+ }
7070
+ },
7071
+ "required": ["name"]
7072
+ },
7073
+ "network": {
7074
+ "type": "object"
7075
+ },
7076
+ "children": {
7077
+ "type": "array",
7078
+ "items": {
7079
+ "$ref": "#/$defs/node"
7080
+ }
7081
+ }
7082
+ },
7083
+ "required": ["name", "type"]
7084
+ },
7085
+ "UiRenderScaleSelect": {
7086
+ "type": "object",
7087
+ "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
7088
+ "properties": {
7089
+ "name": {
7090
+ "type": "string"
7091
+ },
7092
+ "uid": {
7093
+ "type": "string"
7094
+ },
7095
+ "type": {
7096
+ "const": "UiRenderScaleSelect"
7097
+ },
7098
+ "groups": {
7099
+ "type": "array",
7100
+ "items": {
7101
+ "type": "string"
7102
+ }
7103
+ },
7104
+ "tags": {
7105
+ "type": "object"
7106
+ },
7107
+ "props": {
7108
+ "type": "object",
7109
+ "properties": {
7110
+ "anchor": {
7111
+ "type": "string",
7112
+ "enum": [
7113
+ "topLeft",
7114
+ "top",
7115
+ "topRight",
7116
+ "left",
7117
+ "center",
7118
+ "right",
7119
+ "bottomLeft",
7120
+ "bottom",
7121
+ "bottomRight"
7122
+ ],
7123
+ "default": "topLeft"
7124
+ },
7125
+ "visible": {
7126
+ "type": "boolean",
7127
+ "default": true
7128
+ },
7129
+ "focusable": {
7130
+ "type": "boolean",
7131
+ "default": true
7132
+ },
7133
+ "draggable": {
7134
+ "type": "boolean",
7135
+ "default": false
7136
+ },
7137
+ "dropTarget": {
7138
+ "type": "boolean",
7139
+ "default": false
7140
+ },
7141
+ "label": {
7142
+ "type": "string",
7143
+ "default": "Resolution"
7144
+ },
7145
+ "options": {
7146
+ "type": "string",
7147
+ "default": "0.5,0.75,1"
7148
+ },
7149
+ "value": {
7150
+ "type": "string",
7151
+ "default": ""
7152
+ }
7153
+ },
7154
+ "additionalProperties": false
7155
+ },
7156
+ "script": {
7157
+ "type": "object",
7158
+ "properties": {
7159
+ "name": {
7160
+ "type": "string"
7161
+ },
7162
+ "props": {
7163
+ "type": "object"
7164
+ }
7165
+ },
7166
+ "required": ["name"]
7167
+ },
7168
+ "network": {
7169
+ "type": "object"
7170
+ },
7171
+ "children": {
7172
+ "type": "array",
7173
+ "items": {
7174
+ "$ref": "#/$defs/node"
7175
+ }
7176
+ }
7177
+ },
7178
+ "required": ["name", "type"]
7179
+ },
6886
7180
  "UiSelect": {
6887
7181
  "type": "object",
6888
7182
  "x-signals": ["dragStarted", "dragCancelled", "droppedOn", "dropped", "changed"],
@@ -990,6 +990,21 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `presse
990
990
 
991
991
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `lineShown` · `choiceMade` · `dialogueFinished`
992
992
 
993
+ ## `UiFrameCapSelect` — `incanto`
994
+
995
+ | Prop | Default | Kind |
996
+ |---|---|---|
997
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
998
+ | `visible` | `true` | boolean |
999
+ | `focusable` | `true` | boolean |
1000
+ | `draggable` | `false` | boolean |
1001
+ | `dropTarget` | `false` | boolean |
1002
+ | `label` | `"Frame rate"` | string |
1003
+ | `options` | `"30,60,120,0"` | string |
1004
+ | `value` | `""` | string |
1005
+
1006
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1007
+
993
1008
  ## `UiImage` — `incanto`
994
1009
 
995
1010
  | Prop | Default | Kind |
@@ -1044,6 +1059,36 @@ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `change
1044
1059
 
1045
1060
  Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped`
1046
1061
 
1062
+ ## `UiQualitySelect` — `incanto`
1063
+
1064
+ | Prop | Default | Kind |
1065
+ |---|---|---|
1066
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1067
+ | `visible` | `true` | boolean |
1068
+ | `focusable` | `true` | boolean |
1069
+ | `draggable` | `false` | boolean |
1070
+ | `dropTarget` | `false` | boolean |
1071
+ | `label` | `"Quality"` | string |
1072
+ | `options` | `"low,medium,high"` | string |
1073
+ | `value` | `""` | string |
1074
+
1075
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1076
+
1077
+ ## `UiRenderScaleSelect` — `incanto`
1078
+
1079
+ | Prop | Default | Kind |
1080
+ |---|---|---|
1081
+ | `anchor` | `"topLeft"` | one of: `topLeft` `top` `topRight` `left` `center` `right` `bottomLeft` `bottom` `bottomRight` |
1082
+ | `visible` | `true` | boolean |
1083
+ | `focusable` | `true` | boolean |
1084
+ | `draggable` | `false` | boolean |
1085
+ | `dropTarget` | `false` | boolean |
1086
+ | `label` | `"Resolution"` | string |
1087
+ | `options` | `"0.5,0.75,1"` | string |
1088
+ | `value` | `""` | string |
1089
+
1090
+ Signals: `dragStarted` · `dragCancelled` · `droppedOn` · `dropped` · `changed`
1091
+
1047
1092
  ## `UiSelect` — `incanto`
1048
1093
 
1049
1094
  | Prop | Default | Kind |
@@ -0,0 +1,212 @@
1
+ ---
2
+ name: incanto-performance
3
+ description: Graphics settings the engine provides so a game does not have to invent them — quality tier (shadows/bloom/post/clouds), frame cap, resolution scale, and the one lever (antialiasing) that can only change at boot. They persist, they apply themselves, and the ones that can change live do. Use when supporting low-end devices or building a settings menu.
4
+ ---
5
+
6
+ # incanto — performance settings (quality tier, frame cap, resolution)
7
+
8
+ The settings a player expects to find in a graphics menu, provided by the engine so
9
+ your game does not have to invent them: **quality tier**, **frame cap**, **resolution
10
+ scale**. They persist, they apply themselves, and the ones that can change live do.
11
+
12
+ You do not implement any of this. You decide whether to expose it.
13
+
14
+ ---
15
+
16
+ ## The short version
17
+
18
+ ```ts
19
+ const game = await createGame3D({ canvas, scene });
20
+
21
+ // createGame3D has ALREADY done all of this:
22
+ // - picked a starting tier from the device, on a first visit only
23
+ // - read the saved tier at boot for antialiasing (see "boot vs live" below)
24
+ // - bound the tier, the frame cap and the resolution scale to the live game
25
+
26
+ game.engine.settings.set('quality', 'low'); // shadows/bloom/post drop THIS FRAME
27
+ game.engine.settings.set('maxFps', 30); // the loop slows down, and stays cool
28
+ game.engine.settings.set('renderScale', 0.7); // fewer pixels, same world
29
+ ```
30
+
31
+ Every one of those is written to the save store and comes back on the next launch.
32
+
33
+ ---
34
+
35
+ ## The four levers, and what each actually costs
36
+
37
+ | setting | values | costs | changes live |
38
+ | --- | --- | --- | --- |
39
+ | `quality` | `'low'` `'medium'` `'high'` | shadows, bloom, post, clouds | **yes** |
40
+ | `maxFps` | `0` (uncapped) or fps | nothing visual — battery and heat | **yes** |
41
+ | `renderScale` | `0.25`–`2` | image softness only | **yes** |
42
+ | antialiasing | follows the tier | jagged edges at `low` | **no — boot only** |
43
+
44
+ ### quality
45
+
46
+ `low` turns off shadows, bloom, post and clouds. `medium` keeps shadows but makes them
47
+ **static** (they render once instead of every frame — the measured 42% of a shadow's
48
+ cost is re-rendering it) and drops post. `high` is everything.
49
+
50
+ The tier is applied as an environment patch, so a scene that never had bloom does not
51
+ gain any at `high` — the tier can only take things away or restore what the scene asked
52
+ for.
53
+
54
+ ### maxFps
55
+
56
+ Gates the **whole frame**, not just the render: an uncapped loop on a 144 Hz phone runs
57
+ game logic 144 times a second that nobody will ever see. `0` means "as fast as the
58
+ display goes" and is the default — a cap nobody asked for is a downgrade.
59
+
60
+ `dt` still comes from the clock, so a capped game is not a slow game. Physics is fixed-
61
+ step and unaffected.
62
+
63
+ ### renderScale
64
+
65
+ The cheapest lever on a weak GPU. At `0.66` the fragment shaders run over **44%** of the
66
+ pixels; the image goes soft and nothing else changes. Compare that with dropping a
67
+ quality tier, which costs the shadows outright. On a phone that cannot hold 60, try
68
+ `renderScale` before `quality`.
69
+
70
+ It multiplies with the engine's automatic resolution governor rather than fighting it,
71
+ so a player who already scaled down does not get scaled down twice.
72
+
73
+ ---
74
+
75
+ ## Boot vs live — the one thing that cannot change while running
76
+
77
+ `antialias` is a **WebGL context attribute**. three fixes it when the context is
78
+ created, and there is no API anywhere that changes it afterwards. So:
79
+
80
+ - the tier's antialiasing is read **at boot** from the saved setting
81
+ - changing `quality` mid-game changes shadows/bloom/post immediately, and the
82
+ antialiasing on the **next launch**
83
+
84
+ If your settings menu wants to be honest about that, say "applies on restart" next to
85
+ antialiasing only. Everything else is immediate.
86
+
87
+ A scene's own `environment.rendering` and an explicit `pixelRatio` option both still win
88
+ over the tier — an author who pinned a value meant it.
89
+
90
+ ---
91
+
92
+ ## Detection runs once, and a choice is permanent
93
+
94
+ ```ts
95
+ engine.settings.autoQuality(); // first visit only — picks from the device
96
+ engine.settings.chooseQuality('high'); // a HUMAN chose; never auto-detect again
97
+ ```
98
+
99
+ `createGame3D` calls `autoQuality()` for you. Call `chooseQuality(tier)` — not
100
+ `set('quality', tier)` — from a settings menu, so re-detection on the next launch does
101
+ not silently undo it. (A separate `qualityChosen` flag is needed because a chosen tier
102
+ that happens to equal the detected one is otherwise indistinguishable from no choice.)
103
+
104
+ Device hints used: `hardwareConcurrency`, `deviceMemory`, and whether the pointer is
105
+ coarse. They are hints; a player's own choice always wins.
106
+
107
+ ---
108
+
109
+ ## The menu itself is three nodes
110
+
111
+ You do not build a graphics menu. You place it.
112
+
113
+ ```json
114
+ { "name": "Graphics", "type": "UiPanel", "props": { "anchor": "center" },
115
+ "children": [
116
+ { "name": "Quality", "type": "UiQualitySelect" },
117
+ { "name": "FrameCap", "type": "UiFrameCapSelect" },
118
+ { "name": "Resolution", "type": "UiRenderScaleSelect" }
119
+ ] }
120
+ ```
121
+
122
+ No script, no signal connections, no `Settings` knowledge. Each node reads the
123
+ setting it owns, writes it when the player picks, and follows a change made
124
+ anywhere else — so two menus (a title screen and a pause screen) can never
125
+ disagree.
126
+
127
+ | node | writes | reads as |
128
+ | --- | --- | --- |
129
+ | `UiQualitySelect` | `chooseQuality()` | Low / Medium / High |
130
+ | `UiFrameCapSelect` | `maxFps` + `engine.maxFps` | 30 / 60 / 120 / Unlimited |
131
+ | `UiRenderScaleSelect` | `renderScale` | 50% / 75% / 100% |
132
+
133
+ `UiQualitySelect` goes through **`chooseQuality`**, not `set('quality')`, so the
134
+ next launch's device detection cannot undo what a person picked.
135
+
136
+ Restrict or extend the choices with `options` — `"60,120,0"` on a game that is
137
+ unplayable at 30, `"0.5,0.75,1,1.5"` for supersampling on a strong GPU:
138
+
139
+ ```json
140
+ { "name": "FrameCap", "type": "UiFrameCapSelect", "props": { "options": "60,120,0" } }
141
+ ```
142
+
143
+ ### Localizing the labels
144
+
145
+ The option text is English unless the scene declares a string for it — a menu
146
+ reading `settings.quality.high` is worse than one reading `High`, so an
147
+ undeclared key falls back to English rather than showing the key:
148
+
149
+ ```json
150
+ "strings": {
151
+ "ko": {
152
+ "settings.quality.low": "낮음",
153
+ "settings.quality.medium": "보통",
154
+ "settings.quality.high": "높음",
155
+ "settings.frameCap.unlimited": "제한 없음"
156
+ }
157
+ }
158
+ ```
159
+
160
+ The `label` prop takes `@t:` like every other widget:
161
+ `{ "label": "@t:settings.quality" }`.
162
+
163
+ ---
164
+
165
+ ## Wiring your own menu
166
+
167
+ Each setting has a `bind*` that applies the saved value immediately and again on every
168
+ change, and returns an unsubscribe:
169
+
170
+ ```ts
171
+ const off = engine.settings.bindFrameCap((fps) => { engine.maxFps = fps; });
172
+ engine.settings.bindRenderScale((s) => renderer.setRenderScale(s));
173
+ engine.settings.bindQuality((patch) => setEnvironment3D(engine, patch));
174
+ ```
175
+
176
+ `createGame3D` already installs all three. You only reach for these when you build your
177
+ own game loop.
178
+
179
+ To read the current values for your UI:
180
+
181
+ ```ts
182
+ engine.settings.get('quality'); // 'low' | 'medium' | 'high'
183
+ engine.settings.get('maxFps'); // 0 = uncapped
184
+ engine.settings.get('renderScale'); // 1 = native
185
+ ```
186
+
187
+ ---
188
+
189
+ ## What NOT to do
190
+
191
+ - **Do not build your own frame limiter** with `setTimeout` around your update. The
192
+ engine's cap is deadline-based; a naive `if (now - last < interval) return` snaps a
193
+ 45 fps request to 30 on a 60 Hz display, which is worse than not capping.
194
+ - **Do not lower the quality tier to fix a slow scene you can fix properly.** The tier
195
+ is the player's lever. If your game is slow at `high` on a normal machine, run
196
+ `bunx incanto-check` and look at the triangle and draw-call counts first.
197
+ - **Do not offer antialiasing as a live toggle.** It cannot be one. See above.
198
+ - **Do not read `devicePixelRatio` yourself.** The renderer already clamps it to 2 and
199
+ scales it by the tier and the governor.
200
+
201
+ ---
202
+
203
+ ## Verifying
204
+
205
+ ```ts
206
+ const { fps, triangles, drawCalls } = game.stats();
207
+ ```
208
+
209
+ Set `maxFps` to 30 and confirm `fps` settles near 30 — if it settles near 20, the game
210
+ is slower than the cap and the cap is not what is limiting it.
211
+
212
+ See `incanto-verifying-your-game.md` for the full loop.
@@ -14,7 +14,7 @@
14
14
  "@dimforge/rapier2d-compat": "0.19.3",
15
15
  "@dimforge/rapier3d-compat": "0.19.3",
16
16
  "@pixiv/three-vrm": "^3.5.3",
17
- "incanto": "^0.37.0",
17
+ "incanto": "^0.38.0",
18
18
  "three": "^0.184.0"
19
19
  },
20
20
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.37.0",
16
+ "incanto": "^0.38.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {
@@ -13,7 +13,7 @@
13
13
  "@dimforge/rapier2d-compat": "0.19.3",
14
14
  "@dimforge/rapier3d-compat": "0.19.3",
15
15
  "@pixiv/three-vrm": "^3.5.3",
16
- "incanto": "^0.37.0",
16
+ "incanto": "^0.38.0",
17
17
  "three": "^0.184.0"
18
18
  },
19
19
  "devDependencies": {