incanto 0.74.0 → 0.76.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -179,6 +179,22 @@ reveals the line then advances; choice lines render buttons and wait for
179
179
  `choose(i)`. Buttons opt into pointer events — the rest of the HUD stays
180
180
  click-through.
181
181
 
182
+ **A line that moves on by itself** — `autoAdvanceSeconds` (default `0` = wait
183
+ for the click) keeps a fully-shown line up that long and then advances it. A
184
+ cutscene has nobody clicking: the camera is on a rail and the controller is
185
+ off. A line with choices always waits.
186
+
187
+ ```json
188
+ { "name": "Talk", "type": "UiDialogue", "props": { "autoAdvanceSeconds": 2.6 } }
189
+ ```
190
+
191
+ **Cinematic bars** are a property of the LAYER, not two panels: `HudLayer`'s
192
+ `letterbox` (0..0.5) is the fraction of the layer's height each bar covers,
193
+ drawn full-bleed at the top and bottom edges over every widget, and animated
194
+ by whatever writes it (`hud.letterbox = 0.11` on a cutscene's first frame,
195
+ `0` on its last). Two `UiPanel`s sat in their anchor slots — inside the layer's
196
+ padding, above the dialogue box — and never reached the edge.
197
+
182
198
  **Driving them with no screen** — a HUD widget is DOM, so a headless harness
183
199
  cannot click one. Both take the gesture directly:
184
200
 
@@ -279,6 +295,67 @@ or moving a widget to another corner all work from a behavior — and from the
279
295
  editor's inspector, which is the same code path. They used to be baked in when
280
296
  the widget was built and could never be changed afterwards.
281
297
 
