@genex-ai/cli-demo 1.38.0 → 1.38.1-dev.1097

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 (25) hide show
  1. package/dist/{blender-mcp-QVEGHE5L.js → blender-mcp-WSURPZET.js} +2 -2
  2. package/dist/{blender-serve-66OHC4ML.js → blender-serve-EH2MOW3U.js} +1 -1
  3. package/dist/{chunk-4BBTPRJW.js → chunk-EABIVAGT.js} +2 -2
  4. package/dist/{chunk-QI7FIYBY.js → chunk-K4EHXOZK.js} +1 -1
  5. package/dist/index.js +1700 -811
  6. package/package.json +1 -1
  7. package/templates/skills/genex-authoritative-multiplayer/SKILL.md +208 -0
  8. package/templates/skills/genex-authoritative-multiplayer/references/module-api.md +172 -0
  9. package/templates/skills/genex-authoritative-multiplayer/references/physics.md +150 -0
  10. package/templates/skills/genex-authoritative-multiplayer/references/presets.md +394 -0
  11. package/templates/skills/genex-authoritative-multiplayer/references/projectiles.md +195 -0
  12. package/templates/skills/genex-authoritative-multiplayer/references/room-template.md +1189 -0
  13. package/templates/skills/genex-authoritative-multiplayer/references/settings.md +122 -0
  14. package/templates/skills/genex-authoritative-multiplayer/references/testing.md +118 -0
  15. package/templates/skills/genex-game-director/SKILL.md +2 -1
  16. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  17. package/templates/skills/genex-llm-in-games/SKILL.md +209 -72
  18. package/templates/skills/genex-llm-in-games/references/pricing.md +126 -85
  19. package/templates/skills/genex-monetization/SKILL.md +2 -11
  20. package/templates/skills/genex-threejs-embed-auth/SKILL.md +4 -1
  21. package/templates/skills/genex-threejs-embed-auth/references/shells.md +104 -0
  22. package/templates/skills/genex-threejs-multiplayer/SKILL.md +4 -0
  23. package/templates/skills/genex-tool-llm/SKILL.md +11 -9
  24. package/templates/skills/genex-tool-publish/SKILL.md +7 -0
  25. package/templates/skills/genex-updates/SKILL.md +1 -1
