@lagless/create 0.0.44 → 0.0.48

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.
@@ -35,12 +35,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity directly
35
35
 
36
36
  ## Creating Bodies and Colliders
37
37
 
38
- Use `PhysicsWorldManager2d` to create physics bodies:
38
+ Use `PhysicsWorldManager2d` to create physics bodies. The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
39
39
 
40
40
  ```typescript
41
- import { ECSSystem, IECSSystem } from '@lagless/core';
41
+ import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
42
42
  import { PhysicsWorldManager2d } from '@lagless/physics2d';
43
43
  import { BodyType, CollisionLayers } from '@lagless/physics-shared';
44
+ import { Transform2d, PhysicsRefs } from '../code-gen/core.js';
44
45
 
45
46
  @ECSSystem()
46
47
  export class SpawnSystem implements IECSSystem {
@@ -48,6 +49,7 @@ export class SpawnSystem implements IECSSystem {
48
49
  private readonly _physics: PhysicsWorldManager2d,
49
50
  private readonly _entities: EntitiesManager,
50
51
  private readonly _transform: Transform2d,
52
+ private readonly _physicsRefs: PhysicsRefs,
51
53
  ) {}
52
54
 
53
55
  update(tick: number): void {
@@ -56,45 +58,60 @@ export class SpawnSystem implements IECSSystem {
56
58
  this._entities.addComponent(entity, Transform2d);
57
59
  this._entities.addComponent(entity, PhysicsRefs);
58
60
 
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
- });
61
+ // Set initial position in ECS (including prev for interpolation)
62
+ const t = this._transform.unsafe;
63
+ t.positionX[entity] = 100;
64
+ t.positionY[entity] = 200;
65
+ t.prevPositionX[entity] = 100;
66
+ t.prevPositionY[entity] = 200;
67
+ t.rotation[entity] = 0;
68
+ t.prevRotation[entity] = 0;
69
+
70
+ // Create a dynamic Rapier body and configure it
71
+ const body = this._physics.createDynamicBody();
72
+ body.setTranslation({ x: 100, y: 200 }, true);
73
+ body.setLinearDamping(5.0);
74
+
75
+ // Create a ball collider attached to the body
76
+ const groups = CollisionLayers.get('player');
77
+ const collider = this._physics.createBallCollider(20, body, groups);
78
+
79
+ // Store handles in ECS for later lookup
80
+ const pr = this._physicsRefs.unsafe;
81
+ pr.bodyHandle[entity] = body.handle;
82
+ pr.colliderHandle[entity] = collider.handle;
83
+ pr.bodyType[entity] = BodyType.DYNAMIC;
84
+ pr.collisionLayer[entity] = groups;
85
+
86
+ // Register collider→entity mapping (used by collision events)
87
+ this._physics.registerCollider(collider.handle, entity);
80
88
  }
81
89
  }
82
90
  ```
83
91
 
84
92
  ### Collider Shapes
85
93
 
94
+ All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
95
+
86
96
  ```typescript
87
97
  // Circle
88
- { type: 'ball', radius: 20 }
98
+ this._physics.createBallCollider(radius, parent, groups, activeEvents);
89
99
 
90
- // Rectangle
91
- { type: 'cuboid', hx: 50, hy: 25 } // half-extents
100
+ // Rectangle (half-extents)
101
+ this._physics.createCuboidCollider(hx, hy, parent, groups, activeEvents);
92
102
 
93
103
  // Capsule
94
- { type: 'capsule', halfHeight: 30, radius: 10 }
104
+ this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
105
+
106
+ // Convex polygon (returns null if hull computation fails)
107
+ this._physics.createConvexHullCollider(new Float32Array([x1,y1, x2,y2, ...]), parent, groups, activeEvents);
95
108
 
96
- // Convex polygon
97
- { type: 'convexHull', points: [x1,y1, x2,y2, ...] }
109
+ // Triangle mesh (static geometry only)
110
+ this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
111
+
112
+ // Custom collider from a Rapier ColliderDesc
113
+ const desc = this._physics.rapier.ColliderDesc.ball(10).setDensity(2.0).setFriction(0.5);
114
+ this._physics.createColliderFromDesc(desc, parent);
98
115
  ```
99
116
 
100
117
  ## Collision Layers