298
+ ## A minimap (UiMinimap)
299
+
300
+ Every open world, every stealth game, every wave shooter wants one, and the
301
+ HUD had text, bars, banners, images and buttons — no map:
302
+
303
+ ```json
304
+ { "name": "Map", "type": "UiMinimap",
305
+ "props": { "anchor": "topRight", "size": 160, "radius": 40,
306
+ "dots": { "enemy": "#ff5050", "pickup": "#ffd54f", "goal": "#3aa0ff" },
307
+ "heading": "%Skin" } }
308
+ ```
309
+
310
+ | prop | default | what it does |
311
+ |---|---|---|
312
+ | `size` | `160` | diameter, px |
313
+ | `radius` | `40` | world units from the centre to the rim (m in 3D, px in 2D) |
314
+ | `follow` | `""` | the node at the centre; `""` = the first node in the `player` group |
315
+ | `dots` | `{}` | group → colour: every node in a listed group is a dot (not `groups` — that is the node's own membership) |
316
+ | `heading` | `""` | a node whose yaw turns the map so its "ahead" is up (+Z-forward — the skin the controller turns); `""` = north (−z) up |
317
+ | `self` / `dotSize` / `background` / `shape` | `#ffffff` / `6` / dark / `circle` | the look |
318
+
319
+ - The plane is x/z in 3D and x/y in 2D; things beyond `radius` are not drawn;
320
+ a group you do not list is not drawn. It follows moving parents (an enemy
321
+ under a spawner's group node is where it is drawn).
322
+ - **`markers()` is the same list headless** — `{x, y, color, group, node}`
323
+ in canvas px — so a harness can ask what the map shows (`examples/survivor-3d`
324
+ asserts the husks that reached you are on it), and `centre()` is the node
325
+ it is following. Draws on a `<canvas>` in the browser; nothing in a test.
326
+ - A dot's colour that is not a string is a load error: the keys of `dots`
327
+ are your group names (any is right), the values must be colours.
328
+
329
+ ## A waypoint — the objective marker (UiWaypoint)
330
+
331
+ Every open world puts a marker over the objective; every example here told
332
+ the player where to go in a sentence at the top of the screen. One widget:
333
+
334
+ ```json
335
+ { "name": "Mark", "type": "UiWaypoint",
336
+ "props": { "target": "/root/Shop1", "label": "bakery", "hideWithin": 4, "arriveWithin": 3 } }
337
+ ```
338
+
339
+ | prop | default | what it does |
340
+ |---|---|---|
341
+ | `target` | `''` | the node the marker floats over — change it from a behavior as the objective moves on |
342
+ | `label` / `color` / `size` | `''` / `#ffd54f` / `22` | the text under the diamond, its colour, its size in px |
343
+ | `distance` | `true` | show the metres left under it |
344
+ | `clamp` / `margin` | `true` / `40` | off screen or behind the camera, slide to a rectangle `margin` px in from the edge and turn the arrow that way (behind: mirrored through the centre first, so it points the way to turn) |
345
+ | `hideWithin` | `0` | hide when you are this close (m); 0 = never |
346
+ | `arriveWithin` | `0` | `arrived` fires once when `from` comes this close, and again after leaving and coming back; 0 = never |
347
+ | `from` | `''` | who the distance is measured from — the first node in the `player` group by default |
348
+
349
+ - It floats over its target through the renderer's own projection
350
+ (`engine.toScreen`), so `anchor` means nothing here and it never sits in a
351
+ HUD column.
352
+ - **`screen()` is the same answer headless** — `{x, y, angle, behind,
353
+ onScreen, clamped, distance, visible}` in canvas px — from whatever
354
+ `toScreen` a test installs (`engine.toScreen = (w) => ({ x, y, behind })`),
355
+ so a harness can ask where the marker is and which way it points
356
+ (`examples/errands-3d` asks that the marker moves from shop to shop);
357
+ `distanceNow()` is the metres alone, projection or not.
358
+
282
359
  ## Playable on a controller (focus navigation)
283
360
 
284
361
  A menu you can only click is not playable on a gamepad, and "add controller
@@ -310,6 +387,12 @@ What activation MEANS is per widget: a button presses, a toggle flips (left/righ
310
387
  sets it explicitly, which reads better on a pad), a select walks its list, a
311
388
  slider nudges by one `step`.
312
389
 
390
+ **A `disabled` widget is out of the ring** — a title screen's CONTINUE before
391
+ there is a save is the ordinary case, and stopping on it gave a pad player a
392
+ button where `A` does nothing and nothing on screen said why. Enable it and it
393
+ is back in the ring on the same frame; `hud.focus()` will not move onto one
394
+ either.
395
+
313
396
  **A `GameFlow` pause panel arms it for you** — that screen IS a menu, it knows
314
397
  exactly when it opens and closes, and it puts the layer's own value back on
315
398
  resume. Every game with a pause menu was otherwise writing the same two lines,
@@ -381,6 +464,39 @@ the pointer path — a non-`draggable` source does nothing, a target that is not
381
464
 
382
465
 
383
466
 
467
+ ## The shell every shipped game has
468
+
469
+ A title screen, a pause menu, options that persist, a minimap and an objective
470
+ marker are all HudLayer widgets in scene JSON — `examples/shell-3d` composes the
471
+ lot in one game, and it is the shortest thing to copy:
472
+
473
+ - **One panel per screen** (`Title`, `PauseMenu`, `Options`), and ONE place in
474
+ your behaviour that writes their `visible`. A screen two places can show is a
475
+ screen that gets stuck.
476
+ - **`GameFlow` owns every screen**, and none of it is TypeScript: `pauseAction`
477
+ toggles the pause and shows `pausePanelPath`, `titlePanelPath` holds the world
478
+ at boot, `screen(path)` pushes options over whichever menu asked and `back()`
479
+ returns to it. Wire the buttons with `connections` — `pressed → resume`,
480
+ `pressed → screen ["…/Options"]`, `pressed → back`, `pressed → restart` — and
481
+ author every panel that is not up at boot `"visible": false`. See "The whole
482
+ shell, without a script" in `incanto-gameplay-behaviors.md`.
483
+ - **The options are the engine's own widgets** — `UiVolumeSlider`,
484
+ `UiMuteToggle`, `UiQualitySelect`, `UiFrameCapSelect`, `UiRenderScaleSelect`
485
+ read and write `engine.settings` and persist themselves. Anything you ADD
486
+ (look sensitivity, difficulty) is two lines: set the widget's `value` from
487
+ `settings` at ready, and `settings.set` on `changed`.
488
+ - **`focusNavigation` goes ON for a menu and OFF for play**, or the arrow keys
489
+ belong to the menu while the player is trying to walk.
490
+ - **`UiWaypoint.screen()`** answers where the marker is — `onScreen`, `behind`,
491
+ `clamped` — and a headless run can ask it, so "does my objective marker stay
492
+ on screen when the objective is behind me" is a check and not a hope.
493
+ - **Give the start button a name a machine can read** — PLAY, START, RESUME.
494
+ A title screen holds the world at `timeScale` 0, and `incanto-feel` and the
495
+ facing check press their way past it before measuring (`HudLayer.focusables()`
496
+ in that order, then everything else); a button they cannot start reports
497
+ `the clock never ran` instead of numbers. `incanto-playtest`'s bot presses
498
+ widgets on its own.
499
+
384
500
  ## More than one language
385
501
 
386
502
  Any text prop here can name a translation key instead of a literal:
@@ -45,6 +45,9 @@ Authority rules:
45
45
  > `incanto-multiplay` prints its verdict instead of exiting 13 with nothing.
46
46
  > Take one lock, do the whole read-modify-write inside it, and release.
47
47
  - physics/AI run client-side; one-simulator needs (NPC waves) use the host-client pattern
48
+ - a SHARED physics object (a ball, a crate, a door) → **owner-handoff**: one
49
+ client simulates it at a time and the server decides which — the recipe is
50
+ "A shared physics object" below, and `examples/soccer-mp-3d` is the composition
48
51
 
49
52
  ## Scene JSON
50
53
 
@@ -95,7 +98,13 @@ a half-finished edit that behaves exactly like a broken one.
95
98
  after two seconds, by name. `source: "users"` spawns one
96
99
  instance of the registered scene per OTHER account (self skipped); the flat `sync` patch
97
100
  applies onto the spawned scene's root; `position` lerps when `interpolate: true`
98
- (remote entities render slightly in the past — that's correct). Emits
101
+ (remote entities render slightly in the past — that's correct). **A remote is
102
+ made from its FIRST patch, where that patch says it is** — not at the join,
103
+ at the origin, sliding to its place: the kernel lists an account the moment it
104
+ joins with nothing in it, and an instance built then stood on the kick-off
105
+ spot for a few frames (with a body on the remote scene it kicked the ball
106
+ into the player beside it). So a remote scene with a `CharacterBody3D` root is
107
+ fine, and is how the ball bounces off the OTHER player in `soccer-mp-3d`. Emits
99
108
  `spawned(node, key)` / `despawned(node, key)`. `source: "collection:<id>"` mirrors a
100
109
  room collection by `__id` — a collection ENTITY applies as your server wrote it
101
110
  (`addCollectionItem('coins', { position: [x, y] })` lands on the spawned node;
@@ -344,6 +353,109 @@ Runnable reference: `examples/arena-preview` (Coin Dash — server-authoritative
344
353
  `$lock`'d coin claim + `$roomTick` match clock, two clients split-screen). Going
345
354
  live is the one-line transport swap below.
346
355
 
356
+ ## A shared physics object (a ball two players kick)
357
+
358
+ The replication above is per PLAYER: one owner node, its own state. A 1v1
359
+ soccer game has one thing both players act on — the ball — and a ball is a
360
+ physics body no data-authoritative server can integrate at 60 Hz (`$roomTick`
361
+ runs every 100–1000 ms; a ball at 10 m/s moves a metre or ten between ticks).
362
+ The shape that works, measured in `examples/soccer-mp-3d`:
363
+
364
+ - **Every client runs its own Rapier ball.** It bounces off boards and off
365
+ the other player's body locally; nothing is a ghost.
366
+ - **One client at a time is the ball's OWNER**, and the server says which
367
+ (`room.ballOwner`). The owner publishes position + velocity at the owner
368
+ throttle (a custom server function that DROPS a publish from anyone else —
369
+ that is the authority) into a room collection entity; the others MIRROR it:
370
+ set the local ball's velocity to the published one plus a position-error
371
+ gain (`6/s`), so it chases the truth while still colliding, and teleport
372
+ when the gap is wide (`2.5 m`).
373
+ - **Touching the ball takes it over** (`claimBall`, `$lock`'d) — the kick
374
+ applies its impulse to the local ball AT ONCE and the claim makes that
375
+ simulation the room's; a player WALKING at the ball (the move stick, not a
376
+ shove) claims it too, which is what makes dribbling possible. A player the
377
+ ball hits does not claim it, so a shot off a standing opponent bounces as the
378
+ shooter's simulation says, and two players standing on the ball do not hand
379
+ it back and forth every window.
380
+ - **A `seq` on the entity says "snap, do not chase".** The server bumps it on
381
+ every reset and every handoff, and writes the entity itself when it puts the
382
+ ball on the spot — every client snaps to a new seq, except the client that
383
+ wrote it.
384
+ - **The server is the referee** in `$roomTick`: sides, the clock, a GOAL read
385
+ from the replicated ball (it is caught in the net, so a slow tick cannot miss
386
+ it), the reset, and who kicks off. Clients paint the score from room state
387
+ and never call a goal.
388
+
389
+ What it is NOT: fair under latency. The kicker's client is right by
390
+ definition, so a laggy opponent's tackle lands late — the "casual sync" the
391
+ model is built for, stated on purpose. Two things the engine changed for it:
392
+ an impulse now composes with a velocity written in the same frame (the
393
+ mirror's write used to erase the kick a step later), and a remote spawns
394
+ where its first patch says it is.
395
+
396
+ ## An online race: one physics body PER owner
397
+
398
+ The ball above is one body two players fight over. A race is the other shape —
399
+ a body EACH, nothing to hand off — and `examples/race-mp-3d` ("Apex Duel") is
400
+ the composition:
401
+
402
+ ```json
403
+ { "name": "Car", "type": "RigidBody3D",
404
+ "props": { "mass": 900, "linearDamping": 0.3, "angularDamping": 1.5,
405
+ "collider": { "shape": "box", "size": [1.9, 0.7, 4.2] } },
406
+ "groups": ["car", "player"],
407
+ "network": { "mode": "owner",
408
+ "sync": ["position", "rotation", "Wheel0.rotation", "Wheel1.rotation"],
409
+ "throttleMs": 50 },
410
+ "script": { "name": "Racer" },
411
+ "children": [
412
+ { "name": "Drive", "type": "Vehicle3D",
413
+ "props": { "enabled": false, "engineForce": 4200, "wheels": [
414
+ { "position": [-0.85, -0.15, 1.45], "radius": 0.38, "steer": true },
415
+ { "position": [ 0.85, -0.15, 1.45], "radius": 0.38, "steer": true },
416
+ { "position": [-0.85, -0.15, -1.45], "radius": 0.38, "drive": true },
417
+ { "position": [ 0.85, -0.15, -1.45], "radius": 0.38, "drive": true } ] } }
418
+ ] }
419
+ ```
420
+
421
+ - **The other car is a kinematic box**, not a vehicle: `remote-car.scene.json`
422
+ is a `CharacterBody3D` root with the same meshes and `Wheel<i>` nodes (so the
423
+ synced `Wheel0.rotation` path resolves on the copy). The spawner poses it
424
+ from the wire — position interpolated, rotation applied outright — and the
425
+ local car BUMPS it, because a kinematic body pushes a dynamic one.
426
+ - **The lights hold the car, not the input.** `Vehicle3D.enabled` is false
427
+ until the room says `race`; parked, the car still settles on its springs and
428
+ still reads its actions, so green is a real launch. Offline (no
429
+ NetworkManager) the behaviour enables it at once.
430
+ - **The server counts gates, IN ORDER.** `passGate(i)` is `$lock`'d and
431
+ accepted only for the gate that car is due (`next[account]`); gate 0 after
432
+ the last one is a lap; the lap that reaches `LAPS` sets `phase: 'done'` and
433
+ `winner`. The client never counts online — it reads `laps`/`next` off the
434
+ room state and turns a change into its `gate`/`lap` signals. A cut across
435
+ the infield therefore counts nothing, on the server, where cutting is decided.
436
+ - **Gates are `Area3D` sensors driven THROUGH** (the wheel rays ignore them),
437
+ routed by `connections` with `"filter": { "group": "player" }` so the remote
438
+ car — group `remote`, never `player` — cannot trip a gate on this client.
439
+ - **Who is ahead** is laps, then gates this lap, then distance to the next
440
+ gate — computable on every client from the room state plus the two car
441
+ positions, so the HUD's `1st`/`2nd` needs no server field.
442
+ - **No frame before the manager.** `createGame3D` starts the loop at once —
443
+ and ticks two warm-up frames before it even returns — while
444
+ `NetworkManager.create` takes a moment to join; every frame in between runs
445
+ the game OFFLINE. The race's `Racer` grids itself to slot 0 when it finds no
446
+ manager (the right thing for `incanto verify` and the editor's ▶ play), and
447
+ in the browser that offline pose went out as the client's FIRST replicated
448
+ state, on top of the other player's car, which the kinematic copy then lifted
449
+ off the ground. Boot with `autoStart: false` and call `game.engine.start()`
450
+ once the manager exists (`examples/race-mp-3d/src/main.ts`):
451
+
452
+ ```ts
453
+ const game = await createGame3D({ canvas, scene, behaviors, autoStart: false });
454
+ const manager = await NetworkManager.create(game.engine, { transport });
455
+ manager.registerScene('remote-car', remoteCarJson);
456
+ game.engine.start(); // the first frame is a networked one
457
+ ```
458
+
347
459
  ## Going live on agent8 (gameserver-sdk v2)
348
460
 
349
461
  The agent8 platform exposes NOTHING client-callable by default — every function
@@ -451,7 +563,14 @@ kernel with no remote functions, so `roomState` came back `{}` instead of
451
563
  changed nothing — a ✓ that could not have been a ✗.
452
564
 
453
565
  If your server cannot be loaded the run REFUSES rather than quietly measuring a
454
- game without its own rules.
566
+ game without its own rules. It is looked for one and two directories above the
567
+ scene (`src/game.scene.json` and `src/scenes/game.scene.json` both put the
568
+ project root there) and in the working directory; the report's first line
569
+ names the one it loaded — `· server: …/server/src/server.ts (Server)` — or
570
+ says `no server loaded`, and the `--json` report carries `server` and
571
+ `serverNote`. Before this it looked two levels up only, found nothing for a
572
+ scene at `src/`, said nothing, and ran every online example's `agrees` rung on
573
+ the bare kernel with an empty room state.
455
574
 
456
575
  **Pass `--behaviors src/behaviors.ts` too.** A sync key is very often written by
457
576
  game TypeScript (`player.firing = input.isPressed('fire')`), and without the
@@ -472,3 +591,23 @@ every client agreed perfectly about a value nobody ever sent.
472
591
  cannot see them — a bullet has no account — so a spawner that took a hundred
473
592
  entities off the wire and materialised none of them used to be entirely silent.
474
593
  Now it says which spawner, and how many entities it dropped.
594
+
595
+ **A body still moving is allowed one send window.** The rung quiesces for
596
+ sixty frames — the driver's held inputs are dropped first — and then compares
597
+ the owner's truth with what the copies last RECEIVED. A character stops; a car
598
+ does not (released, a raycast vehicle coasts at constant speed), so its copies
599
+ are honestly `speed × (throttleMs + the server's turn)` behind. That much slack
600
+ is granted on `position` and `rotation` keys, scaled by the owner's own
601
+ `linearVelocity` / `angularVelocity` — a dead sync still fails, its gap grows
602
+ with the match — and the report names who was still moving:
603
+
604
+ ```
605
+ · p1, p2 still moving when compared — a copy may trail by one send window, and that is not a disagreement
606
+ ```
607
+
608
+ `rotation` is compared as an ORIENTATION (the angle between the two), because
609
+ Euler XYZ writes one attitude two ways near the gimbal lock and wraps at ±180°
610
+ — `[177, 88, -177]` and `[-180, 88, 180]` are a degree apart, not 357°. And
611
+ `incanto multiplay` drives a physics owner (one with a `linearVelocity`) along
612
+ its own forward at 3 m/s rather than teleporting it — a car thrown through the
613
+ kerbs every frame is the instrument's doing, not the game's.