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.
- package/bin/incanto-multiplay.mjs +52 -4
- package/dist/2d.d.ts +83 -7
- package/dist/2d.js +4 -3
- package/dist/3d.d.ts +861 -167
- package/dist/3d.js +6 -6
- package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
- package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
- package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
- package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
- package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
- package/dist/editor.js +1600 -1529
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
- package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
- package/dist/gameplay.d.ts +573 -31
- package/dist/gameplay.js +3 -2
- package/dist/index.d.ts +150 -6
- package/dist/index.js +6 -6
- package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +2 -2
- package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
- package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
- package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
- package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
- package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
- package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
- package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
- package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
- package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
- package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
- package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
- package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
- package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
- package/dist/test.d.ts +90 -12
- package/dist/test.js +2 -2
- package/dist/vite.js +34 -5
- package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
- package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
- package/editor/assets/index-Db74LZY-.js +11579 -0
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +1639 -280
- package/skills/incanto-3d-character.md +383 -20
- package/skills/incanto-audio.md +24 -2
- package/skills/incanto-building-2d-games.md +21 -2
- package/skills/incanto-building-3d-games.md +38 -3
- package/skills/incanto-editor.md +29 -8
- package/skills/incanto-environment.md +257 -14
- package/skills/incanto-game-feel.md +60 -1
- package/skills/incanto-gameplay-behaviors.md +668 -20
- package/skills/incanto-hud.md +116 -0
- package/skills/incanto-multiplayer.md +141 -2
- package/skills/incanto-node-reference.md +398 -16
- package/skills/incanto-performance.md +32 -0
- package/skills/incanto-physics-and-input.md +241 -13
- package/skills/incanto-playtesting.md +2 -2
- package/skills/incanto-save-slots.md +61 -2
- package/skills/incanto-scene-json-authoring.md +13 -0
- package/skills/incanto-verifying-your-game.md +86 -3
- package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
- package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
- package/templates-app/molehill-2d/package.json +1 -1
- package/templates-app/molehill-2d/src/game.scene.json +129 -2
- package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
- package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +40 -26
- package/templates-app/platformer-2d/src/game.scene.json +144 -2
- package/templates-app/platformer-2d/verify.ts +18 -1
- package/templates-app/star-survivor/PROJECT/Status.md +7 -0
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/behaviors.ts +9 -8
- package/templates-app/star-survivor/src/game.scene.json +142 -4
- package/templates-app/star-survivor/verify.ts +13 -0
- package/templates-app/tps-3d/PROJECT/Status.md +6 -0
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +71 -10
- package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
- package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
- package/templates-app/village-quest-3d/src/village.scene.json +126 -2
- package/templates-app/village-quest-3d/verify.ts +52 -2
- package/editor/assets/index-CrUCQoaB.js +0 -11046
package/skills/incanto-hud.md
CHANGED
|
@@ -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).
|
|
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.
|