incanto 0.32.0 → 0.33.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 (49) hide show
  1. package/dist/2d.d.ts +59 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +8 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-D_jMpFh8.d.ts → behavior-CKwTCjfR.d.ts} +140 -1
  6. package/dist/{create-game-bKHgHcsZ.js → create-game-D2QU5x7S.js} +13 -6
  7. package/dist/{create-game-gXI7PYl0.js → create-game-sFuTLqjD.js} +11 -5
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/{duplicate-KRPtUtzl.js → duplicate-BgnG1Lqz.js} +1 -1
  10. package/dist/editor.js +35 -0
  11. package/dist/{environment-presets--DigHNg4.js → environment-presets-CQtEGogB.js} +2 -2
  12. package/dist/{gameplay-BftxM_It.js → gameplay-BpQCbABv.js} +90 -2
  13. package/dist/gameplay.d.ts +1 -1
  14. package/dist/gameplay.js +1 -1
  15. package/dist/index.d.ts +125 -23
  16. package/dist/index.js +5 -58
  17. package/dist/{loader-BlaRQGaA.js → loader-Buk8Bu1h.js} +53 -0
  18. package/dist/{loader-BYBrqTxP.d.ts → loader-COn5fS0o.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{pathfinding-BwhqPD3i.d.ts → pathfinding-DUw9mir9.d.ts} +1 -1
  22. package/dist/{physics-2d-D9wquBvK.js → physics-2d-DjXR5DMu.js} +12 -2
  23. package/dist/{physics-3d-CnPygVGo.js → physics-3d-DF8npb1O.js} +55 -9
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Dzkd6-os.js → register-CscIzJEO.js} +551 -13
  27. package/dist/{register-CUY284Is.js → register-CtI-itec.js} +2 -2
  28. package/dist/{register-tkR_8tWg.js → register-FIJtNbub.js} +145 -8
  29. package/dist/{test-WwRIlXsK.js → test-BRxLd2jH.js} +10 -10
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +1 -1
  32. package/dist/vite.js +1 -1
  33. package/editor/assets/{agent8-C5k1nTCH.js → agent8-BdDP3xKW.js} +1 -1
  34. package/editor/assets/{debug-BT_0mjk-.js → debug-DbjTyTlC.js} +1 -1
  35. package/editor/assets/{index-gfyrByWw.js → index-BWCudoz1.js} +90 -90
  36. package/editor/index.html +1 -1
  37. package/package.json +1 -1
  38. package/schemas/scene.schema.json +520 -0
  39. package/skills/incanto-audio.md +43 -0
  40. package/skills/incanto-building-2d-games.md +36 -0
  41. package/skills/incanto-hud.md +41 -0
  42. package/skills/incanto-node-reference.md +89 -0
  43. package/skills/incanto-physics-and-input.md +50 -0
  44. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +40 -19
  45. package/templates-app/beacon-isle-3d/package.json +1 -1
  46. package/templates-app/tps-3d/docs/project-3d-rules.md +41 -19
  47. package/templates-app/tps-3d/package.json +1 -1
  48. package/templates-app/village-quest-3d/docs/project-3d-rules.md +40 -19
  49. 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-gfyrByWw.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-BWCudoz1.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.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Vibe-coding-first web game engine SDK — JSON-driven scenes on three.js",
5
5
  "keywords": [
6
6
  "game-engine",
@@ -322,9 +322,24 @@
322
322
  {
323
323
  "$ref": "#/$defs/UiDialogue"
324
324
  },
325
+ {
326
+ "$ref": "#/$defs/UiImage"
327
+ },
328
+ {
329
+ "$ref": "#/$defs/UiPanel"
330
+ },
331
+ {
332
+ "$ref": "#/$defs/UiSelect"
333
+ },
334
+ {
335
+ "$ref": "#/$defs/UiSlider"
336
+ },
325
337
  {
326
338
  "$ref": "#/$defs/UiText"
327
339
  },
340
+ {
341
+ "$ref": "#/$defs/UiToggle"
342
+ },
328
343
  {
329
344
  "$ref": "#/$defs/VoxelGrid3D"
330
345
  },
@@ -1798,6 +1813,7 @@
1798
1813
  },
