@lagless/create 0.0.36 → 0.0.39

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 (46) hide show
  1. package/LICENSE +26 -0
  2. package/dist/index.js +96 -16
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -5
  5. package/templates/pixi-react/AGENTS.md +57 -27
  6. package/templates/pixi-react/CLAUDE.md +225 -49
  7. package/templates/pixi-react/README.md +16 -6
  8. package/templates/pixi-react/__packageName__-backend/package.json +1 -0
  9. package/templates/pixi-react/__packageName__-backend/src/main.ts +2 -0
  10. package/templates/pixi-react/__packageName__-frontend/package.json +8 -1
  11. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/grid-background.tsx +4 -0
  12. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/player-view.tsx +68 -0
  13. package/templates/pixi-react/__packageName__-frontend/src/app/game-view/runner-provider.tsx +57 -0
  14. package/templates/pixi-react/__packageName__-frontend/src/app/hooks/use-start-multiplayer-match.ts +5 -5
  15. package/templates/pixi-react/__packageName__-frontend/src/app/screens/title.screen.tsx +18 -1
  16. package/templates/pixi-react/__packageName__-frontend/{vite.config.ts → vite.config.ts.ejs} +0 -2
  17. package/templates/pixi-react/__packageName__-simulation/package.json +7 -0
  18. package/templates/pixi-react/__packageName__-simulation/src/lib/arena.ts +12 -0
  19. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +90 -6
  20. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +73 -0
  21. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/boundary.system.ts +2 -0
  22. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/damping.system.ts +2 -0
  23. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +8 -0
  24. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/integrate.system.ts +2 -0
  25. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/physics-step.system.ts +65 -0
  26. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-connection.system.ts +158 -0
  27. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-leave.system.ts +70 -0
  28. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/save-prev-transform.system.ts +46 -0
  29. package/templates/pixi-react/docs/01-schema-and-codegen.md +244 -0
  30. package/templates/pixi-react/docs/02-ecs-systems.md +293 -0
  31. package/templates/pixi-react/docs/03-determinism.md +204 -0
  32. package/templates/pixi-react/docs/04-input-system.md +255 -0
  33. package/templates/pixi-react/docs/05-signals.md +175 -0
  34. package/templates/pixi-react/docs/06-rendering.md +256 -0
  35. package/templates/pixi-react/docs/07-multiplayer.md +277 -0
  36. package/templates/pixi-react/docs/08-physics2d.md +266 -0
  37. package/templates/pixi-react/docs/08-physics3d.md +312 -0
  38. package/templates/pixi-react/docs/09-recipes.md +362 -0
  39. package/templates/pixi-react/docs/10-common-mistakes.md +224 -0
  40. package/templates/pixi-react/docs/api-quick-reference.md +254 -0
  41. package/templates/pixi-react/package.json +6 -0
  42. /package/templates/pixi-react/__packageName__-backend/{tsconfig.json → tsconfig.json.ejs} +0 -0
  43. /package/templates/pixi-react/__packageName__-frontend/{tsconfig.json → tsconfig.json.ejs} +0 -0
  44. /package/templates/pixi-react/__packageName__-simulation/{.swcrc → .swcrc.ejs} +0 -0
  45. /package/templates/pixi-react/__packageName__-simulation/{tsconfig.json → tsconfig.json.ejs} +0 -0
  46. /package/templates/pixi-react/{tsconfig.base.json → tsconfig.base.json.ejs} +0 -0