@@ -0,0 +1,394 @@
1
+ # Movement presets and prediction
2
+
3
+ A **preset** is movement the server simulates and every client replays identically, so a player's
4
+ own body moves the instant they press a key and the server's correction, when it comes, is
5
+ invisible. Name one in the module's `config.preset` and `connectAuthority` predicts with it on its
6
+ own; there is nothing to wire on the client but the input (and, for two of them, the world).
7
+
8
+ Presets are append-only by name. There are three:
9
+
10
+ | The game | Preset | Ask for |
11
+ |---|---|---|
12
+ | Seen from above: a twin-stick arena, a top-down shooter, a board game with moving pieces | `topdown` | `tickHz: 30` |
13
+ | A character on foot through a level: a first-person or over-the-shoulder shooter, with jumping, ducking, stairs and ramps | `firstperson` | `tickHz: 60` |
14
+ | Cars: car football, a derby, an arena brawl, a track of flats and ramps | `vehicle` | `tickHz: 60` |
15
+ | Anything else: flying, swimming, climbing, a double jump, a boat, a hovercraft | your own step ([below](#movement-of-your-own)) | |
16
+
17
+ Pick a preset whenever one fits: each is ported to every engine and held to the same conformance
18
+ vectors, so prediction matches on every client. `firstperson` and `vehicle` need `@genex-ai/multiplayer`
19
+ 0.17.4 or newer.
20
+
21
+ ## `topdown`
22
+
23
+ Planar movement on the server's x/y plane, with an aim angle.
24
+
25
+ | Parameter | Range | Default |
26
+ |---|---|---|
27
+ | `speed` | 0 to 1000 world units per second | 6 |
28
+ | `arena` | above 0, up to 1e6: bodies stay within [-arena, arena] on x and y | 20 |
29
+
30
+ ```ts
31
+ config: { tickHz: 30, preset: { name: "topdown", params: { speed: 6, arena: 20 } } }
32
+ ```
33
+
34
+ Each input moves the player's first owned entity: `moveX` and `moveY` are the stick (a diagonal
35
+ is scaled back to length 1), `yaw` is the aim in radians with 0 along +x, and the body is kept
36
+ inside the arena. The server applies it before `onInput` sees the input, so `onInput` reads the
37
+ moved body. An unknown or out-of-range parameter is refused when the module loads.
38
+
39
+ In three.js, draw the server's `x` as `x` and its `y` as `z`, and turn the body by `-yaw` around
40
+ three's y axis.
41
+
42
+ The reference implementation is exported from `@genex-ai/multiplayer/authority` as `stepTopdown`
43
+ (with `resolveTopdownParams`, `TOPDOWN_DEFAULTS` and `PRESET_MAX_DT_S`); the session already runs it
44
+ for you.
45
+
46
+ ## `firstperson`
47
+
48
+ Source-engine-style movement: friction, ground and air acceleration (air-strafing
49
+ works), a jump, ducking and crouch-jumping, a free step up ledges, staying on the ground down slopes
50
+ and stairs, and a hull that slides along what it hits. The body is an upright box whose feet are at
51
+ `(x, y, z)`, moving through a fixed world of boxes and ramps ([the collision world](#the-collision-world-firstperson-and-vehicle)).
52
+
53
+ ```js
54
+ export default defineRoom({
55
+ contract: 1,
56
+ config: { tickHz: 60, maxPlayers: 10, preset: { name: "firstperson", params: {} }, lagComp: { buttons: FIRE } },
57
+ world: WORLD, // the same object the client hands its session
58
+ // init, onJoin, onLeave, onInput, step …
59
+ });
60
+ ```
61
+
62
+ **Units.** The defaults are the Source engine's, in Source units of about an inch: a hull 32 wide
63
+ and 72 tall (54 ducked), a run of 250 units/s, gravity 800. The easy way to a game in metres is to
64
+ keep these on the server and scale the drawing (`0.0254` metres a unit). A game that sets its own
65
+ units sets every distance, speed and acceleration below, `skin` and `groundSnap` included.
66
+
67
+ | Parameter | Default | Range | What it does |
68
+ |---|---|---|---|
69
+ | `maxSpeed` | 250 | 0 to 10000 | Run speed, units/s, before the body's `cap` |
70
+ | `walkSpeed` | 0.52 | 0 to 1 | Speed multiplier while walk is held |
71
+ | `crouchSpeed` | 0.34 | 0 to 1 | Speed multiplier while ducking (wins over walk) |
72
+ | `accelerate` | 5.5 | 0 to 1000 | Ground acceleration |
73
+ | `airAccelerate` | 12 | 0 to 1000 | Air acceleration |
74
+ | `airMaxWishSpeed` | 30 | 0 to 10000 | Air wish speed cap: what makes air-strafing work |
75
+ | `friction` | 5.2 | 0 to 1000 | Ground friction |
76
+ | `stopSpeed` | 80 | 0 to 10000 | Below this speed friction acts as if at this speed |
77
+ | `gravity` | 800 | 0 to 100000 | Units/s² along −z |
78
+ | `jumpImpulse` | 301.993 | 0 to 100000 | The upward speed a jump sets |
79
+ | `maxVelocity` | 3500 | 0 to 100000 | Each velocity axis is held within ±this |
80
+ | `radius` | 16 | above 0, to 10000 | Half the side of the hull's square footprint |
81
+ | `standHeight` | 72 | above 0, to 10000 | Hull height standing |
82
+ | `crouchHeight` | 54 | above 0, to 10000 | Hull height fully ducked |
83
+ | `duckTime` | 0.4 | above 0, to 100 | Seconds to duck fully, and to stand back up |
84
+ | `stepHeight` | 18 | 0 to 10000 | Highest ledge walked up without a jump |
85
+ | `standable` | 0.7 | 0 to 1 | Least upward share of a ramp's normal that can be stood on (0.7 is about 45°) |
86
+ | `groundTrace` | 2.5 | 0 to 10000 | How far below the feet ground is found |
87
+ | `groundSnap` | 1 | 0 to 10000 | How far above the feet ground is found |
88
+ | `skin` | 0.03 | 0 to 100 | Clearance kept from a surface |
89
+ | `pitchLimit` | 1.5533430576324463 | 0 to π/2 | Largest look up or down, radians (89°) |
90
+ | `jumpBit`, `duckBit`, `walkBit` | 1, 2, 4 | integer below 2^24 | Which `buttons` bits jump, duck and walk; 0 turns one off |
91
+ | `substeps` | 2 | integer 1 to 8 | Sub-steps per input ([the CPU budget](#sub-steps-and-the-cpu-budget)) |
92
+ | `world` | the empty world's hash | | Written by the sandbox from the module's `world`: leave it out |
93
+
94
+ An unknown parameter, or one out of its range, is refused when the module loads.
95
+
96
+ **Input.** `moveX` and `moveY` are the direction the player wants to move **in the world's axes**,
97
+ not the camera's: turn the keys by the view's yaw before sending them (the step itself has no
98
+ trigonometry). `yaw` and `pitch` are the view's aim, stored on the body for drawing and for the
99
+ module's shots. The preset's bits are on `buttons`; give the game's own buttons (fire, reload,
100
+ switch weapon) other bits, 8 and up.
101
+
102
+ ```ts
103
+ const forward = (keys.has("KeyW") ? 1 : 0) - (keys.has("KeyS") ? 1 : 0);
104
+ const right = (keys.has("KeyD") ? 1 : 0) - (keys.has("KeyA") ? 1 : 0);
105
+ session.input.moveX = forward * Math.cos(viewYaw) + right * Math.sin(viewYaw);
106
+ session.input.moveY = forward * Math.sin(viewYaw) - right * Math.cos(viewYaw);
107
+ session.input.yaw = viewYaw; // +yaw turns left: the mouse moving right is viewYaw -= dx * sensitivity
108
+ session.input.pitch = viewPitch; // positive looks up; the step holds it within ±pitchLimit
109
+ session.input.buttons = (jump ? 1 : 0) | (duck ? 2 : 0) | (walk ? 4 : 0) | (firing ? FIRE : 0);
110
+ ```
111
+
112
+ A jump fires once per press: holding the key does not hop again on landing.
113
+
114
+ **The body.** On top of `x y z vx vy vz yaw`, the preset writes `pitch` (the aim), `duck` (0 standing
115
+ to 1 fully ducked) and `mstate` (bits: 1 on the ground, 2 ducking, 4 jump held), and reads `cap`: a
116
+ speed multiplier from 0 to 4 (default 1) that the module writes, for a weapon's weight or a wound. It
117
+ limits how fast the body can go, never how hard it pushes, so a slowed player still overcomes
118
+ friction. A `cap` written in `onInput` applies from the next input. The eye height is the game's:
119
+ The Source engine's is `64 - 18 * duck` over the feet. A respawn writes the new spot with
120
+ `vx: 0, vy: 0, vz: 0, duck: 0, mstate: 0` and bumps `epoch`.
121
+
122
+ **What it does not do.** Players pass through each other: the preset collides only with the world.
123
+ The world is fixed for the room's life, so a door, a lift or a crate that moves is the module's own
124
+ rule, not a solid. And `ctx.rewind.hitscan` does not know the world either: test the shot against
125
+ the same boxes yourself (a ray against each box) and refuse a hit behind a wall. The shot leaves
126
+ from the eye along `(cos(pitch) * cos(yaw), cos(pitch) * sin(yaw), sin(pitch))`.
127
+
128
+ **Tick rate.** Ask for `tickHz: 60`. Air-strafing turns the wish direction once per input, so the
129
+ speed a strafe gains grows with the input rate: at 30 Hz a perfect strafer gains less than half of
130
+ what they gain at 60 (sub-steps do not change it). A jump pressed on an input whose last sub-step
131
+ lands is lost, as on a Source server's tick.
132
+
133
+ ## `vehicle`
134
+
135
+ An arcade car on Rocket League's ground and air handling, kept level (it never pitches, rolls or
136
+ flips): throttle, braking and coasting; boost from a fuel tank; grip that turns the velocity with the
137
+ heading, and a drift button that lets the tail slide; a turn rate that grows with speed and mirrors
138
+ in reverse; a jump, and steering in the air. It drives the same hull through the same world as
139
+ `firstperson`.
140
+
141
+ ```js
142
+ export default defineRoom({
143
+ contract: 1,
144
+ config: { tickHz: 60, maxPlayers: 4, preset: { name: "vehicle", params: { radius: 50, height: 36 } } },
145
+ world: WORLD, // the same object the client hands its session
146
+ // init, onJoin, onLeave, onInput, step …
147
+ });
148
+ ```
149
+
150
+ **Units.** About a centimetre, Rocket League's own scale: a car about 120 long, 1410 units/s on the
151
+ throttle and 2300 with boost, gravity 650. The hull does not turn with the heading: its square
152
+ footprint, 100 across by default, stands in for a car about 118 long and 84 wide. A game in other
153
+ units sets every distance, speed and acceleration, `skin` included.
154
+
155
+ | Parameter | Default | Range | What it does |
156
+ |---|---|---|---|
157
+ | `maxSpeed` | 1410 | 0 to 100000 | Top speed on the throttle, units/s, before `cap` |
158
+ | `accel` | 1000 | 0 to 100000 | Throttle acceleration, units/s² |
159
+ | `brake` | 3500 | 0 to 100000 | Deceleration while the throttle opposes the motion |
160
+ | `coast` | 525 | 0 to 100000 | Deceleration with no throttle, and back down to the throttle's top speed after a boost |
161
+ | `boostAccel` | 1000 | 0 to 100000 | Boost's push along the heading, on top of the throttle's |
162
+ | `boostMaxSpeed` | 2300 | 0 to 100000 | Top speed under boost, before `cap` |
163
+ | `boostBurn` | 0.3333333432674408 | 0 to 1000 | Fuel burned per second of boost: a full tank (1) lasts 3 s |
164
+ | `grip` | 6000 | 0 to 100000 | How fast sideways speed is lost on the ground, units/s² |
165
+ | `driftGrip` | 1500 | 0 to 100000 | The same, while drift is held |
166
+ | `turnRate` | 2.2 | 0 to 100 | Turn rate at full steer on the ground, rad/s, at or above `turnSpeed` |
167
+ | `turnSpeed` | 400 | above 0, to 100000 | The forward speed where the full turn rate is reached; below it the rate scales with speed |
168
+ | `spinAccel` | 30 | 0 to 10000 | How fast the turn rate follows the steering on the ground, rad/s² |
169
+ | `airTurnRate` | 5.5 | 0 to 100 | Turn rate at full steer in the air, rad/s |
170
+ | `airSpinAccel` | 9 | 0 to 10000 | How fast the turn rate follows the steering in the air, rad/s² |
171
+ | `gravity` | 650 | 0 to 100000 | Units/s² along −z |
172
+ | `jumpImpulse` | 450 | 0 to 100000 | The upward speed a jump sets |
173
+ | `maxVelocity` | 6000 | 0 to 100000 | Each velocity axis is held within ±this |
174
+ | `radius` | 50 | above 0, to 10000 | Half the side of the hull's square footprint |
175
+ | `height` | 36 | above 0, to 10000 | Hull height |
176
+ | `stepHeight` | 8 | 0 to 10000 | Highest kerb driven up without a jump, and the deepest drop a sub-step follows down |
177
+ | `standable` | 0.7 | 0 to 1 | Least upward share of a ramp's normal that can be driven on (0.7 is about 45°) |
178
+ | `groundTrace` | 4 | 0 to 10000 | How far below the hull ground is found |
179
+ | `groundSnap` | 2 | 0 to 10000 | How far above the hull's base ground is found |
180
+ | `skin` | 0.03 | 0 to 100 | Clearance kept from a surface |
181
+ | `jumpBit`, `boostBit`, `driftBit` | 1, 2, 4 | integer below 2^24 | Which `buttons` bits jump, boost and drift; 0 turns one off |
182
+ | `substeps` | 2 | integer 1 to 8 | Sub-steps per input ([the CPU budget](#sub-steps-and-the-cpu-budget)) |
183
+ | `world` | the empty world's hash | | Written by the sandbox from the module's `world`: leave it out |
184
+
185
+ **Input.** The heading is the car's own state, not input: `yaw`, `pitch` and `moveZ` are unused.
186
+
187
+ ```ts
188
+ session.input.moveY = (keys.has("KeyW") ? 1 : 0) - (keys.has("KeyS") ? 1 : 0); // throttle: below 0 brakes, then reverses
189
+ session.input.moveX = (keys.has("KeyD") ? 1 : 0) - (keys.has("KeyA") ? 1 : 0); // steer: +1 turns RIGHT
190
+ session.input.buttons = (keys.has("Space") ? 1 : 0) | (keys.has("ShiftLeft") ? 2 : 0) | (keys.has("KeyQ") ? 4 : 0); // jump, boost, drift
191
+ ```
192
+
193
+ `moveX = +1` turns the car right, toward −yaw (with z up, +yaw is counterclockwise seen from
194
+ above): the steer-right key sends +1, like every other right input. A stick sends its x as is.
195
+
196
+ **How it drives.** Boost always drives forward at full throttle, burns a full tank in 3 seconds and
197
+ works in the air along the level heading. The preset only burns fuel: the module gives it (a boost
198
+ pad, a kickoff, a slow refill) by writing `fuel`. A standing car does not turn: the turn rate grows
199
+ with speed up to `turnSpeed`. Walls do not bounce: a car meeting one head-on stops in one sub-step,
200
+ and one scraping along a wall with its nose turned into it slows (past about 10° the throttle can
201
+ no longer hold its speed; parallel to the wall it recovers). A ramp the car can drive on (up to
202
+ about 45° with the default `standable`) is climbed at its flat-ground speed; a steeper one is a
203
+ wall. Downhill the car keeps to the road only while one sub-step's drop stays under `stepHeight`:
204
+ at 60 Hz with 2 sub-steps that is a slope of about 23° at top boost speed, so a steep downhill or a
205
+ crest launches it. Cars pass through each other: a bump or a demolition is the module's own rule,
206
+ and a velocity the module writes into a car shows on its driver's screen as a correction. A car the
207
+ module pushes into a wall, a ramp or the floor is pushed back out on its next step, through the
208
+ solid's nearest face (never its bottom, nor a top another solid sits on, such as a wall's top under
209
+ the ceiling); keep a push under half a wall's thickness plus the car's radius, or that nearest face
210
+ is the far one.
211
+
212
+ **The body.** On top of `x y z vx vy vz yaw` (the heading), the preset writes `spin` (the turn rate,
213
+ rad/s, positive toward +yaw: for wheels and skid marks), `fuel` (the tank, 0 to 1) and `mstate`
214
+ (bits: 1 on the ground, 4 jump held), and reads `cap`, a multiplier of both top speeds from 0 to 4
215
+ (default 1) that the module writes. It never writes `pitch` or `duck`. A respawn or a kickoff writes
216
+ the new spot and heading with `vx: 0, vy: 0, vz: 0, spin: 0, mstate: 0`, the fuel it starts with,
217
+ and bumps `epoch`.
218
+
219
+ **Tick rate.** Ask for `tickHz: 60`. The turn, the grip and the heading's turn of the velocity are
220
+ integrated once per sub-step, so another `tickHz` or `substeps` is a slightly different car (the
221
+ same one on every engine). A jump pressed on an input whose last sub-step lands is lost.
222
+
223
+ **Sine and cosine.** The preset turns the heading with `psin` and `pcos`, a sine and cosine every
224
+ engine computes to the same bits; the platform's `Math.sin` and `Math.cos` differ in the last bit
225
+ between engines and would split prediction from the server. The client can import them from
226
+ `@genex-ai/multiplayer/authority`; a room module cannot import a package, so it rounds `Math.sin` and
227
+ `Math.cos` with `Math.fround` ([module-api.md](module-api.md)) or, like template 3, uses none.
228
+
229
+ ## The collision world (`firstperson` and `vehicle`)
230
+
231
+ Both presets move through one static world of boxes and ramps, z up:
232
+
233
+ ```ts
234
+ // src/world.ts: imported by server/room.js AND by the client, so both have the same world
235
+ export const WORLD = {
236
+ boxes: [
237
+ [-1024, -1024, -64, 1024, 1024, 0], // [minX, minY, minZ, maxX, maxY, maxZ]: the floor
238
+ [-1056, -1056, 0, -1024, 1056, 256], // a wall
239
+ ],
240
+ ramps: [
241
+ [400, -160, 0, 592, 160, 96, 0, 0, 96], // [minX, minY, minZ, maxX, maxY, maxZ, axis, lo, hi]
242
+ ],
243
+ };
244
+ ```
245
+
246
+ - **A ramp** is a box whose top rises along x (`axis` 0) or y (`axis` 1): `lo` at that axis's min
247
+ end, `hi` at its max end, `maxZ` equal to the higher of them and `minZ` at or below both.
248
+ - **Nothing is implicit.** With no floor box a body falls forever: open ground is one box.
249
+ - **Every box is axis-aligned** and every ramp rises along x or y, so a level is built on a grid; a
250
+ diagonal wall is a staircase of boxes.
251
+ - **Every number must be a float32**: integers are always safe, and so are halves and quarters;
252
+ `0.1` is refused. Each min is below its max. At most 4096 boxes and 512 ramps.
253
+ - **The module exports it**: `defineRoom({ …, world: WORLD })`. The sandbox validates it when the
254
+ module loads (a bad world is a bad config: the module does not load) and writes its hash into
255
+ the preset's `params.world`. Leave `params.world` out.
256
+ - **The client hands its session the same world**: `connectAuthority({ …, world: WORLD })`, or
257
+ `session.setWorld(WORLD)` before the first `frame()`. It returns the hash, and throws a
258
+ `WorldError` naming the first bad value when the world is one the room would refuse.
259
+ - **The hash is the gate.** The session predicts your own body only while its world's hash equals
260
+ the room's, so one number changed on one side turns prediction off: your body is then drawn from
261
+ the server, a round trip late, and the console says why once. `session.prediction` reports it:
262
+ `reason` is `world_mismatch`, with both hashes in `world` and `roomWorld`. Keep the world in one
263
+ file both sides import; never rebuild it on the client from a model or a level file.
264
+ - **Size is cheap.** The server and the session both cull with a bounding-volume tree, so a step
265
+ costs the solids near the body, not the size of the map, and a tower of stacked boxes costs no
266
+ more than the same boxes spread out. The results are the same with or without it.
267
+ - **Draw what collides.** Build the visible level from the same boxes and ramps (or art that sits
268
+ on them exactly): a wall drawn a little off is one players stop at in the air. The module's own
269
+ tests (a shot's line of sight, a ball's bounce) use the same `WORLD`.
270
+
271
+ ## Axes and three.js
272
+
273
+ The server's ground is the x/y plane and **+z is up**. `yaw` turns from +x toward +y,
274
+ counterclockwise seen from above (yaw 0 faces +x), and a positive `pitch` looks up. For
275
+ `firstperson` and `vehicle`, draw with three.js's handedness kept:
276
+
277
+ ```ts
278
+ mesh.position.set(e.x, e.z, -e.y); // server (x, y, z) -> three (x, z, -y)
279
+ mesh.rotation.y = e.yaw; // a model that faces +x turns onto the heading
280
+ camera.rotation.set(pitch, yaw - Math.PI / 2, 0, "YXZ"); // a first-person camera at the eye, looking along the aim
281
+ ```
282
+
283
+ Do not use topdown's `(x, z, y)` drawing with these presets. It is a mirror image, which does not
284
+ matter while a game reads its input in the same drawn axes, as the topdown templates do. Here the
285
+ input's right is the server's, and in a mirror image the steer-right key turns the car left on
286
+ screen. Convert only where you draw and where you read input; the world stays in server axes, value
287
+ for value the module's, or its hash differs.
288
+
289
+ ## Running a ball
290
+
291
+ A ball, in car football or any game where bodies knock something about, is **one server projectile
292
+ the module re-launches** whenever its flight changes ([projectiles.md](projectiles.md)): a fixed id
293
+ such as `ball`, launched from `step` with `src` `''`. Every client draws it from its flight at the
294
+ server's present, and nothing moves it in between. Template 3 in [room-template.md](room-template.md)
295
+ is a whole car arena built this way. Every tick, `step` reads it with `ctx.projectiles.at("ball")`
296
+ and re-launches it from where it is when one of these changes it:
297
+
298
+ - **Off the world.** A sphere test against every solid of the same `WORLD` (by each box's closest
299
+ point, and a ramp's closest point too, never its planes, which report a touch near an edge before
300
+ there is one). Moving into a surface with normal `n`, the new velocity is
301
+ `v - (1 + e) * dot(v, n) * n`, with a restitution `e` of about 0.6. Lose tangential speed only on
302
+ a real impact (`-dot(v, n)` over a threshold): a ball sliding down a slope touches it every few
303
+ ticks under gravity, and a loss on each touch glues it to the slope.
304
+ - **Off a car, at the time of impact.** Sweep the ball's flight from the tick before against the
305
+ car's move since the module last saw it (keep each car's position at the end of `step`), relative
306
+ to each other, to their first contact: sample the tick, then halve the step that first touches.
307
+ Tested only where both are at the end of the tick, a hit's direction and speed depend on how deep
308
+ the car got, and at 30 Hz a fast car passes under the ball. At the contact, push the ball out
309
+ along the normal of the sphere against the car's hull (the preset's box), give it the car's
310
+ velocity relative to it along that normal times `1 + e`, plus a share of the car's speed, and
311
+ place it by where the car is now. A minimum hit speed, if you want one, scales with the car's own
312
+ speed into the ball: a fixed one adds energy at every contact with a parked car, so the ball
313
+ bounces forever on its roof or between it and a wall.
314
+ - **Rolling.** A ball resting on the floor is launched with `g: 0` and `vz: 0`, so its flight stays
315
+ on the floor. Every few ticks (6 at 60 Hz) re-launch it with its speed `s` reduced by a share and a
316
+ constant, `max(0, s * (1 - k * N / tickHz) - d * N / tickHz)` for `N` ticks, and at rest once it
317
+ is slow: a share alone decays forever, so a slow ball creeps for seconds.
318
+ - **Goals.** The whole ball past a goal line inside the goal mouth is a `goal` event and a kickoff:
319
+ the ball at rest on the centre spot, every car back on its slot with an `epoch` bump.
320
+ - **Speed.** Cap the ball's speed below its radius per tick (`radius * tickHz`), so one tick's move
321
+ never carries it through a wall thinner than its diameter.
322
+
323
+ Each re-launch is one entity update, far inside a tick's caps. **The limit to say out loud:** every
324
+ client draws the ball where the server has it now, its own car ahead of the server by its input
325
+ lead, and other cars 100 ms behind. So your own touch shows your car reaching the ball a moment
326
+ before the ball reacts, and another car's touch shows the ball leaving before that car visibly gets
327
+ there: both are off by about the one-way latency. Draw the ball from `session.entities()` and leave
328
+ it there; do not try to hide this by moving the ball on the client.
329
+
330
+ ## Sub-steps and the CPU budget
331
+
332
+ `substeps` (default 2) splits each input's step, so at 60 Hz the default runs 120 sub-steps a
333
+ second per body. Each sub-step costs about the same, so a room's preset time is about bodies ×
334
+ sub-steps. Genex's servers have room for a full car room at the most sub-steps. The local server runs
335
+ the module on QuickJS, several times slower: under Genex's limits a full car room (eight cars at
336
+ 60 Hz) fits there at 2 sub-steps and is ended for CPU at 4 or more. **Keep `substeps` at 2 for a
337
+ full car room**; raise it only for fewer players, and only when the REPORT line of
338
+ `npx genex-authority-dev start --module server/room.js --root . --bots 7` keeps `tick.p99Ms` well
339
+ under `tick.budgetMs`. A `firstperson` map at the scale of a competitive shooter map (about 1500 boxes
340
+ and 60 ramps) at 2 sub-steps holds ten players on Genex's servers and about five on QuickJS.
341
+
342
+ The bots and the validation fuzz press button bit 1 most often. On both presets that is jump by
343
+ default, so in tests the bots mostly hop; a game whose main shot should be exercised binds it to
344
+ bit 1 and moves jump (`jumpBit: 8`), or accepts that the fuzz fires its other bits less often.
345
+
346
+ ## Movement of your own
347
+
348
+ A game whose movement is none of these (flying, swimming, a double jump, a dash) leaves `preset` out
349
+ and moves the body itself in `onInput`. Write the step ONCE, in a file both sides import, and hand
350
+ the same function to the session so it predicts exactly what the server will do:
351
+
352
+ ```ts
353
+ // src/sim/step.ts: imported by server/room.ts AND by the client
354
+ import type { InputData } from "@genex-ai/multiplayer/authority/module";
355
+
356
+ export const SPEED = 8;
357
+ export function step(body: { x: number; y: number; yaw: number }, input: InputData, dt: number): void {
358
+ body.x = Math.fround(body.x + input.moveX * SPEED * dt);
359
+ body.y = Math.fround(body.y + input.moveY * SPEED * dt);
360
+ body.yaw = Math.fround(input.yaw);
361
+ }
362
+ ```
363
+
364
+ ```ts
365
+ // server/room.ts: onInput
366
+ onInput(ctx, playerId, input) {
367
+ const me = ctx.entities.ownedBy(playerId);
368
+ if (me) step(me, input, ctx.dt);
369
+ },
370
+ ```
371
+
372
+ ```ts
373
+ // the client
374
+ const session = await connectAuthority({
375
+ urls: getAuthorityUrls(),
376
+ room: GENEX.slug,
377
+ auth: () => getColyseusAuth(),
378
+ predict: { step, fields: ["x", "y", "yaw"] },
379
+ });
380
+ ```
381
+
382
+ `fields` lists what the step writes (any of `x y z vx vy vz yaw pitch duck mstate cap spin fuel`;
383
+ all of them when left out). `predict: false` turns prediction off: the player's own body is then
384
+ drawn from the server's state, a round trip late, which is right only for something that does not
385
+ move under the player's hands (a turret, a card table).
386
+
387
+ Rules for a step of your own:
388
+
389
+ - Pure: the body, the input and `dt` in, the body out. No clock, no random, no state of its own.
390
+ - `Math.fround` every value it stores, as the wire carries float32.
391
+ - `Math.sin`, `Math.cos`, `Math.atan2` and `Math.exp` may differ in the last bit between engines:
392
+ round what they give with `Math.fround` before storing it.
393
+ - The server stays the judge: the step predicts movement; collisions with other players, damage
394
+ and scoring stay in the module and reach the client as state.
@@ -0,0 +1,195 @@
1
+ # Projectiles
2
+
3
+ A projectile is an entity on a fixed ballistic flight: a start point, a velocity and an optional
4
+ gravity. The room module writes the launch **once**; every client works out where the projectile is
5
+ at any instant from that launch. Other players' projectiles are therefore drawn where the server has
6
+ them now, not 100 ms behind like bodies, and your own is drawn from the frame you fire it. A flying
7
+ projectile costs nothing per tick on the wire: only its launch, a re-launch and its removal go out.
8
+
9
+ Projectiles need `@genex-ai/multiplayer` 0.17.4 or newer. If `session.predictProjectile` does not
10
+ exist after `npm i @genex-ai/multiplayer@latest`, the release is not out yet: say so and stop; do
11
+ not move projectile entities by hand every tick instead.
12
+
13
+ A complete room and client to start from: the bolt arena, the second template in
14
+ [room-template.md](room-template.md).
15
+
16
+ ## Projectile or hitscan
17
+
18
+ | Use | When | How it is judged |
19
+ |---|---|---|
20
+ | Hitscan, `ctx.rewind.hitscan` | The shot is instant: rifles, lasers, a sniper. It must land where the crosshair was. | Lag-compensated (with `config.lagComp`): against where the shooter saw the targets. |
21
+ | A projectile, `ctx.projectiles` | Players can see the shot travel and dodge it: bolts, rockets, grenades, arrows, dodgeballs, fireballs. | Against where the targets are on the server now. No lag compensation. |
22
+
23
+ A projectile is fair without lag compensation because the target sees it coming at the server's
24
+ present and can step aside. That also means a shooter must lead a moving target: the bodies they see
25
+ are about 100 ms in the past. Keep projectiles slow enough to see (a few to a few tens of units per
26
+ second); a shot that should feel instant is hitscan.
27
+
28
+ ## The room module
29
+
30
+ ### `ctx.projectiles.launch(init)`
31
+
32
+ ```js
33
+ const id = ctx.projectiles.launch({ kind: "bolt", x, y, z: 0, vx, vy, vz: 0, g: 0 });
34
+ ```
35
+
36
+ Writes a projectile whose flight starts at the tick being stepped and returns its id. `kind`, `x`,
37
+ `y`, `z`, `vx`, `vy` and `vz` are required (finite, at most 1e9 in magnitude); `g` is the downward
38
+ pull along −z in units/s², from 0 to 1000 (default 0, a straight line); `hp`, `flags` and `data` are
39
+ optional and work as on any entity. The server stores every number as float32. `owner` is always
40
+ `''`: a projectile is never a player's predicted body.
41
+
42
+ - **In `onInput`** the projectile belongs to that input: its `src` is the shooter's seat and its id
43
+ is `<seat>.<seq>.<n>`, the firing input's sequence number and the launch's index within that input
44
+ from 0. This is the id the shooter's client predicted it under, so never pass `id` here — it is
45
+ refused (a `TypeError`).
46
+ - **In `step`, `onJoin` or `onLeave`** pass an `id` yourself; `src` is `''` (a server projectile: a
47
+ turret, a trap, a bounce).
48
+ - **Never in `init`**: a launch there throws. `init` steps no tick.
49
+ - **The same id again re-launches it**: the new launch replaces the old one whole, from the tick being
50
+ stepped (pass `hp`, `flags` and `data` again to keep them). That is how a projectile bounces or
51
+ turns. A re-launch from `step` makes it a server projectile (`src` becomes `''`), so keep who
52
+ fired it in `ctx.state` if you score with it.
53
+ - **Change a flight only by re-launching.** Writing `x`…`vz` with `ctx.entities.update` moves the
54
+ start point but keeps the old launch tick, so the projectile jumps. `t0`, `g` and `src` are
55
+ read-only (`ctx.entities.update` ignores them). `hp`, `flags` and `data` update normally.
56
+
57
+ ### `ctx.projectiles.at(id, tick?)`
58
+
59
+ Where the projectile is at `tick` (default: the tick being stepped): `{ x, y, z, vx, vy, vz }`, or
60
+ `undefined` when `id` is not a projectile. Use it for walls, the floor and bounds:
61
+
62
+ ```js
63
+ const at = ctx.projectiles.at(id);
64
+ if (Math.abs(at.x) > ARENA || Math.abs(at.y) > ARENA || at.z < 0) ctx.entities.remove(id);
65
+ ```
66
+
67
+ ### `ctx.projectiles.sweep(id, { kinds, radius })`
68
+
69
+ The entities of those `kinds` whose sphere of `radius` the projectile's path crossed between the
70
+ previous tick and this one, nearest first, as `{ id, dist }` (`dist`: how far along that path, from
71
+ its point at the previous tick). A fast projectile never tunnels through a body between two ticks.
72
+
73
+ - The projectile is a point: `radius` is the target's radius **plus** the projectile's.
74
+ - Swept in `step`, bodies are tested where they stand this tick, after this tick's inputs moved them.
75
+ - **The shooter's own body comes back too** when the projectile starts inside it (launched from the
76
+ body's centre, as the templates do). Skip hits whose `owner` is the shooter.
77
+ - When `step` sweeps a projectile this tick's input launched, its path is just its start point: a
78
+ target standing inside the muzzle is hit before any client sees the shot (a point-blank hit).
79
+ - Cost is the number of entities of the kinds you ask for. A couple of hundred projectiles each
80
+ swept against the players fit a tick; hundreds swept against each other do not. Name only the
81
+ kinds that can be hit.
82
+
83
+ ### Removing them is the module's job
84
+
85
+ The server never ages a projectile out. Remove it on a hit, past the arena, below the floor and after
86
+ a maximum age, or the room fills up: a tick that would leave more than 1024 entities is a fault.
87
+ Keep the live projectiles in `ctx.state` (an array, in launch order) and walk that list in `step`,
88
+ so the order is fixed and every replay is identical.
89
+
90
+ ```js
91
+ step(ctx) {
92
+ ctx.state.shots = ctx.state.shots.filter((shot) => fly(ctx, shot)); // false once it is gone
93
+ }
94
+ ```
95
+
96
+ ### Limits
97
+
98
+ - Projectiles are entities: they count against the 1024 per room, and their launches and removals
99
+ against the 256 updates and 256 removals per tick.
100
+ - Launches still waiting to go out may hold at most 224 entities and 28 KiB of output — about a
101
+ hundred new projectiles in one tick. A launch past that throws a `RangeError` and writes nothing.
102
+ Uncaught, it is a fault, and the tick's changes go out with the next tick. A shotgun or a burst
103
+ catches it:
104
+
105
+ ```js
106
+ for (let i = 0; i < PELLETS; i++) {
107
+ try { ctx.projectiles.launch(pellet(i)); } catch (e) { if (e instanceof RangeError) break; throw e; }
108
+ }
109
+ ```
110
+
111
+ - Cap the fire rate per seat with a cooldown in ticks, as the template does. It is what keeps the
112
+ number of live projectiles, and of hit events per tick (at most 64), bounded.
113
+
114
+ ## The client
115
+
116
+ ### Fire: predict while staging the input
117
+
118
+ ```ts
119
+ if (wantsToFire && !armed) {
120
+ session.input.buttons = FIRE;
121
+ session.predictProjectile({ kind: "bolt", x: me.x, y: me.y, z: 0, vx, vy, vz: 0, g: 0 });
122
+ armed = true; // staged: predict nothing more until it is sent
123
+ }
124
+ const sent = session.frame(now);
125
+ if (armed && sent > 0) {
126
+ session.input.buttons = 0; // only the input the prediction was made for fires
127
+ armed = false;
128
+ }
129
+ ```
130
+
131
+ `predictProjectile` must run **before the `frame()` that sends the firing input**, once per
132
+ projectile the module will launch for it, in the same order. It returns the id the module's launch
133
+ will get; the projectile is in `session.entities()` that same frame. When the server's projectile of
134
+ that id arrives, the same entry keeps flying from where the prediction was, so a mesh keyed by id
135
+ never jumps back.
136
+
137
+ - **Predict the module's numbers**: the same start point, speed, gravity and aim. Round the aim with
138
+ `Math.fround` before you send it and before you use it, as the server stores it. The module
139
+ launches from the body after this input moved it, the client from its drawn body; a shooter firing
140
+ on the run sees a small nudge when the server's projectile takes over.
141
+ - **Mirror the cooldown, with slack.** A prediction the module refuses is drawn and then vanishes.
142
+ Count inputs with `session.telemetry.inputsSent` and wait a few inputs longer than the module's
143
+ cooldown in ticks.
144
+ - Predict only your own shots. Never create, move or remove projectile entities on the client.
145
+
146
+ ### Draw them from `session.entities()`
147
+
148
+ An entity with `t0 > 0` is a projectile. Its `x`, `y`, `z` (and `vx`, `vy`, `vz`) are already where
149
+ it is this frame: set the mesh there every frame and do not smooth, lerp or extrapolate it.
150
+
151
+ - `e.src === session.id` is your own shot; `''` is a server projectile, or one the server re-launched
152
+ (a bounce). A projectile is never `session.me()`.
153
+ - When an id leaves `session.entities()`, remove its mesh: it hit something, left the arena or was
154
+ refused.
155
+ - **A refused or point-blank shot**: the prediction disappears once the server has handled the input
156
+ that fired it (at the latest after max(2·RTT, 600 ms)). A refusal (cooldown, dead shooter) and a
157
+ point-blank hit look the same from the state; the module's own events say which, so draw hits,
158
+ damage and kills only from those events and from `hp`.
159
+ - `session.telemetry` counts `projectilesPredicted`, `projectilesConfirmed` and
160
+ `projectilesRejected`; `lastProjectileLeadMs` is how much later the server launched your latest
161
+ confirmed shot than you predicted it (roughly the one-way latency plus up to a tick).
162
+
163
+ ### Axes in three.js
164
+
165
+ The server's ground is x/y and +z is up; `yaw` 0 points along +x. In three.js y is up:
166
+
167
+ ```ts
168
+ mesh.position.set(e.x, e.z + HEIGHT, e.y); // server (x, y, z) → three (x, z, y)
169
+ mesh.lookAt(e.x + e.vx, e.z + HEIGHT + e.vz, e.y + e.vy); // point a rocket along its flight
170
+ ```
171
+
172
+ A body turns by `-yaw` around three's y axis. A projectile launched at `z: 0` and drawn at a fixed
173
+ `HEIGHT` keeps the hit test on the ground plane; a lob launches with `vz > 0` and `g > 0` and lands
174
+ when `ctx.projectiles.at(id).z < 0`.
175
+
176
+ ## Every client must support projectiles
177
+
178
+ A client that does not implement projectiles draws each one frozen where it was launched: the server
179
+ writes the launch once and never moves it. Every client in a room whose module launches projectiles
180
+ must support them:
181
+
182
+ - Ship `@genex-ai/multiplayer` 0.17.4 or newer in the same build as the first module that launches.
183
+ - A native build (Unity, Godot, Unreal, Rust) in the same room supports them only once its Genex
184
+ multiplayer SDK passes the projectile conformance checks; until then, keep that build out of rooms
185
+ with projectiles.
186
+
187
+ ## Before calling it done
188
+
189
+ 1. `validate` passes, and its fuzz players fire (bind the main shot to button bit 1, the bit the fuzz
190
+ and the local bots press most).
191
+ 2. At `--latency 75`, your shot leaves the instant you click and flies on without a jump; another
192
+ player's shots fly smoothly toward you, not 100 ms late.
193
+ 3. `session.telemetry.projectilesRejected` stays near 0 while you fire steadily (a rising count means
194
+ the client predicts shots the module refuses).
195
+ 4. Every projectile is removed: a few minutes of play leave the entity count flat.