@@ -199,9 +216,7 @@ You generally don't interact with this directly — it's managed by `PhysicsWorl
199
216
  On rollback:
200
217
  1. ArrayBuffer is restored → ECS state reverts
201
218
  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.
219
+ 3. `ColliderEntityMap` is rebuilt automatically
205
220
 
206
221
  ## State Transfer
207
222
 
@@ -217,7 +232,7 @@ This is handled automatically by the physics runner. You don't need to do anythi
217
232
  ```typescript
218
233
  import { ECSSystem, IECSSystem, AbstractInputProvider, ECSConfig, EntitiesManager } from '@lagless/core';
219
234
  import { MathOps } from '@lagless/math';
220
- import { PhysicsWorldManager2d, CollisionEvents2d } from '@lagless/physics2d';
235
+ import { PhysicsWorldManager2d } from '@lagless/physics2d';
221
236
  import { BodyType } from '@lagless/physics-shared';
222
237
  import { Transform2d, PhysicsRefs, PlayerBody, PlayerFilter, MoveInput } from '../code-gen/core.js';
223
238
 
@@ -228,9 +243,9 @@ export class ApplyMoveInputSystem implements IECSSystem {
228
243
  constructor(
229
244
  private readonly _input: AbstractInputProvider,
230
245
  private readonly _physics: PhysicsWorldManager2d,
246
+ private readonly _physicsRefs: PhysicsRefs,
231
247
  private readonly _playerBody: PlayerBody,
232
248
  private readonly _filter: PlayerFilter,
233
- private readonly _config: ECSConfig,
234
249
  ) {}
235
250
 
236
251
  update(tick: number): void {
@@ -244,11 +259,10 @@ export class ApplyMoveInputSystem implements IECSSystem {
244
259
  if (this._playerBody.unsafe.playerSlot[entity] !== slot) continue;
245
260
 
246
261
  const speed = 300;
247
- // Apply velocity to Rapier body
248
- this._physics.setLinearVelocity(entity, {
249
- x: dirX * speed,
250
- y: dirY * speed,
251
- });
262
+ // Get the Rapier body via its handle stored in PhysicsRefs
263
+ const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
264
+ // Apply velocity directly on the Rapier body
265
+ body.setLinvel({ x: dirX * speed, y: dirY * speed }, true);
252
266
  break;
253
267
  }
254
268
  }
@@ -37,9 +37,13 @@ BodyType.KINEMATIC_VELOCITY // 3 — moved by setting velocity
37
37
 
38
38
  ## Creating Bodies and Colliders
39
39
 
40
+ The manager provides factory methods that return Rapier body/collider objects. You configure them via Rapier's native API, then store handles in the ECS `PhysicsRefs` component and register the collider for entity lookup.
41
+
40
42
  ```typescript
43
+ import { ECSSystem, IECSSystem, EntitiesManager } from '@lagless/core';
41
44
  import { PhysicsWorldManager3d } from '@lagless/physics3d';
42
45
  import { BodyType, CollisionLayers } from '@lagless/physics-shared';
46
+ import { Transform3d, PhysicsRefs } from '../code-gen/core.js';
43
47
 
44
48
  @ECSSystem()
45
49
  export class SpawnSystem implements IECSSystem {
@@ -47,6 +51,7 @@ export class SpawnSystem implements IECSSystem {
47
51
  private readonly _physics: PhysicsWorldManager3d,
48
52
  private readonly _entities: EntitiesManager,
49
53
  private readonly _transform: Transform3d,
54
+ private readonly _physicsRefs: PhysicsRefs,
50
55
  ) {}
51
56
 
52
57
  update(tick: number): void {
@@ -54,50 +59,74 @@ export class SpawnSystem implements IECSSystem {
54
59
  this._entities.addComponent(entity, Transform3d);
55
60
  this._entities.addComponent(entity, PhysicsRefs);
56
61
 
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
- });
62
+ // Set initial position in ECS (including prev for interpolation)
63
+ const t = this._transform.unsafe;
64
+ t.positionX[entity] = 0;
65
+ t.positionY[entity] = 5;
66
+ t.positionZ[entity] = 0;
67
+ t.rotationX[entity] = 0;
68
+ t.rotationY[entity] = 0;
69
+ t.rotationZ[entity] = 0;
70
+ t.rotationW[entity] = 1;
71
+ t.prevPositionX[entity] = 0;
72
+ t.prevPositionY[entity] = 5;
73
+ t.prevPositionZ[entity] = 0;
74
+ t.prevRotationX[entity] = 0;
75
+ t.prevRotationY[entity] = 0;
76
+ t.prevRotationZ[entity] = 0;
77
+ t.prevRotationW[entity] = 1;
78
+
79
+ // Create a dynamic Rapier body and configure it
80
+ const body = this._physics.createDynamicBody();
81
+ body.setTranslation({ x: 0, y: 5, z: 0 }, true);
82
+ body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
83
+
84
+ // Create a ball collider attached to the body
85
+ const groups = CollisionLayers.get('player');
86
+ const collider = this._physics.createBallCollider(0.5, body, groups);
87
+
88
+ // Store handles in ECS for later lookup
89
+ const pr = this._physicsRefs.unsafe;
90
+ pr.bodyHandle[entity] = body.handle;
91
+ pr.colliderHandle[entity] = collider.handle;
92
+ pr.bodyType[entity] = BodyType.DYNAMIC;
93
+ pr.collisionLayer[entity] = groups;
94
+
95
+ // Register collider→entity mapping (used by collision events)
96
+ this._physics.registerCollider(collider.handle, entity);
77
97
  }
78
98
  }
79
99
  ```
80
100
 
81
101
  ### Collider Shapes (3D)
82
102
 
103
+ All collider factories take an optional `parent` body, `groups` (collision groups), and `activeEvents` bitmask:
104
+
83
105
  ```typescript
84
106
  // Sphere
85
- { type: 'ball', radius: 0.5 }
107
+ this._physics.createBallCollider(radius, parent, groups, activeEvents);
86
108
 
87
- // Box
88
- { type: 'cuboid', hx: 1, hy: 0.5, hz: 1 } // half-extents
109
+ // Box (half-extents, 3 dimensions)
110
+ this._physics.createCuboidCollider(hx, hy, hz, parent, groups, activeEvents);
89
111
 
90
112
  // Capsule
91
- { type: 'capsule', halfHeight: 0.5, radius: 0.3 }
113
+ this._physics.createCapsuleCollider(halfHeight, radius, parent, groups, activeEvents);
114
+
115
+ // Cylinder (3D only)
116
+ this._physics.createCylinderCollider(halfHeight, radius, parent, groups, activeEvents);
92
117
 
93
- // Cylinder
94
- { type: 'cylinder', halfHeight: 1.0, radius: 0.5 }
118
+ // Cone (3D only)
119
+ this._physics.createConeCollider(halfHeight, radius, parent, groups, activeEvents);
95
120
 
96
- // Convex hull
97
- { type: 'convexHull', points: Float32Array }
121
+ // Convex hull (returns null if hull computation fails)
122
+ this._physics.createConvexHullCollider(new Float32Array([...]), parent, groups, activeEvents);
98
123
 
99
- // Triangle mesh (static only)
100
- { type: 'trimesh', vertices: Float32Array, indices: Uint32Array }
124
+ // Triangle mesh (static geometry only)
125
+ this._physics.createTrimeshCollider(vertices, indices, parent, groups, activeEvents);
126
+
127
+ // Custom collider from a Rapier ColliderDesc
128
+ const desc = this._physics.rapier.ColliderDesc.ball(0.5).setDensity(2.0).setFriction(0.5);
129
+ this._physics.createColliderFromDesc(desc, parent);
101
130
  ```
102
131
 
103
132
  ## Collision Layers and Events
@@ -281,11 +310,9 @@ export const systems = [
281
310
  On rollback:
282
311
  1. ArrayBuffer is restored → ECS state reverts
283
312
  2. Rapier 3D world snapshot is restored → physics state reverts
284
- 3. `updateSceneQueries()` is called → QueryPipeline is rebuilt
313
+ 3. `ColliderEntityMap` is rebuilt automatically
285
314
  4. KCC controllers are recreated via `recreateAll()`
286
315
 
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
316
  ## State Transfer
290
317
 
291
318
  After `applyExternalState()`:
@@ -360,3 +360,63 @@ export class ProjectileLifetimeSystem implements IECSSystem {
360
360
  this._arenaConfig.radius -= this._arenaConfig.shrinkRate;
361
361
  }
362
362
  ```
363
+ <% if (simulationType === 'physics2d') { -%>
364
+
365
+ ## Add Procedural 2D Map
366
+
367
+ 1. **Install packages:**
368
+ ```bash
369
+ pnpm add @lagless/2d-map-generator @lagless/2d-map-renderer
370
+ ```
371
+
372
+ 2. **Define object types** in `simulation/src/lib/map-config/objects.ts`:
373
+ ```typescript
374
+ import type { MapObjectDef, MapObjectRegistry } from '@lagless/2d-map-generator';
375
+ import { RenderLayer, ShapeType, CANOPY_SENSOR_TAG } from '@lagless/2d-map-generator';
376
+
377
+ export enum ObjectType { Tree = 0 }
378
+
379
+ const TREE: MapObjectDef = {
380
+ typeId: ObjectType.Tree,
381
+ colliders: [
382
+ { shape: { type: ShapeType.Circle, radius: 30 } },
383
+ { shape: { type: ShapeType.Circle, radius: 128 }, isSensor: true, tag: CANOPY_SENSOR_TAG },
384
+ ],
385
+ visuals: [
386
+ { texture: 'tree-trunk', layer: RenderLayer.Ground },
387
+ { texture: 'tree-foliage', layer: RenderLayer.Canopy },
388
+ ],
389
+ scaleRange: [0.1, 0.2],
390
+ includeSensorsInBounds: true,
391
+ };
392
+
393
+ export const OBJECT_REGISTRY: MapObjectRegistry = new Map([[0, TREE]]);
394
+ ```
395
+
396
+ 3. **Create generator factory** in `simulation/src/lib/map-config/create-map-generator.ts`:
397
+ ```typescript
398
+ import { MapGenerator, BiomeFeature, ShoreFeature, GrassFeature,
399
+ ObjectPlacementFeature, PlacementKind, TerrainZone, STANDARD_BIOME,
400
+ } from '@lagless/2d-map-generator';
401
+ import { OBJECT_REGISTRY, ObjectType } from './objects.js';
402
+
403
+ export function createMapGenerator(): MapGenerator {
404
+ return new MapGenerator({ baseWidth: 720, baseHeight: 720, scale: 1, extension: 80, gridSize: 16 })
405
+ .addFeature(new BiomeFeature(), STANDARD_BIOME)
406
+ .addFeature(new ShoreFeature(), { inset: 48, divisions: 12, variation: 4 })
407
+ .addFeature(new GrassFeature(), { inset: 18, variation: 3 })
408
+ .addFeature(new ObjectPlacementFeature(), {
409
+ registry: OBJECT_REGISTRY,
410
+ stages: [{ kind: PlacementKind.Density, typeId: ObjectType.Tree, density: 100, terrainZone: TerrainZone.Grass }],
411
+ });
412
+ }
413
+ ```
414
+
415
+ 4. **Extend runner** — generate map + create physics colliders in constructor, call `capturePreStartState()` after.
416
+
417
+ 5. **Render** — use `MapTerrainRenderer` and `MapObjectRenderer` from `@lagless/2d-map-renderer`.
418
+
419
+ 6. **Canopy transparency** — use `extractCanopyZones()` + `isInsideCanopyZone()` per frame.
420
+
421
+ > Full details: [docs/11-2d-map-generation.md](11-2d-map-generation.md)
422
+ <% } -%>
@@ -178,17 +178,12 @@ this._transform.unsafe.positionX[entity] = newX; // Physics will overwrite!
178
178
  ```
179
179
  **Correct:**
180
180
  ```typescript
181
- this._physics.setLinearVelocity(entity, { x: vx, y: vy });
181
+ const body = this._physics.getBody(this._physicsRefs.unsafe.bodyHandle[entity]);
182
+ body.setLinvel({ x: vx, y: vy }, true);
182
183
  // or
183
- this._physics.applyImpulse(entity, { x: fx, y: fy });
184
+ body.applyImpulse({ x: fx, y: fy }, true);
184
185
  ```
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.
186
+ **Why:** PhysicsStep syncs Rapier→ECS, overwriting manual position changes. Move dynamic bodies via forces/velocity on the Rapier body.
192
187
 
193
188
  ## Multiplayer
194
189