@@ -0,0 +1,362 @@
1
+ # Recipes — Step-by-Step Cookbook
2
+
3
+ ## Add a New Component
4
+
5
+ 1. Edit `ecs.yaml`:
6
+ ```yaml
7
+ components:
8
+ Health:
9
+ current: uint16
10
+ max: uint16
11
+ ```
12
+ 2. Run `pnpm codegen`
13
+ 3. Import in systems: `import { Health } from '../code-gen/core.js';`
14
+ 4. Add to constructor DI: `private readonly _health: Health`
15
+ 5. Access data: `this._health.unsafe.current[entity]`
16
+
17
+ ## Add a Tag Component
18
+
19
+ Tags have no fields — they only occupy a bitmask bit (zero memory per entity).
20
+
21
+ 1. Edit `ecs.yaml`:
22
+ ```yaml
23
+ components:
24
+ Frozen: {}
25
+ Dead: {}
26
+ ```
27
+ 2. Run `pnpm codegen`
28
+ 3. Use in systems:
29
+ ```typescript
30
+ // Add tag
31
+ this._entities.addComponent(entity, Frozen);
32
+ // Remove tag
33
+ this._entities.removeComponent(entity, Frozen);
34
+ // Check tag
35
+ if (this._entities.hasComponent(entity, Frozen)) { ... }
36
+ ```
37
+ 4. Use in filters:
38
+ ```yaml
39
+ filters:
40
+ ActivePlayerFilter:
41
+ include: [Transform2d, PlayerBody]
42
+ exclude: [Frozen, Dead]
43
+ ```
44
+
45
+ ## Add a New System
46
+
47
+ 1. Create `systems/my-feature.system.ts`:
48
+ ```typescript
49
+ import { ECSSystem, IECSSystem } from '@lagless/core';
50
+ import { Transform2d, PlayerFilter } from '../code-gen/core.js';
51
+
52
+ @ECSSystem()
53
+ export class MyFeatureSystem implements IECSSystem {
54
+ constructor(
55
+ private readonly _transform: Transform2d,
56
+ private readonly _filter: PlayerFilter,
57
+ ) {}
58
+
59
+ update(tick: number): void {
60
+ for (const entity of this._filter) {
61
+ // your logic here
62
+ }
63
+ }
64
+ }
65
+ ```
66
+ 2. Add to `systems/index.ts`:
67
+ ```typescript
68
+ import { MyFeatureSystem } from './my-feature.system.js';
69
+
70
+ export const systems = [
71
+ // ... existing systems
72
+ MyFeatureSystem, // add in correct execution order
73
+ // ... hash verification last
74
+ ];
75
+ ```
76
+
77
+ ## Add a New Input Type
78
+
79
+ 1. Edit `ecs.yaml`:
80
+ ```yaml
81
+ inputs:
82
+ ShootInput:
83
+ targetX: float32
84
+ targetY: float32
85
+ ```
86
+ 2. Run `pnpm codegen`
87
+ 3. Send from client (in `runner-provider.tsx` drainInputs):
88
+ ```typescript
89
+ if (mouseClicked) {
90
+ addRPC(ShootInput, { targetX: mouseWorldX, targetY: mouseWorldY });
91
+ }
92
+ ```
93
+ 4. Read in system:
94
+ ```typescript
95
+ const rpcs = this._input.collectTickRPCs(tick, ShootInput);
96
+ for (const rpc of rpcs) {
97
+ const targetX = MathOps.clamp(finite(rpc.data.targetX), -1000, 1000);
98
+ const targetY = MathOps.clamp(finite(rpc.data.targetY), -1000, 1000);
99
+ // spawn projectile, etc.
100
+ }
101
+ ```
102
+
103
+ ## Add a New Entity Type
104
+
105
+ 1. Define components and filter in `ecs.yaml`:
106
+ ```yaml
107
+ components:
108
+ Projectile:
109
+ ownerSlot: uint8
110
+ damage: uint16
111
+ spawnTick: uint32
112
+ lifetimeTicks: uint16
113
+ Velocity2d:
114
+ velocityX: float32
115
+ velocityY: float32
116
+
117
+ filters:
118
+ ProjectileFilter:
119
+ include: [Transform2d, Projectile, Velocity2d]
120
+ ```
121
+ 2. Run `pnpm codegen`
122
+ 3. Create spawn logic in a system:
123
+ ```typescript
124
+ const entity = this._entities.createEntity();
125
+ this._entities.addComponent(entity, Transform2d);
126
+ this._entities.addComponent(entity, Projectile);
127
+ this._entities.addComponent(entity, Velocity2d);
128
+
129
+ this._transform.set(entity, {
130
+ positionX: startX, positionY: startY,
131
+ prevPositionX: startX, prevPositionY: startY,
132
+ });
133
+ this._projectile.set(entity, {
134
+ ownerSlot: slot, damage: 10, spawnTick: tick, lifetimeTicks: 60,
135
+ });
136
+ ```
137
+ 4. Create view component (see [06-rendering.md](06-rendering.md))
138
+ 5. Add `<FilterViews filter={runner.Core.ProjectileFilter} View={ProjectileView} />`
139
+
140
+ ## Add a Signal
141
+
142
+ 1. Define in `signals/index.ts`:
143
+ ```typescript
144
+ @ECSSignal()
145
+ export class ExplosionSignal extends Signal<{ x: number; y: number }> {}
146
+ ```
147
+ 2. Register in `arena.ts` signals array
148
+ 3. Emit in system:
149
+ ```typescript
150
+ this._explosionSignal.emit(tick, { x: posX, y: posY });
151
+ ```
152
+ 4. Subscribe in view:
153
+ ```typescript
154
+ explosionSignal.Predicted.subscribe(e => spawnExplosionParticles(e.data.x, e.data.y));
155
+ explosionSignal.Cancelled.subscribe(e => removeExplosionParticles());
156
+ ```
157
+
158
+ ## Add a New Screen
159
+
160
+ 1. Create `screens/lobby.screen.tsx`:
161
+ ```tsx
162
+ export function LobbyScreen() {
163
+ const navigate = useNavigate();
164
+ return (
165
+ <div>
166
+ <h1>Lobby</h1>
167
+ <button onClick={() => navigate('/game')}>Start</button>
168
+ </div>
169
+ );
170
+ }
171
+ ```
172
+ 2. Add route in `router.tsx`:
173
+ ```tsx
174
+ <Route path="/lobby" element={<LobbyScreen />} />
175
+ ```
176
+
177
+ ## Add Bot AI
178
+
179
+ Bots are simulated as if they were players sending RPCs via server events.
180
+
181
+ 1. Add bot management to server hooks:
182
+ ```typescript
183
+ // In game-hooks.ts:
184
+ onRoomCreated: (ctx) => {
185
+ // Schedule bot input every tick
186
+ setInterval(() => {
187
+ const botSlot = 1; // reserved for bot
188
+ ctx.emitServerEvent(MoveInput, {
189
+ directionX: calculateBotDirX(),
190
+ directionY: calculateBotDirY(),
191
+ }, botSlot);
192
+ }, 50); // 20 ticks/sec
193
+ },
194
+ ```
195
+ 2. Bot inputs flow through the same RPC system — no special handling in simulation
196
+
197
+ ## Add Game Phases (Lobby → Playing → GameOver)
198
+
199
+ 1. Add singleton in `ecs.yaml`:
200
+ ```yaml
201
+ singletons:
202
+ GameState:
203
+ gamePhase: uint8 # 0=lobby, 1=countdown, 2=playing, 3=gameover
204
+ phaseStartTick: uint32
205
+ ```
206
+ 2. Create `game-phase.system.ts`:
207
+ ```typescript
208
+ @ECSSystem()
209
+ export class GamePhaseSystem implements IECSSystem {
210
+ constructor(
211
+ private readonly _gameState: GameState,
212
+ private readonly _config: ECSConfig,
213
+ ) {}
214
+
215
+ update(tick: number): void {
216
+ const phase = this._gameState.gamePhase;
217
+ const elapsed = tick - this._gameState.phaseStartTick;
218
+
219
+ if (phase === 1 && elapsed > 60) { // 3 seconds at 20 tps
220
+ this._gameState.gamePhase = 2;
221
+ this._gameState.phaseStartTick = tick;
222
+ }
223
+ // ... more phase transitions
224
+ }
225
+ }
226
+ ```
227
+
228
+ ## Add Timer / Countdown
229
+
230
+ ```typescript
231
+ @ECSSystem()
232
+ export class TimerSystem implements IECSSystem {
233
+ constructor(
234
+ private readonly _gameState: GameState,
235
+ private readonly _config: ECSConfig,
236
+ ) {}
237
+
238
+ update(tick: number): void {
239
+ const elapsed = tick - this._gameState.phaseStartTick;
240
+ const elapsedSeconds = elapsed * this._config.frameLength;
241
+ const remainingSeconds = Math.max(0, 120 - elapsedSeconds); // 2 minute timer
242
+
243
+ if (remainingSeconds <= 0) {
244
+ this._gameState.gamePhase = 3; // gameover
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ Display in React:
251
+ ```tsx
252
+ const elapsed = runner.Simulation.tick - gameState.phaseStartTick;
253
+ const remaining = Math.max(0, 120 - elapsed / runner.Config.tickRate);
254
+ return <div>{Math.ceil(remaining)}s</div>;
255
+ ```
256
+
257
+ ## Add Score Tracking
258
+
259
+ 1. Add to playerResources in `ecs.yaml`:
260
+ ```yaml
261
+ playerResources:
262
+ PlayerResource:
263
+ score: uint32
264
+ kills: uint16
265
+ deaths: uint16
266
+ ```
267
+ 2. Update in system:
268
+ ```typescript
269
+ this._playerResource.score[killerSlot] += 100;
270
+ this._playerResource.kills[killerSlot] += 1;
271
+ this._playerResource.deaths[victimSlot] += 1;
272
+ ```
273
+ 3. Read in view:
274
+ ```tsx
275
+ const score = runner.Core.PlayerResource.score[localSlot];
276
+ ```
277
+
278
+ ## Add Death / Respawn
279
+
280
+ ```typescript
281
+ @ECSSystem()
282
+ export class DeathRespawnSystem implements IECSSystem {
283
+ constructor(
284
+ private readonly _entities: EntitiesManager,
285
+ private readonly _transform: Transform2d,
286
+ private readonly _playerBody: PlayerBody,
287
+ private readonly _filter: PlayerFilter,
288
+ private readonly _prng: PRNG,
289
+ ) {}
290
+
291
+ update(tick: number): void {
292
+ for (const entity of this._filter) {
293
+ if (this._playerBody.unsafe.health[entity] <= 0) {
294
+ // Death: add Dead tag
295
+ this._entities.addComponent(entity, Dead);
296
+
297
+ // Schedule respawn: store death tick
298
+ this._playerBody.unsafe.deathTick[entity] = tick;
299
+ }
300
+ }
301
+
302
+ // Check for respawn
303
+ for (const entity of this._deadFilter) {
304
+ const deathTick = this._playerBody.unsafe.deathTick[entity];
305
+ if (tick - deathTick >= 60) { // 3 seconds at 20 tps
306
+ this._entities.removeComponent(entity, Dead);
307
+ // Respawn at random position
308
+ const x = this._prng.getRandomInt(-400, 400);
309
+ const y = this._prng.getRandomInt(-300, 300);
310
+ this._transform.set(entity, {
311
+ positionX: x, positionY: y,
312
+ prevPositionX: x, prevPositionY: y,
313
+ });
314
+ this._playerBody.unsafe.health[entity] = this._playerBody.unsafe.maxHealth[entity];
315
+ }
316
+ }
317
+ }
318
+ }
319
+ ```
320
+
321
+ ## Add Projectile with Lifetime
322
+
323
+ ```typescript
324
+ @ECSSystem()
325
+ export class ProjectileLifetimeSystem implements IECSSystem {
326
+ constructor(
327
+ private readonly _entities: EntitiesManager,
328
+ private readonly _projectile: Projectile,
329
+ private readonly _filter: ProjectileFilter,
330
+ ) {}
331
+
332
+ update(tick: number): void {
333
+ for (const entity of this._filter) {
334
+ const spawnTick = this._projectile.unsafe.spawnTick[entity];
335
+ const lifetime = this._projectile.unsafe.lifetimeTicks[entity];
336
+
337
+ if (tick - spawnTick >= lifetime) {
338
+ this._entities.removeEntity(entity);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ ```
344
+
345
+ ## Add a Singleton
346
+
347
+ 1. Edit `ecs.yaml`:
348
+ ```yaml
349
+ singletons:
350
+ ArenaConfig:
351
+ radius: float32
352
+ shrinkRate: float32
353
+ ```
354
+ 2. Run `pnpm codegen`
355
+ 3. Access in systems:
356
+ ```typescript
357
+ constructor(private readonly _arenaConfig: ArenaConfig) {}
358
+ update(tick: number): void {
359
+ const r = this._arenaConfig.radius;
360
+ this._arenaConfig.radius -= this._arenaConfig.shrinkRate;
361
+ }
362
+ ```
@@ -0,0 +1,224 @@
1
+ # Common Mistakes
2
+
3
+ ## Determinism
4
+
5
+ ### Using Math.sin/cos/atan2/sqrt
6
+
7
+ **Wrong:**
8
+ ```typescript
9
+ const angle = Math.atan2(dy, dx);
10
+ const dist = Math.sqrt(dx * dx + dy * dy);
11
+ ```
12
+ **Correct:**
13
+ ```typescript
14
+ const angle = MathOps.atan2(dy, dx);
15
+ const dist = MathOps.sqrt(dx * dx + dy * dy);
16
+ ```
17
+ **Why:** `Math.*` trig/sqrt results differ between browsers and platforms. MathOps uses WASM for cross-platform determinism.
18
+
19
+ ### Using Math.random()
20
+
21
+ **Wrong:** `const r = Math.random();`
22
+ **Correct:** `const r = this._prng.getFloat();`
23
+ **Why:** Math.random() produces different values on each client. PRNG state is in the ArrayBuffer and deterministic.
24
+
25
+ ### Forgetting prevPosition on Spawn
26
+
27
+ **Wrong:**
28
+ ```typescript
29
+ this._transform.set(entity, { positionX: 100, positionY: 200 });
30
+ ```
31
+ **Correct:**
32
+ ```typescript
33
+ this._transform.set(entity, {
34
+ positionX: 100, positionY: 200,
35
+ prevPositionX: 100, prevPositionY: 200,
36
+ });
37
+ ```
38
+ **Why:** prevPosition defaults to 0. Interpolation between (0,0) and (100,200) causes a one-frame visual jump.
39
+
40
+ ### Array.sort() without Comparator
41
+
42
+ **Wrong:** `entities.sort();`
43
+ **Correct:** `entities.sort((a, b) => a - b);`
44
+ **Why:** Default sort is lexicographic and engine-dependent. Explicit comparator ensures deterministic order.
45
+
46
+ ### Using Date.now() in Simulation
47
+
48
+ **Wrong:** `if (Date.now() > deadline) { ... }`
49
+ **Correct:** `if (tick > deadlineTick) { ... }`
50
+ **Why:** Date.now() differs between clients. Use tick count for all time-based logic.
51
+
52
+ ## Input
53
+
54
+ ### Not Sanitizing RPC Data
55
+
56
+ **Wrong:**
57
+ ```typescript
58
+ const dirX = rpc.data.directionX; // Could be NaN, Infinity
59
+ this._velocity.unsafe.velocityX[entity] = dirX * speed;
60
+ ```
61
+ **Correct:**
62
+ ```typescript
63
+ const finite = (v: number): number => Number.isFinite(v) ? v : 0;
64
+ let dirX = MathOps.clamp(finite(rpc.data.directionX), -1, 1);
65
+ this._velocity.unsafe.velocityX[entity] = dirX * speed;
66
+ ```
67
+ **Why:** Network messages can contain NaN/Infinity. NaN propagates through all math and corrupts state permanently.
68
+
69
+ ### Clamping Before Finite Check
70
+
71
+ **Wrong:** `MathOps.clamp(rpc.data.value, -1, 1)` — clamp(NaN) returns NaN
72
+ **Correct:** `MathOps.clamp(finite(rpc.data.value), -1, 1)`
73
+ **Why:** MathOps.clamp does NOT handle NaN. Always check `Number.isFinite()` first.
74
+
75
+ ## Schema
76
+
77
+ ### Editing Generated Code
78
+
79
+ **Wrong:** Editing files in `code-gen/` directory
80
+ **Correct:** Edit `ecs.yaml` and run `pnpm codegen`
81
+ **Why:** Generated files are overwritten on every codegen run. Changes will be lost.
82
+
83
+ ### Declaring Transform2d with simulationType
84
+
85
+ **Wrong:**
86
+ ```yaml
87
+ simulationType: physics2d
88
+ components:
89
+ Transform2d: # Already auto-prepended!
90
+ positionX: float32
91
+ ```
92
+ **Correct:**
93
+ ```yaml
94
+ simulationType: physics2d
95
+ components:
96
+ PlayerBody: # Only declare your own components
97
+ playerSlot: uint8
98
+ ```
99
+ **Why:** `simulationType: physics2d` auto-prepends Transform2d + PhysicsRefs. Declaring them manually causes conflicts.
100
+
101
+ ## Systems
102
+
103
+ ### Wrong System Order
104
+
105
+ **Wrong:** HashVerificationSystem before game logic systems
106
+ **Correct:** HashVerificationSystem always last
107
+ **Why:** Hash verification must check state after all game logic has run.
108
+
109
+ ### Storing State Outside ArrayBuffer
110
+
111
+ **Wrong:**
112
+ ```typescript
113
+ class MySystem {
114
+ private _cache = new Map(); // Lost on rollback!
115
+ }
116
+ ```
117
+ **Correct:** Store all state in ECS components, singletons, or player resources.
118
+ **Why:** Rollback restores the ArrayBuffer but NOT JavaScript variables. External state causes desync.
119
+
120
+ ### Modifying State in View Layer
121
+
122
+ **Wrong:**
123
+ ```typescript
124
+ // In filterView onUpdate:
125
+ runner.Core.Transform2d.unsafe.positionX[entity] = smoothedX;
126
+ ```
127
+ **Correct:** View layer is read-only. Only systems modify ECS state.
128
+ **Why:** View modifications bypass deterministic simulation, causing desync.
129
+
130
+ ## Rendering
131
+
132
+ ### Not Using VisualSmoother2d
133
+
134
+ **Wrong:**
135
+ ```typescript
136
+ container.position.set(
137
+ transform.unsafe.positionX[entity],
138
+ transform.unsafe.positionY[entity],
139
+ );
140
+ ```
141
+ **Better:**
142
+ ```typescript
143
+ const smoothed = smoother.update(prevX, prevY, currX, currY, factor, dt);
144
+ container.position.set(smoothed.x, smoothed.y);
145
+ ```
146
+ **Why:** Without smoothing, entities teleport on rollback. VisualSmoother2d absorbs position jumps and decays smoothly.
147
+
148
+ ### Using getCursor in Hot Path
149
+
150
+ **Wrong:**
151
+ ```typescript
152
+ onUpdate: ({ entity }, container) => {
153
+ const cursor = transform.getCursor(entity); // Object allocation every frame
154
+ container.x = cursor.positionX;
155
+ }
156
+ ```
157
+ **Correct:**
158
+ ```typescript
159
+ onUpdate: ({ entity }, container) => {
160
+ container.x = transform.unsafe.positionX[entity]; // Direct typed array access
161
+ }
162
+ ```
163
+ **Why:** getCursor creates an object per call. In onUpdate (called every frame per entity), use unsafe arrays.
164
+
165
+ ## Physics
166
+
167
+ ### Using handle | 0 for Rapier Handles
168
+
169
+ **Wrong:** `const index = handle | 0;`
170
+ **Correct:** Use `handleToIndex()` from `@lagless/physics-shared`
171
+ **Why:** Rapier handles are Float64 where low values are denormalized floats. Bitwise OR gives 0 for all of them.
172
+
173
+ ### Setting Dynamic Body Position Directly
174
+
175
+ **Wrong:**
176
+ ```typescript
177
+ this._transform.unsafe.positionX[entity] = newX; // Physics will overwrite!
178
+ ```
179
+ **Correct:**
180
+ ```typescript
181
+ this._physics.setLinearVelocity(entity, { x: vx, y: vy });
182
+ // or
183
+ this._physics.applyImpulse(entity, { x: fx, y: fy });
184
+ ```
185
+ **Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity.
186
+
187
+ ### Forgetting updateSceneQueries After Restore
188
+
189
+ The framework handles this automatically, but if you're doing custom Rapier operations:
190
+ **Always** call `world.updateSceneQueries()` after `World.restoreSnapshot()`.
191
+ **Why:** Restored worlds have empty QueryPipeline — ray casts and shape casts will miss all colliders.
192
+
193
+ ## Multiplayer
194
+
195
+ ### Keeping State in Frontend Variables
196
+
197
+ **Wrong:**
198
+ ```typescript
199
+ const [score, setScore] = useState(0);
200
+ // Updated in signal handler, lost on page refresh
201
+ ```
202
+ **Better:** Read score from `PlayerResource.score[slot]` in the ArrayBuffer.
203
+ **Why:** React state is not synchronized across clients. ECS state in the ArrayBuffer is.
204
+
205
+ ### Not Testing with Two Players
206
+
207
+ Always test multiplayer with at least 2 browser tabs or dev-player instances. Single-player testing misses:
208
+ - Rollback behavior
209
+ - Input delay effects
210
+ - State transfer
211
+ - Determinism bugs
212
+
213
+ ## Error Message Solutions
214
+
215
+ | Error | Cause | Solution |
216
+ |-------|-------|---------|
217
+ | `MathOps not initialized` | MathOps.init() not called | Add `await MathOps.init()` before simulation starts |
218
+ | `Entity X has no component Y` | Component not added before access | Call `addComponent(entity, Y)` first |
219
+ | `Filter overflow` | More entities than maxEntities | Increase `maxEntities` in ECSConfig |
220
+ | `Cannot read property of undefined` in system | DI injection failed | Check constructor parameter type matches imported class |
221
+ | `reflect-metadata` error | Missing import | Add `import '@abraham/reflection'` in main entry point |
222
+ | Hash mismatch in debug panel | Determinism violation | See [03-determinism.md](03-determinism.md) debugging section |
223
+ | `WASM module not found` | MathOps WASM not loaded | Ensure `await MathOps.init()` completes before any math |
224
+ | State transfer failed | No quorum on snapshot hash | Check for determinism bugs — clients diverged before state transfer |