1799
1814
  "CharacterController2D": {
1800
1815
  "type": "object",
1816
+ "x-signals": ["movementStateChanged"],
1801
1817
  "properties": {
1802
1818
  "name": {
1803
1819
  "type": "string"
@@ -1840,6 +1856,47 @@
1840
1856
  "jumpAction": {
1841
1857
  "type": "string",
1842
1858
  "default": "jump"
1859
+ },
1860
+ "coyoteSeconds": {
1861
+ "type": "number",
1862
+ "default": 0
1863
+ },
1864
+ "jumpBufferSeconds": {
1865
+ "type": "number",
1866
+ "default": 0
1867
+ },
1868
+ "jumpCutMultiplier": {
1869
+ "type": "number",
1870
+ "default": 1
1871
+ },
1872
+ "maxJumps": {
1873
+ "type": "number",
1874
+ "default": 1
1875
+ },
1876
+ "dashSpeed": {
1877
+ "type": "number",
1878
+ "default": 0
1879
+ },
1880
+ "dashSeconds": {
1881
+ "type": "number",
1882
+ "default": 0.15
1883
+ },
1884
+ "dashAction": {
1885
+ "type": "string",
1886
+ "default": "dash"
1887
+ },
1888
+ "wallSlideSpeed": {
1889
+ "type": "number",
1890
+ "default": 0
1891
+ },
1892
+ "wallJumpImpulse": {
1893
+ "type": "array",
1894
+ "items": {
1895
+ "type": "number"
1896
+ },
1897
+ "minItems": 2,
1898
+ "maxItems": 2,
1899
+ "default": [0, 0]
1843
1900
  }
1844
1901
  },
1845
1902
  "additionalProperties": false
@@ -6383,6 +6440,390 @@
6383
6440
  },
6384
6441
  "required": ["name", "type"]
6385
6442
  },
