@lagless/create 0.0.38 → 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 -4
  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 -0
  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__-simulation/package.json +7 -0
  17. package/templates/pixi-react/__packageName__-simulation/src/lib/arena.ts +12 -0
  18. package/templates/pixi-react/__packageName__-simulation/src/lib/schema/ecs.yaml +90 -6
  19. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/apply-move-input.system.ts +73 -0
  20. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/boundary.system.ts +2 -0
  21. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/damping.system.ts +2 -0
  22. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/index.ts +8 -0
  23. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/integrate.system.ts +2 -0
  24. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/physics-step.system.ts +65 -0
  25. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-connection.system.ts +158 -0
  26. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/player-leave.system.ts +70 -0
  27. package/templates/pixi-react/__packageName__-simulation/src/lib/systems/save-prev-transform.system.ts +46 -0
  28. package/templates/pixi-react/docs/01-schema-and-codegen.md +244 -0
  29. package/templates/pixi-react/docs/02-ecs-systems.md +293 -0
  30. package/templates/pixi-react/docs/03-determinism.md +204 -0
  31. package/templates/pixi-react/docs/04-input-system.md +255 -0
  32. package/templates/pixi-react/docs/05-signals.md +175 -0
  33. package/templates/pixi-react/docs/06-rendering.md +256 -0
  34. package/templates/pixi-react/docs/07-multiplayer.md +277 -0
  35. package/templates/pixi-react/docs/08-physics2d.md +266 -0
  36. package/templates/pixi-react/docs/08-physics3d.md +312 -0
  37. package/templates/pixi-react/docs/09-recipes.md +362 -0
  38. package/templates/pixi-react/docs/10-common-mistakes.md +224 -0
  39. package/templates/pixi-react/docs/api-quick-reference.md +254 -0
  40. package/templates/pixi-react/package.json +6 -0
  41. /package/templates/pixi-react/__packageName__-backend/{tsconfig.json → tsconfig.json.ejs} +0 -0
  42. /package/templates/pixi-react/__packageName__-frontend/{tsconfig.json → tsconfig.json.ejs} +0 -0
  43. /package/templates/pixi-react/__packageName__-frontend/{vite.config.ts → vite.config.ts.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,266 @@
1
+ # Physics 2D (Rapier)
2
+
3
+ ## Overview
4
+
5
+ `@lagless/physics2d` integrates [Rapier 2D](https://rapier.rs/) for deterministic rigid body physics. The framework manages Rapier world snapshots for rollback, body↔entity mapping, and transform synchronization.
6
+
7
+ ## Auto-Prepended Components
8
+
9
+ When `simulationType: physics2d` is set in `ecs.yaml`, codegen auto-prepends:
10
+
11
+ **Transform2d** (6 float32 fields):
12
+ - `positionX`, `positionY` — current position
13
+ - `rotation` — current rotation (radians)
14
+ - `prevPositionX`, `prevPositionY` — previous position (for interpolation)
15
+ - `prevRotation` — previous rotation
16
+
17
+ **PhysicsRefs** (4 fields):
18
+ - `bodyHandle: float64` — Rapier rigid body handle
19
+ - `colliderHandle: float64` — Rapier collider handle
20
+ - `bodyType: uint8` — body type enum (Dynamic, Fixed, etc.)
21
+ - `collisionLayer: uint16` — collision layer bitmask
22
+
23
+ **Do NOT declare these manually** — they are auto-prepended by codegen.
24
+
25
+ ## Body Types
26
+
27
+ ```typescript
28
+ import { BodyType } from '@lagless/physics-shared';
29
+
30
+ BodyType.DYNAMIC // 0 — affected by forces and collisions
31
+ BodyType.FIXED // 1 — immovable (walls, ground)
32
+ BodyType.KINEMATIC_POSITION // 2 — moved by setting position directly
33
+ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity directly
34
+ ```
35
+
36
+ ## Creating Bodies and Colliders
37
+
38
+ Use `PhysicsWorldManager2d` to create physics bodies:
39
+
40
+ ```typescript
41
+ import { ECSSystem, IECSSystem } from '@lagless/core';
42
+ import { PhysicsWorldManager2d } from '@lagless/physics2d';
43
+ import { BodyType, CollisionLayers } from '@lagless/physics-shared';
44
+
45
+ @ECSSystem()
46
+ export class SpawnSystem implements IECSSystem {
47
+ constructor(
48
+ private readonly _physics: PhysicsWorldManager2d,
49
+ private readonly _entities: EntitiesManager,
50
+ private readonly _transform: Transform2d,
51
+ ) {}
52
+
53
+ update(tick: number): void {
54
+ // Create entity
55
+ const entity = this._entities.createEntity();
56
+ this._entities.addComponent(entity, Transform2d);
57
+ this._entities.addComponent(entity, PhysicsRefs);
58
+
59
+ // Set initial position
60
+ this._transform.set(entity, {
61
+ positionX: 100, positionY: 200,
62
+ prevPositionX: 100, prevPositionY: 200,
63
+ rotation: 0, prevRotation: 0,
64
+ });
65
+
66
+ // Create physics body + collider
67
+ this._physics.createBody(entity, {
68
+ bodyType: BodyType.DYNAMIC,
69
+ position: { x: 100, y: 200 },
70
+ rotation: 0,
71
+ });
72
+
73
+ this._physics.createCollider(entity, {
74
+ shape: { type: 'ball', radius: 20 },
75
+ density: 1.0,
76
+ friction: 0.5,
77
+ restitution: 0.3,
78
+ collisionLayer: CollisionLayers.get('player'),
79
+ });
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### Collider Shapes
85
+
86
+ ```typescript
87
+ // Circle
88
+ { type: 'ball', radius: 20 }
89
+
90
+ // Rectangle
91
+ { type: 'cuboid', hx: 50, hy: 25 } // half-extents
92
+
93
+ // Capsule
94
+ { type: 'capsule', halfHeight: 30, radius: 10 }
95
+
96
+ // Convex polygon
97
+ { type: 'convexHull', points: [x1,y1, x2,y2, ...] }
98
+ ```
99
+
100
+ ## Collision Layers
101
+
102
+ Named collision groups (max 16 layers). Control which objects collide with which.
103
+
104
+ ```typescript
105
+ import { CollisionLayers } from '@lagless/physics-shared';
106
+
107
+ // Register layers (call once at startup):
108
+ CollisionLayers.register('player');
109
+ CollisionLayers.register('wall');
110
+ CollisionLayers.register('projectile');
111
+ CollisionLayers.register('pickup');
112
+
113
+ // Get layer value:
114
+ const playerLayer = CollisionLayers.get('player');
115
+
116
+ // Interaction groups — which layers collide:
117
+ CollisionLayers.setInteraction('player', 'wall', true);
118
+ CollisionLayers.setInteraction('player', 'projectile', true);
119
+ CollisionLayers.setInteraction('projectile', 'wall', true);
120
+ CollisionLayers.setInteraction('player', 'pickup', true);
121
+ // player↔player collision:
122
+ CollisionLayers.setInteraction('player', 'player', true);
123
+ ```
124
+
125
+ ## Collision Events
126
+
127
+ Drain collision events in a system each tick:
128
+
129
+ ```typescript
130
+ import { CollisionEvents2d } from '@lagless/physics2d';
131
+
132
+ @ECSSystem()
133
+ export class CollisionSystem implements IECSSystem {
134
+ constructor(
135
+ private readonly _collisionEvents: CollisionEvents2d,
136
+ ) {}
137
+
138
+ update(tick: number): void {
139
+ // Drain events (must be called every tick)
140
+ this._collisionEvents.drain();
141
+
142
+ // Process collision start events
143
+ for (let i = 0; i < this._collisionEvents.startCount; i++) {
144
+ const entityA = this._collisionEvents.startEntityA[i];
145
+ const entityB = this._collisionEvents.startEntityB[i];
146
+ // Handle collision between entityA and entityB
147
+ }
148
+
149
+ // Process collision end events
150
+ for (let i = 0; i < this._collisionEvents.endCount; i++) {
151
+ const entityA = this._collisionEvents.endEntityA[i];
152
+ const entityB = this._collisionEvents.endEntityB[i];
153
+ // Handle separation
154
+ }
155
+ }
156
+ }
157
+ ```
158
+
159
+ ### Collision Events and Determinism
160
+
161
+ Collision events are **ephemeral** — they are cleared each tick in `drain()` and regenerated on re-simulation after rollback. No snapshot storage needed. The Rapier EventQueue is stateless between ticks.
162
+
163
+ ## Physics Step System
164
+
165
+ The physics step system advances the Rapier world and syncs transforms:
166
+
167
+ ```typescript
168
+ import { ECSSystem, IECSSystem } from '@lagless/core';
169
+ import { PhysicsWorldManager2d } from '@lagless/physics2d';
170
+
171
+ @ECSSystem()
172
+ export class PhysicsStepSystem implements IECSSystem {
173
+ constructor(
174
+ private readonly _physics: PhysicsWorldManager2d,
175
+ ) {}
176
+
177
+ update(tick: number): void {
178
+ this._physics.step();
179
+ // Rapier positions → ECS Transform2d (automatic)
180
+ }
181
+ }
182
+ ```
183
+
184
+ The `step()` method:
185
+ 1. Steps the Rapier world with the configured timestep
186
+ 2. Syncs Rapier body positions/rotations → ECS Transform2d component
187
+ 3. Drains collision events
188
+
189
+ ## ColliderEntityMap — Handle↔Entity Mapping
190
+
191
+ Rapier uses Float64 handles to identify bodies and colliders. The `ColliderEntityMap` maps these to entity IDs.
192
+
193
+ **Critical:** Rapier handles are Float64 values where the bit pattern encodes an arena index. `handle | 0` gives 0 for denormalized floats — **never** use bitwise OR for conversion. The framework uses `handleToIndex()` with Float64Array→Uint32Array reinterpretation.
194
+
195
+ You generally don't interact with this directly — it's managed by `PhysicsWorldManager2d`.
196
+
197
+ ## Rollback
198
+
199
+ On rollback:
200
+ 1. ArrayBuffer is restored → ECS state reverts
201
+ 2. Rapier world snapshot is restored → physics state reverts
202
+ 3. `updateSceneQueries()` is called → QueryPipeline is rebuilt
203
+
204
+ **Critical fix applied:** `World.restoreSnapshot()` creates a world with an **empty** QueryPipeline (not serialized). The framework calls `updateSceneQueries()` after restore to fix this. Without it, ray casts and shape casts fail on the first tick after rollback.
205
+
206
+ ## State Transfer
207
+
208
+ After `applyExternalState()` (late join / reconnect):
209
+ 1. Rapier world snapshot is applied alongside ArrayBuffer
210
+ 2. `ColliderEntityMap` is rebuilt by iterating all entities with PhysicsRefs
211
+ 3. Collision layers are re-applied
212
+
213
+ This is handled automatically by the physics runner. You don't need to do anything special.
214
+
215
+ ## Complete Physics System Example
216
+
217
+ ```typescript
218
+ import { ECSSystem, IECSSystem, AbstractInputProvider, ECSConfig, EntitiesManager } from '@lagless/core';
219
+ import { MathOps } from '@lagless/math';
220
+ import { PhysicsWorldManager2d, CollisionEvents2d } from '@lagless/physics2d';
221
+ import { BodyType } from '@lagless/physics-shared';
222
+ import { Transform2d, PhysicsRefs, PlayerBody, PlayerFilter, MoveInput } from '../code-gen/core.js';
223
+
224
+ const finite = (v: number): number => Number.isFinite(v) ? v : 0;
225
+
226
+ @ECSSystem()
227
+ export class ApplyMoveInputSystem implements IECSSystem {
228
+ constructor(
229
+ private readonly _input: AbstractInputProvider,
230
+ private readonly _physics: PhysicsWorldManager2d,
231
+ private readonly _playerBody: PlayerBody,
232
+ private readonly _filter: PlayerFilter,
233
+ private readonly _config: ECSConfig,
234
+ ) {}
235
+
236
+ update(tick: number): void {
237
+ const rpcs = this._input.collectTickRPCs(tick, MoveInput);
238
+ for (const rpc of rpcs) {
239
+ const slot = rpc.meta.playerSlot;
240
+ let dirX = MathOps.clamp(finite(rpc.data.directionX), -1, 1);
241
+ let dirY = MathOps.clamp(finite(rpc.data.directionY), -1, 1);
242
+
243
+ for (const entity of this._filter) {
244
+ if (this._playerBody.unsafe.playerSlot[entity] !== slot) continue;
245
+
246
+ const speed = 300;
247
+ // Apply velocity to Rapier body
248
+ this._physics.setLinearVelocity(entity, {
249
+ x: dirX * speed,
250
+ y: dirY * speed,
251
+ });
252
+ break;
253
+ }
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ## Tips
260
+
261
+ - **Dynamic bodies** are moved by forces/impulses/velocity — never set position directly
262
+ - **Kinematic bodies** are moved by setting position or velocity — not affected by forces
263
+ - **Fixed bodies** never move — use for walls, ground, boundaries
264
+ - **Substeps** — Rapier can run multiple sub-steps per tick for stability. Configure in physics config.
265
+ - **Gravity** — set in physics world config. Default is (0, 0) for top-down games.
266
+ - **CCD** — continuous collision detection prevents tunneling through thin walls at high speeds.
@@ -0,0 +1,312 @@
1
+ # Physics 3D (Rapier)
2
+
3
+ ## Overview
4
+
5
+ `@lagless/physics3d` integrates [Rapier 3D](https://rapier.rs/) for deterministic rigid body physics. The framework manages Rapier world snapshots for rollback, body↔entity mapping, and transform synchronization.
6
+
7
+ For character controllers, see also `@lagless/character-controller-3d`.
8
+
9
+ ## Auto-Prepended Components
10
+
11
+ When `simulationType: physics3d` is set in `ecs.yaml`, codegen auto-prepends:
12
+
13
+ **Transform3d** (14 float32 fields):
14
+ - `positionX`, `positionY`, `positionZ` — current position
15
+ - `rotationX`, `rotationY`, `rotationZ`, `rotationW` — current rotation (quaternion)
16
+ - `prevPositionX`, `prevPositionY`, `prevPositionZ` — previous position
17
+ - `prevRotationX`, `prevRotationY`, `prevRotationZ`, `prevRotationW` — previous rotation
18
+
19
+ **PhysicsRefs** (4 fields):
20
+ - `bodyHandle: float64` — Rapier rigid body handle
21
+ - `colliderHandle: float64` — Rapier collider handle
22
+ - `bodyType: uint8` — body type enum
23
+ - `collisionLayer: uint16` — collision layer bitmask
24
+
25
+ **Do NOT declare these manually** — they are auto-prepended by codegen.
26
+
27
+ ## Body Types
28
+
29
+ ```typescript
30
+ import { BodyType } from '@lagless/physics-shared';
31
+
32
+ BodyType.DYNAMIC // 0 — affected by forces and collisions
33
+ BodyType.FIXED // 1 — immovable (walls, ground)
34
+ BodyType.KINEMATIC_POSITION // 2 — moved by setting position
35
+ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity
36
+ ```
37
+
38
+ ## Creating Bodies and Colliders
39
+
40
+ ```typescript
41
+ import { PhysicsWorldManager3d } from '@lagless/physics3d';
42
+ import { BodyType, CollisionLayers } from '@lagless/physics-shared';
43
+
44
+ @ECSSystem()
45
+ export class SpawnSystem implements IECSSystem {
46
+ constructor(
47
+ private readonly _physics: PhysicsWorldManager3d,
48
+ private readonly _entities: EntitiesManager,
49
+ private readonly _transform: Transform3d,
50
+ ) {}
51
+
52
+ update(tick: number): void {
53
+ const entity = this._entities.createEntity();
54
+ this._entities.addComponent(entity, Transform3d);
55
+ this._entities.addComponent(entity, PhysicsRefs);
56
+
57
+ this._transform.set(entity, {
58
+ positionX: 0, positionY: 5, positionZ: 0,
59
+ rotationX: 0, rotationY: 0, rotationZ: 0, rotationW: 1,
60
+ prevPositionX: 0, prevPositionY: 5, prevPositionZ: 0,
61
+ prevRotationX: 0, prevRotationY: 0, prevRotationZ: 0, prevRotationW: 1,
62
+ });
63
+
64
+ this._physics.createBody(entity, {
65
+ bodyType: BodyType.DYNAMIC,
66
+ position: { x: 0, y: 5, z: 0 },
67
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
68
+ });
69
+
70
+ this._physics.createCollider(entity, {
71
+ shape: { type: 'ball', radius: 0.5 },
72
+ density: 1.0,
73
+ friction: 0.5,
74
+ restitution: 0.3,
75
+ collisionLayer: CollisionLayers.get('player'),
76
+ });
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Collider Shapes (3D)
82
+
83
+ ```typescript
84
+ // Sphere
85
+ { type: 'ball', radius: 0.5 }
86
+
87
+ // Box
88
+ { type: 'cuboid', hx: 1, hy: 0.5, hz: 1 } // half-extents
89
+
90
+ // Capsule
91
+ { type: 'capsule', halfHeight: 0.5, radius: 0.3 }
92
+
93
+ // Cylinder
94
+ { type: 'cylinder', halfHeight: 1.0, radius: 0.5 }
95
+
96
+ // Convex hull
97
+ { type: 'convexHull', points: Float32Array }
98
+
99
+ // Triangle mesh (static only)
100
+ { type: 'trimesh', vertices: Float32Array, indices: Uint32Array }
101
+ ```
102
+
103
+ ## Collision Layers and Events
104
+
105
+ Same API as 2D — see [08-physics2d.md](08-physics2d.md) for `CollisionLayers` and `CollisionEvents` documentation. Use `CollisionEvents3d` instead of `CollisionEvents2d`.
106
+
107
+ ## Physics Step System
108
+
109
+ ```typescript
110
+ @ECSSystem()
111
+ export class PhysicsStepSystem implements IECSSystem {
112
+ constructor(private readonly _physics: PhysicsWorldManager3d) {}
113
+
114
+ update(tick: number): void {
115
+ this._physics.step();
116
+ // Rapier 3D positions/rotations → ECS Transform3d (automatic)
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Character Controller (KCC)
122
+
123
+ The `@lagless/character-controller-3d` library provides deterministic character movement using Rapier's KinematicCharacterController.
124
+
125
+ ### Setup
126
+
127
+ ```typescript
128
+ import { CharacterControllerManager } from '@lagless/character-controller-3d';
129
+
130
+ // Create manager (in runner setup or first system tick):
131
+ const kccManager = new CharacterControllerManager(physicsWorld, {
132
+ offset: 0.01, // skin width
133
+ maxSlopeClimbAngle: 0.8, // ~45 degrees
134
+ maxSlopeSlideAngle: 0.6, // ~34 degrees
135
+ stepHeight: 0.3,
136
+ snapToGround: 0.3,
137
+ });
138
+ ```
139
+
140
+ ### Movement System
141
+
142
+ ```typescript
143
+ @ECSSystem()
144
+ export class CharacterMovementSystem implements IECSSystem {
145
+ constructor(
146
+ private readonly _kcc: CharacterControllerManager,
147
+ private readonly _input: AbstractInputProvider,
148
+ private readonly _transform: Transform3d,
149
+ private readonly _filter: PlayerFilter,
150
+ private readonly _config: ECSConfig,
151
+ ) {}
152
+
153
+ update(tick: number): void {
154
+ const rpcs = this._input.collectTickRPCs(tick, MoveInput);
155
+ for (const rpc of rpcs) {
156
+ let dirX = MathOps.clamp(finite(rpc.data.directionX), -1, 1);
157
+ let dirZ = MathOps.clamp(finite(rpc.data.directionZ), -1, 1);
158
+ const cameraYaw = finite(rpc.data.cameraYaw);
159
+
160
+ for (const entity of this._filter) {
161
+ if (this._playerBody.unsafe.playerSlot[entity] !== rpc.meta.playerSlot) continue;
162
+
163
+ // Rotate input by camera yaw
164
+ const sinYaw = MathOps.sin(cameraYaw);
165
+ const cosYaw = MathOps.cos(cameraYaw);
166
+ const worldX = dirX * cosYaw - dirZ * sinYaw;
167
+ const worldZ = dirX * sinYaw + dirZ * cosYaw;
168
+
169
+ const speed = 5.0;
170
+ const dt = this._config.frameLength;
171
+
172
+ // Move character via KCC
173
+ this._kcc.computeMovement(entity, {
174
+ x: worldX * speed * dt,
175
+ y: -9.81 * dt, // gravity
176
+ z: worldZ * speed * dt,
177
+ });
178
+
179
+ // Apply movement result
180
+ const result = this._kcc.getMovementResult(entity);
181
+ this._kcc.applyMovement(entity, result);
182
+
183
+ // Check grounding
184
+ const grounded = this._kcc.isGrounded(entity);
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ ```
191
+
192
+ ### Rollback with KCC
193
+
194
+ After rollback, character controllers must be recreated:
195
+
196
+ ```typescript
197
+ // The physics runner handles this automatically via recreateAll()
198
+ // after Rapier world snapshot restore
199
+ ```
200
+
201
+ ## Animation Controller
202
+
203
+ The `@lagless/animation-controller` library provides deterministic animation state machines.
204
+
205
+ ### AnimationStateMachine
206
+
207
+ ```typescript
208
+ import { AnimationStateMachine } from '@lagless/animation-controller';
209
+
210
+ const fsm = new AnimationStateMachine({
211
+ states: {
212
+ idle: { animation: 'idle', loop: true },
213
+ walk: { animation: 'walk', loop: true },
214
+ run: { animation: 'run', loop: true },
215
+ jump: { animation: 'jump', loop: false },
216
+ },
217
+ transitions: [
218
+ { from: 'idle', to: 'walk', condition: (ctx) => ctx.speed > 0.1 },
219
+ { from: 'walk', to: 'run', condition: (ctx) => ctx.speed > 3.0 },
220
+ { from: 'walk', to: 'idle', condition: (ctx) => ctx.speed < 0.1 },
221
+ { from: 'run', to: 'walk', condition: (ctx) => ctx.speed < 3.0 },
222
+ { from: '*', to: 'jump', condition: (ctx) => !ctx.grounded },
223
+ { from: 'jump', to: 'idle', condition: (ctx) => ctx.grounded },
224
+ ],
225
+ initialState: 'idle',
226
+ crossfadeDuration: 0.2,
227
+ });
228
+ ```
229
+
230
+ ### LocomotionBlendCalculator
231
+
232
+ Blends between idle/walk/run based on speed:
233
+
234
+ ```typescript
235
+ import { LocomotionBlendCalculator } from '@lagless/animation-controller';
236
+
237
+ const locomotion = new LocomotionBlendCalculator({
238
+ walkSpeed: 2.0,
239
+ runSpeed: 5.0,
240
+ });
241
+
242
+ // Each tick:
243
+ const blend = locomotion.calculate(currentSpeed);
244
+ // blend.idle, blend.walk, blend.run — weights summing to 1.0
245
+ ```
246
+
247
+ ### AnimationViewAdapter
248
+
249
+ Connects deterministic animation state to 3D engine (BabylonJS, Three.js):
250
+
251
+ ```typescript
252
+ import { AnimationViewAdapter } from '@lagless/animation-controller';
253
+
254
+ const adapter = new AnimationViewAdapter(fsm, {
255
+ playAnimation: (name, options) => { /* play in 3D engine */ },
256
+ stopAnimation: (name) => { /* stop in 3D engine */ },
257
+ setWeight: (name, weight) => { /* blend weight */ },
258
+ });
259
+
260
+ // Each frame:
261
+ adapter.update(deltaTime);
262
+ ```
263
+
264
+ ## System Execution Order for 3D
265
+
266
+ ```typescript
267
+ export const systems = [
268
+ SavePrevTransformSystem, // 1. Store prev positions/rotations
269
+ PlayerConnectionSystem, // 2. Handle join/leave
270
+ ApplyMoveInputSystem, // 3. Read inputs
271
+ CharacterMovementSystem, // 4. KCC movement (before physics step)
272
+ PhysicsStepSystem, // 5. Step Rapier, sync transforms
273
+ AnimationSystem, // 6. Update animation FSM (after physics)
274
+ PlayerLeaveSystem, // 7. Cleanup
275
+ HashVerificationSystem, // 8. Always last
276
+ ];
277
+ ```
278
+
279
+ ## Rollback
280
+
281
+ On rollback:
282
+ 1. ArrayBuffer is restored → ECS state reverts
283
+ 2. Rapier 3D world snapshot is restored → physics state reverts
284
+ 3. `updateSceneQueries()` is called → QueryPipeline is rebuilt
285
+ 4. KCC controllers are recreated via `recreateAll()`
286
+
287
+ **Critical fix:** `World.restoreSnapshot()` creates a world with an empty QueryPipeline. The framework calls `updateSceneQueries()` after restore. Without this, `computeColliderMovement()` queries fail on the first tick after rollback.
288
+
289
+ ## State Transfer
290
+
291
+ After `applyExternalState()`:
292
+ 1. Rapier 3D world snapshot is applied alongside ArrayBuffer
293
+ 2. `ColliderEntityMap` is rebuilt by iterating all entities with PhysicsRefs
294
+ 3. KCC controllers are recreated
295
+ 4. Collision layers are re-applied
296
+
297
+ Handled automatically by the physics runner.
298
+
299
+ ## Rapier Handle Encoding
300
+
301
+ Rapier WASM handles are **Float64** values where the bit pattern encodes an arena index in the low 32 bits.
302
+ - Handle `0` = float64 `0.0`, handle `1` = float64 `5e-324` (Number.MIN_VALUE)
303
+ - **NEVER** use `handle | 0` — gives 0 for all denormalized floats
304
+ - The framework uses `handleToIndex()` with Float64Array→Uint32Array reinterpretation
305
+
306
+ ## Tips
307
+
308
+ - **Gravity:** default is (0, -9.81, 0) for 3D. Set in physics config.
309
+ - **Quaternion rotation:** rotationW=1 for identity rotation. Set all 4 components.
310
+ - **KCC offset:** small positive value (0.01) prevents character from getting stuck in geometry
311
+ - **System order matters:** KCC movement must run BEFORE PhysicsStep
312
+ - **Animation updates** should run AFTER physics to use final position/velocity