6443
+ "UiImage": {
6444
+ "type": "object",
6445
+ "properties": {
6446
+ "name": {
6447
+ "type": "string"
6448
+ },
6449
+ "uid": {
6450
+ "type": "string"
6451
+ },
6452
+ "type": {
6453
+ "const": "UiImage"
6454
+ },
6455
+ "groups": {
6456
+ "type": "array",
6457
+ "items": {
6458
+ "type": "string"
6459
+ }
6460
+ },
6461
+ "tags": {
6462
+ "type": "object"
6463
+ },
6464
+ "props": {
6465
+ "type": "object",
6466
+ "properties": {
6467
+ "anchor": {
6468
+ "type": "string",
6469
+ "enum": [
6470
+ "topLeft",
6471
+ "top",
6472
+ "topRight",
6473
+ "left",
6474
+ "center",
6475
+ "right",
6476
+ "bottomLeft",
6477
+ "bottom",
6478
+ "bottomRight"
6479
+ ],
6480
+ "default": "topLeft"
6481
+ },
6482
+ "visible": {
6483
+ "type": "boolean",
6484
+ "default": true
6485
+ },
6486
+ "src": {
6487
+ "type": "string",
6488
+ "default": ""
6489
+ },
6490
+ "width": {
6491
+ "type": "number",
6492
+ "default": 48
6493
+ },
6494
+ "height": {
6495
+ "type": "number",
6496
+ "default": 48
6497
+ },
6498
+ "fit": {
6499
+ "type": "string",
6500
+ "enum": ["contain", "cover", "fill"],
6501
+ "default": "contain"
6502
+ },
6503
+ "tint": {
6504
+ "type": "string",
6505
+ "default": ""
6506
+ },
6507
+ "opacity": {
6508
+ "type": "number",
6509
+ "default": 1
6510
+ }
6511
+ },
6512
+ "additionalProperties": false
6513
+ },
6514
+ "script": {
6515
+ "type": "object",
6516
+ "properties": {
6517
+ "name": {
6518
+ "type": "string"
6519
+ },
6520
+ "props": {
6521
+ "type": "object"
6522
+ }
6523
+ },
6524
+ "required": ["name"]
6525
+ },
6526
+ "network": {
6527
+ "type": "object"
6528
+ },
6529
+ "children": {
6530
+ "type": "array",
6531
+ "items": {
6532
+ "$ref": "#/$defs/node"
6533
+ }
6534
+ }
6535
+ },
6536
+ "required": ["name", "type"]
6537
+ },
6538
+ "UiPanel": {
6539
+ "type": "object",
6540
+ "properties": {
6541
+ "name": {
6542
+ "type": "string"
6543
+ },
6544
+ "uid": {
6545
+ "type": "string"
6546
+ },
6547
+ "type": {
6548
+ "const": "UiPanel"
6549
+ },
6550
+ "groups": {
6551
+ "type": "array",
6552
+ "items": {
6553
+ "type": "string"
6554
+ }
6555
+ },
6556
+ "tags": {
6557
+ "type": "object"
6558
+ },
6559
+ "props": {
6560
+ "type": "object",
6561
+ "properties": {
6562
+ "anchor": {
6563
+ "type": "string",
6564
+ "enum": [
6565
+ "topLeft",
6566
+ "top",
6567
+ "topRight",
6568
+ "left",
6569
+ "center",
6570
+ "right",
6571
+ "bottomLeft",
6572
+ "bottom",
6573
+ "bottomRight"
6574
+ ],
6575
+ "default": "topLeft"
6576
+ },
6577
+ "visible": {
6578
+ "type": "boolean",
6579
+ "default": true
6580
+ },
6581
+ "layout": {
6582
+ "type": "string",
6583
+ "enum": ["column", "row", "grid"],
6584
+ "default": "column"
6585
+ },
6586
+ "columns": {
6587
+ "type": "number",
6588
+ "default": 4
6589
+ },
6590
+ "gap": {
6591
+ "type": "number",
6592
+ "default": 8
6593
+ },
6594
+ "padding": {
6595
+ "type": "number",
6596
+ "default": 12
6597
+ },
6598
+ "background": {
6599
+ "type": "string",
6600
+ "default": "rgba(0,0,0,0.55)"
6601
+ },
6602
+ "radius": {
6603
+ "type": "number",
6604
+ "default": 10
6605
+ },
6606
+ "width": {
6607
+ "type": "number",
6608
+ "default": 0
6609
+ },
6610
+ "height": {
6611
+ "type": "number",
6612
+ "default": 0
6613
+ },
6614
+ "border": {
6615
+ "type": "string",
6616
+ "default": ""
6617
+ }
6618
+ },
6619
+ "additionalProperties": false
6620
+ },
6621
+ "script": {
6622
+ "type": "object",
6623
+ "properties": {
6624
+ "name": {
6625
+ "type": "string"
6626
+ },
6627
+ "props": {
6628
+ "type": "object"
6629
+ }
6630
+ },
6631
+ "required": ["name"]
6632
+ },
6633
+ "network": {
6634
+ "type": "object"
6635
+ },
6636
+ "children": {
6637
+ "type": "array",
6638
+ "items": {
6639
+ "$ref": "#/$defs/node"
6640
+ }
6641
+ }
6642
+ },
6643
+ "required": ["name", "type"]
6644
+ },
6645
+ "UiSelect": {
6646
+ "type": "object",
6647
+ "x-signals": ["changed"],
6648
+ "properties": {
6649
+ "name": {
6650
+ "type": "string"
6651
+ },
6652
+ "uid": {
6653
+ "type": "string"
6654
+ },
6655
+ "type": {
6656
+ "const": "UiSelect"
6657
+ },
6658
+ "groups": {
6659
+ "type": "array",
6660
+ "items": {
6661
+ "type": "string"
6662
+ }
6663
+ },
6664
+ "tags": {
6665
+ "type": "object"
6666
+ },
6667
+ "props": {
6668
+ "type": "object",
6669
+ "properties": {
6670
+ "anchor": {
6671
+ "type": "string",
6672
+ "enum": [
6673
+ "topLeft",
6674
+ "top",
6675
+ "topRight",
6676
+ "left",
6677
+ "center",
6678
+ "right",
6679
+ "bottomLeft",
6680
+ "bottom",
6681
+ "bottomRight"
6682
+ ],
6683
+ "default": "topLeft"
6684
+ },
6685
+ "visible": {
6686
+ "type": "boolean",
6687
+ "default": true
6688
+ },
6689
+ "label": {
6690
+ "type": "string",
6691
+ "default": ""
6692
+ },
6693
+ "options": {
6694
+ "type": "string",
6695
+ "default": ""
6696
+ },
6697
+ "value": {
6698
+ "type": "string",
6699
+ "default": ""
6700
+ }
6701
+ },
6702
+ "additionalProperties": false
6703
+ },
6704
+ "script": {
6705
+ "type": "object",
6706
+ "properties": {
6707
+ "name": {
6708
+ "type": "string"
6709
+ },
6710
+ "props": {
6711
+ "type": "object"
6712
+ }
6713
+ },
6714
+ "required": ["name"]
6715
+ },
6716
+ "network": {
6717
+ "type": "object"
6718
+ },
6719
+ "children": {
6720
+ "type": "array",
6721
+ "items": {
6722
+ "$ref": "#/$defs/node"
6723
+ }
6724
+ }
6725
+ },
6726
+ "required": ["name", "type"]
6727
+ },
6728
+ "UiSlider": {
6729
+ "type": "object",
6730
+ "x-signals": ["changed"],
6731
+ "properties": {
6732
+ "name": {
6733
+ "type": "string"
6734
+ },
6735
+ "uid": {
6736
+ "type": "string"
6737
+ },
6738
+ "type": {
6739
+ "const": "UiSlider"
6740
+ },
6741
+ "groups": {
6742
+ "type": "array",
6743
+ "items": {
6744
+ "type": "string"
6745
+ }
6746
+ },
6747
+ "tags": {
6748
+ "type": "object"
6749
+ },
6750
+ "props": {
6751
+ "type": "object",
6752
+ "properties": {
6753
+ "anchor": {
6754
+ "type": "string",
6755
+ "enum": [
6756
+ "topLeft",
6757
+ "top",
6758
+ "topRight",
6759
+ "left",
6760
+ "center",
6761
+ "right",
6762
+ "bottomLeft",
6763
+ "bottom",
6764
+ "bottomRight"
6765
+ ],
6766
+ "default": "topLeft"
6767
+ },
6768
+ "visible": {
6769
+ "type": "boolean",
6770
+ "default": true
6771
+ },
6772
+ "label": {
6773
+ "type": "string",
6774
+ "default": ""
6775
+ },
6776
+ "value": {
6777
+ "type": "number",
6778
+ "default": 0.5
6779
+ },
6780
+ "min": {
6781
+ "type": "number",
6782
+ "default": 0
6783
+ },
6784
+ "max": {
6785
+ "type": "number",
6786
+ "default": 1
6787
+ },
6788
+ "step": {
6789
+ "type": "number",
6790
+ "default": 0.01
6791
+ },
6792
+ "width": {
6793
+ "type": "number",
6794
+ "default": 180
6795
+ },
6796
+ "color": {
6797
+ "type": "string",
6798
+ "default": "#6ee7dc"
6799
+ }
6800
+ },
6801
+ "additionalProperties": false
6802
+ },
6803
+ "script": {
6804
+ "type": "object",
6805
+ "properties": {
6806
+ "name": {
6807
+ "type": "string"
6808
+ },
6809
+ "props": {
6810
+ "type": "object"
6811
+ }
6812
+ },
6813
+ "required": ["name"]
6814
+ },
6815
+ "network": {
6816
+ "type": "object"
6817
+ },
6818
+ "children": {
6819
+ "type": "array",
6820
+ "items": {
6821
+ "$ref": "#/$defs/node"
6822
+ }
6823
+ }
6824
+ },
6825
+ "required": ["name", "type"]
6826
+ },
6386
6827
  "UiText": {
6387
6828
  "type": "object",
6388
6829
  "properties": {
@@ -6469,6 +6910,85 @@
6469
6910
  },
6470
6911
  "required": ["name", "type"]
6471
6912
  },
6913
+ "UiToggle": {
6914
+ "type": "object",
6915
+ "x-signals": ["changed"],
6916
+ "properties": {
6917
+ "name": {
6918
+ "type": "string"
6919
+ },
6920
+ "uid": {
6921
+ "type": "string"
6922
+ },
6923
+ "type": {
6924
+ "const": "UiToggle"
6925
+ },
6926
+ "groups": {
6927
+ "type": "array",
6928
+ "items": {
6929
+ "type": "string"
6930
+ }
6931
+ },
6932
+ "tags": {
6933
+ "type": "object"
6934
+ },
6935
+ "props": {
6936
+ "type": "object",
6937
+ "properties": {
6938
+ "anchor": {
6939
+ "type": "string",
6940
+ "enum": [
6941
+ "topLeft",
6942
+ "top",
6943
+ "topRight",
6944
+ "left",
6945
+ "center",
6946
+ "right",
6947
+ "bottomLeft",
6948
+ "bottom",
6949
+ "bottomRight"
6950
+ ],
6951
+ "default": "topLeft"
6952
+ },
6953
+ "visible": {
6954
+ "type": "boolean",
6955
+ "default": true
6956
+ },
6957
+ "label": {
6958
+ "type": "string",
6959
+ "default": ""
6960
+ },
6961
+ "value": {
6962
+ "type": "boolean",
6963
+ "default": false
6964
+ }
6965
+ },
6966
+ "additionalProperties": false
6967
+ },
6968
+ "script": {
6969
+ "type": "object",
6970
+ "properties": {
6971
+ "name": {
6972
+ "type": "string"
6973
+ },
6974
+ "props": {
6975
+ "type": "object"
6976
+ }
6977
+ },
6978
+ "required": ["name"]
6979
+ },
6980
+ "network": {
6981
+ "type": "object"
6982
+ },
6983
+ "children": {
6984
+ "type": "array",
6985
+ "items": {
6986
+ "$ref": "#/$defs/node"
6987
+ }
6988
+ }
6989
+ },
6990
+ "required": ["name", "type"]
6991
+ },
6472
6992
  "VoxelGrid3D": {
6473
6993
  "type": "object",
6474
6994
  "x-signals": ["blocksChanged"],
@@ -330,3 +330,46 @@ this.tree.engine.music.crossfadeTo('incanto/assets/audio/boss.mp3', 3);
330
330
  - **Global volume / mute / settings** → `engine.audio.master/sfx/music/muted`.
331
331
  - **Verifying headlessly** → audio is a no-op in the VM; assert `play()` doesn't
332
332
  throw and check your gameplay/score logic instead (see incanto-verifying).
333
+
334
+ ## Settings that survive a reload (`engine.settings`)
335
+
336
+ Every player's volume used to reset on every page load: `createSaveStore` was
337
+ good and had zero callers, and eight examples set `engine.audio.music = 0.5` at
338
+ boot without reading a saved value.
339
+
340
+ `createGame2D/3D` now binds them, so **a slider that writes `engine.audio.music`
341
+ has already saved it** — no extra call, which is the promise this skill was
342
+ already making.
343
+
344
+ ```ts
345
+ engine.settings.get('quality'); // 'low' | 'medium' | 'high'
346
+ engine.settings.set('sensitivity', 1.4);
347
+ engine.settings.all(); // the whole options screen at once
348
+ engine.settings.reset();
349
+ engine.settings.changed.connect((k) => …); // k is the key that changed
350
+ ```
351
+
352
+ Stored keys: `master`, `sfx`, `music`, `muted`, `quality`, `sensitivity`,
353
+ `invertY`, `reduceMotion`. Setting a value that has not changed does NOT emit,
354
+ and a corrupt/hand-edited `quality` falls back to the default instead of
355
+ bricking the game.
356
+
357
+ ### Quality tiers
358
+
359
+ ```ts
360
+ import { qualityEnvironment, readDeviceHints, suggestQuality, setEnvironment3D } from 'incanto';
361
+
362
+ const tier = engine.settings.get('quality') ?? suggestQuality(readDeviceHints());
363
+ setEnvironment3D(engine, qualityEnvironment(tier));
364
+ ```
365
+
366
+ `qualityEnvironment(tier)` is an **environment patch**, not a second rendering
367
+ pipeline: `setEnvironment3D` already applies one live and validated. `low` turns
368
+ off shadows/bloom/post/clouds and pins pixelRatio to 1; `medium` keeps shadows
369
+ but makes them static (measured ~42% of a dense-forest frame); `high` is
370
+ everything at device pixel ratio.
371
+
372
+ `suggestQuality(readDeviceHints())` picks a STARTING point from cores, memory and
373
+ a coarse pointer — the options menu is the real answer, and a `UiSelect` bound to
374
+ `quality` is the whole options row (see `incanto-hud.md`).
375
+
@@ -293,3 +293,39 @@ behind it), what lights the scene, and which colliders intersect. Then read
293
293
  `stats().errors` (something threw and got skipped) and `assetErrors()` (a model
294
294
  404'd). All four are silent failures otherwise — the screen just looks wrong,
295
295
  or empty, and nothing throws.
296
+
297
+ ## Platformer game feel (CharacterController2D)
298
+
299
+ A jump that only fires while `isOnFloor()` is true feels BROKEN, and players do
300
+ not report it as "the coyote time is missing" — they report the game as
301
+ unresponsive. These are the constants `examples/platformer-2d` hand-rolls in a
302
+ 541-line behavior, as props. **Every one defaults to off**, so an existing scene
303
+ behaves exactly as before.
304
+
305
+ | prop | default | what it buys |
306
+ | --- | --- | --- |
307
+ | `coyoteSeconds` | `0` | jump for this long AFTER walking off a ledge — the single biggest one |
308
+ | `jumpBufferSeconds` | `0` | a press just BEFORE landing still fires on touchdown |
309
+ | `jumpCutMultiplier` | `1` | release early → rise less (variable jump height) |
310
+ | `maxJumps` | `1` | `2` = double jump. Walking off a ledge spends the ground jump |
311
+ | `dashSpeed` / `dashSeconds` / `dashAction` | `0` / `0.15` / `dash` | a straight-line dash |
312
+ | `wallSlideSpeed` | `0` | capped fall while against a wall |
313
+ | `wallJumpImpulse` | `[0, 0]` | `[away, up]` push off a wall |
314
+
315
+ ```json
316
+ { "name": "Controller", "type": "CharacterController2D",
317
+ "props": { "coyoteSeconds": 0.12, "jumpBufferSeconds": 0.15,
318
+ "jumpCutMultiplier": 0.45, "maxJumps": 2,
319
+ "wallSlideSpeed": 60, "wallJumpImpulse": [260, 380] } }
320
+ ```
321
+
322
+ **`movementStateChanged(state)`** fires on every change — `idle` · `run` ·
323
+ `jump` · `fall` · `wallSlide` · `dash` — so an `AnimatedSprite2D` follows the
324
+ character from scene JSON instead of a behavior polling velocities. The 3D
325
+ sibling has had this signal since it shipped.
326
+
327
+ Wall moves need to know there IS a wall: `CharacterBody2D` now answers
328
+ `isOnWall()`, `wallSide()` (-1 left / +1 right) and `isOnCeiling()` as well as
329
+ `isOnFloor()`. Before, the floor was the only surface a character could report,
330
+ which is why a wall jump was not awkward to write but impossible.
331
+