@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,256 @@
1
+ # Rendering
2
+
3
+ ## Architecture
4
+
5
+ Rendering is **non-deterministic** and **read-only**. The view layer reads ECS state but never writes to it. Simulation runs independently of rendering.
6
+
7
+ ```
8
+ Simulation (deterministic) View Layer (non-deterministic)
9
+ ECS systems → ArrayBuffer ──→ FilterViews → Pixi.js sprites
10
+ Tick N state Interpolated between ticks
11
+ ```
12
+
13
+ ## FilterViews — Entity Lifecycle Management
14
+
15
+ `FilterViews` from `@lagless/pixi-react` automatically manages Pixi.js containers for entities matching a filter. When an entity enters the filter, a view is created. When it leaves, the view is destroyed.
16
+
17
+ ```tsx
18
+ import { FilterViews } from '@lagless/pixi-react';
19
+
20
+ // In game scene:
21
+ <FilterViews
22
+ filter={runner.Core.PlayerFilter}
23
+ View={PlayerView}
24
+ />
25
+ ```
26
+
27
+ ## filterView — Define Entity Views
28
+
29
+ `filterView` creates a view component with lifecycle hooks:
30
+
31
+ ```tsx
32
+ import { filterView } from '@lagless/pixi-react';
33
+
34
+ const PlayerView = filterView(
35
+ // Render function — called once on entity creation
36
+ ({ entity, runner }, ref) => {
37
+ return (
38
+ <pixiContainer ref={ref}>
39
+ <pixiGraphics
40
+ draw={(g) => {
41
+ g.circle(0, 0, 20);
42
+ g.fill({ color: 0x4488ff });
43
+ }}
44
+ />
45
+ </pixiContainer>
46
+ );
47
+ },
48
+ {
49
+ // Called every render frame — update positions
50
+ onUpdate: ({ entity, runner }, container) => {
51
+ const transform = runner.Core.Transform2d;
52
+ const sim = runner.Simulation;
53
+
54
+ // Use interpolation for smooth rendering between ticks
55
+ const factor = sim.interpolationFactor;
56
+ const prevX = transform.unsafe.prevPositionX[entity];
57
+ const prevY = transform.unsafe.prevPositionY[entity];
58
+ const currX = transform.unsafe.positionX[entity];
59
+ const currY = transform.unsafe.positionY[entity];
60
+
61
+ container.position.set(
62
+ prevX + (currX - prevX) * factor,
63
+ prevY + (currY - prevY) * factor,
64
+ );
65
+ },
66
+ },
67
+ );
68
+ ```
69
+
70
+ ### filterView Lifecycle
71
+
72
+ | Hook | When | Use For |
73
+ |------|------|---------|
74
+ | Render function | Entity enters filter | Create sprites, set up initial state |
75
+ | `onUpdate` | Every render frame | Update position, rotation, animation |
76
+ | `onDestroy` | Entity leaves filter | Cleanup (optional, auto-handled) |
77
+
78
+ ## VisualSmoother2d — Rollback Smoothing
79
+
80
+ `VisualSmoother2d` handles both simulation↔render interpolation AND rollback lag smoothing. Without it, entities teleport when a rollback changes their position.
81
+
82
+ ```typescript
83
+ import { VisualSmoother2d } from '@lagless/misc';
84
+
85
+ // Create one per entity (e.g., in filterView render):
86
+ const smoother = new VisualSmoother2d();
87
+
88
+ // In onUpdate:
89
+ onUpdate: ({ entity, runner }, container) => {
90
+ const transform = runner.Core.Transform2d;
91
+ const sim = runner.Simulation;
92
+
93
+ const smoothed = smoother.update(
94
+ transform.unsafe.prevPositionX[entity],
95
+ transform.unsafe.prevPositionY[entity],
96
+ transform.unsafe.positionX[entity],
97
+ transform.unsafe.positionY[entity],
98
+ sim.interpolationFactor,
99
+ sim.clock.deltaTime,
100
+ );
101
+
102
+ container.position.set(smoothed.x, smoothed.y);
103
+ }
104
+ ```
105
+
106
+ ### How VisualSmoother2d Works
107
+
108
+ 1. **Normal tick:** Interpolates between prevPosition and position using interpolationFactor
109
+ 2. **After rollback:** Detects position jump, absorbs it into an offset
110
+ 3. **Decay:** Offset decays exponentially (halfLife=200ms) — entity slides smoothly to correct position
111
+ 4. **No feedback loop:** Stores raw sim position (not smoothed) for next-frame comparison
112
+
113
+ ## Pixi.js Setup
114
+
115
+ The template uses Pixi.js 8 with `@pixi/react` for declarative rendering.
116
+
117
+ ```tsx
118
+ // In main.tsx:
119
+ import '@abraham/reflection';
120
+ import { extend } from '@pixi/react';
121
+ import { Container, Graphics, Sprite, Text, Application } from 'pixi.js';
122
+
123
+ extend({ Container, Graphics, Sprite, Text, Application });
124
+ ```
125
+
126
+ ### RunnerTicker
127
+
128
+ The `RunnerTicker` component connects the simulation loop to Pixi.js's render loop:
129
+
130
+ ```tsx
131
+ import { RunnerTicker } from '@lagless/pixi-react';
132
+
133
+ // Inside <Application>:
134
+ <RunnerTicker runner={runner} />
135
+ ```
136
+
137
+ This calls `runner.update(deltaTime)` every frame, advancing the simulation.
138
+
139
+ ## Adding New Entity Visuals
140
+
141
+ ### Step 1: Define Component and Filter
142
+
143
+ ```yaml
144
+ # ecs.yaml
145
+ components:
146
+ Projectile:
147
+ ownerSlot: uint8
148
+ radius: float32
149
+
150
+ filters:
151
+ ProjectileFilter:
152
+ include: [Transform2d, Projectile]
153
+ ```
154
+
155
+ ### Step 2: Run Codegen
156
+
157
+ ```bash
158
+ pnpm codegen
159
+ ```
160
+
161
+ ### Step 3: Create View Component
162
+
163
+ ```tsx
164
+ // game-view/projectile-view.tsx
165
+ import { filterView } from '@lagless/pixi-react';
166
+ import { VisualSmoother2d } from '@lagless/misc';
167
+
168
+ const ProjectileView = filterView(
169
+ ({ entity, runner }, ref) => {
170
+ const smoother = new VisualSmoother2d();
171
+ const radius = runner.Core.Projectile.unsafe.radius[entity];
172
+
173
+ return (
174
+ <pixiContainer ref={ref} userData={{ smoother }}>
175
+ <pixiGraphics
176
+ draw={(g) => {
177
+ g.circle(0, 0, radius);
178
+ g.fill({ color: 0xff4444 });
179
+ }}
180
+ />
181
+ </pixiContainer>
182
+ );
183
+ },
184
+ {
185
+ onUpdate: ({ entity, runner }, container) => {
186
+ const smoother = container.userData.smoother as VisualSmoother2d;
187
+ const t = runner.Core.Transform2d;
188
+ const sim = runner.Simulation;
189
+
190
+ const pos = smoother.update(
191
+ t.unsafe.prevPositionX[entity], t.unsafe.prevPositionY[entity],
192
+ t.unsafe.positionX[entity], t.unsafe.positionY[entity],
193
+ sim.interpolationFactor, sim.clock.deltaTime,
194
+ );
195
+ container.position.set(pos.x, pos.y);
196
+ },
197
+ },
198
+ );
199
+
200
+ export { ProjectileView };
201
+ ```
202
+
203
+ ### Step 4: Add to Game Scene
204
+
205
+ ```tsx
206
+ // game-view/game-scene.tsx
207
+ <FilterViews filter={runner.Core.ProjectileFilter} View={ProjectileView} />
208
+ ```
209
+
210
+ ## Performance Tips
211
+
212
+ - **Use unsafe arrays in `onUpdate`** — avoid `getCursor()` in render loops
213
+ - **Minimize React re-renders** — FilterViews handles lifecycle, don't use React state for positions
214
+ - **Batch draw calls** — use `pixiGraphics.draw()` callback, not imperative calls every frame
215
+ - **Object pooling** — FilterViews already pools containers; entity recycling handles the rest
216
+ - **Avoid allocations** — use `VECTOR2_BUFFER_1..10` scratch vectors in calculation-heavy render code
217
+
218
+ ## Screen Management
219
+
220
+ The template uses React Router for screen navigation:
221
+
222
+ ```tsx
223
+ // router.tsx
224
+ <Routes>
225
+ <Route path="/" element={<TitleScreen />} />
226
+ <Route path="/game" element={<GameScreen />} />
227
+ </Routes>
228
+ ```
229
+
230
+ ### Title Screen → Game Screen
231
+
232
+ ```tsx
233
+ // screens/title.screen.tsx
234
+ const navigate = useNavigate();
235
+ const startMatch = useStartMatch(); // or useStartMultiplayerMatch
236
+
237
+ const handleStart = () => {
238
+ startMatch(); // sets up runner, navigates to /game
239
+ };
240
+ ```
241
+
242
+ ## Debug Panel
243
+
244
+ The `<DebugPanel>` from `@lagless/react` shows network stats in development:
245
+
246
+ ```tsx
247
+ import { DebugPanel } from '@lagless/react';
248
+
249
+ <DebugPanel
250
+ runner={runner}
251
+ hashVerification={true} // show hash table
252
+ // Toggle with F3 key
253
+ />
254
+ ```
255
+
256
+ Shows: RTT, jitter, input delay, nudger offset, tick, rollback count, FPS, hash verification table, disconnect/reconnect buttons.
@@ -0,0 +1,277 @@
1
+ # Multiplayer
2
+
3
+ ## Architecture
4
+
5
+ Lagless uses a **relay model**: the server relays inputs between clients but does NOT run the simulation. Clients run identical deterministic simulations independently. The server is authoritative on time (tick assignment) and input acceptance, but clients are authoritative on game state.
6
+
7
+ ```
8
+ Client A ──→ Server (relay) ──→ Client B
9
+ ↓ ↓ ↓
10
+ Simulation No simulation Simulation
11
+ (same inputs → same state)
12
+ ```
13
+
14
+ ## Input Providers
15
+
16
+ The input provider determines how RPCs are processed:
17
+
18
+ | Provider | Use Case | Rollback | verifiedTick |
19
+ |----------|---------|---------|-------------|
20
+ | `LocalInputProvider` | Single-player | None | `= tick` |
21
+ | `ReplayInputProvider` | Recorded playback | None | `= tick` |
22
+ | `RelayInputProvider` | Multiplayer | On remote inputs | `= maxServerTick - 1` |
23
+
24
+ ### Single-Player Setup
25
+
26
+ ```typescript
27
+ import { LocalInputProvider } from '@lagless/core';
28
+
29
+ const inputProvider = new LocalInputProvider(inputRegistry);
30
+ const runner = new MyRunner(arenaConfig, inputProvider);
31
+ ```
32
+
33
+ ### Multiplayer Setup
34
+
35
+ ```typescript
36
+ import { RelayInputProvider } from '@lagless/relay-client';
37
+
38
+ const connection = new RelayConnection({
39
+ serverUrl: 'ws://localhost:3333',
40
+ scope: 'my-game',
41
+ });
42
+
43
+ const inputProvider = new RelayInputProvider(inputRegistry, connection);
44
+ const runner = new MyRunner(arenaConfig, inputProvider);
45
+
46
+ // Connect and matchmake
47
+ await connection.connect();
48
+ ```
49
+
50
+ ## RelayConnection
51
+
52
+ Manages WebSocket connection to the relay server. Handles matchmaking, reconnection, and message routing.
53
+
54
+ ```typescript
55
+ const connection = new RelayConnection({
56
+ serverUrl: 'ws://localhost:3333', // server URL
57
+ scope: 'my-game', // matchmaking scope (game type)
58
+ });
59
+
60
+ // Connect
61
+ await connection.connect();
62
+
63
+ // Events
64
+ connection.onMatchFound(() => { ... });
65
+ connection.onDisconnect(() => { ... });
66
+
67
+ // Disconnect
68
+ connection.disconnect();
69
+ ```
70
+
71
+ ## Server Setup
72
+
73
+ ### RelayGameServer
74
+
75
+ ```typescript
76
+ import { RelayGameServer } from '@lagless/relay-game-server';
77
+ import { hooks } from './game-hooks.js';
78
+ import { MyInputRegistry } from '@my-game/simulation';
79
+
80
+ const server = new RelayGameServer({
81
+ port: 3333,
82
+ loggerName: 'MyGameServer',
83
+ roomType: {
84
+ name: 'my-game',
85
+ config: {
86
+ maxPlayers: 4,
87
+ reconnectTimeoutMs: 15_000,
88
+ },
89
+ hooks,
90
+ inputRegistry: MyInputRegistry,
91
+ },
92
+ matchmaking: {
93
+ scope: 'my-game',
94
+ config: {
95
+ minPlayersToStart: 1,
96
+ maxPlayers: 4,
97
+ waitTimeoutMs: 5_000,
98
+ },
99
+ },
100
+ });
101
+
102
+ server.start();
103
+ ```
104
+
105
+ ### Dev Tools Integration
106
+
107
+ For development, add dev-tools for the dev-player testing tool:
108
+
109
+ ```typescript
110
+ import { setupDevTools } from '@lagless/dev-tools';
111
+
112
+ setupDevTools(server); // Register latency API routes
113
+ server.start();
114
+ ```
115
+
116
+ ## Room Hooks
117
+
118
+ Room hooks define server-side game lifecycle. The server calls these hooks at appropriate times.
119
+
120
+ ```typescript
121
+ import { RoomHooks, PlayerLeaveReason } from '@lagless/relay-server';
122
+
123
+ export const hooks: RoomHooks = {
124
+ // Room created (before any players join)
125
+ onRoomCreated: (ctx) => {
126
+ console.log('Room created:', ctx.roomId);
127
+ },
128
+
129
+ // Player joins the room
130
+ onPlayerJoin: (ctx, player) => {
131
+ ctx.emitServerEvent(PlayerJoined, {
132
+ slot: player.slot,
133
+ playerId: player.id,
134
+ });
135
+ },
136
+
137
+ // Player leaves (disconnect, kick, etc.)
138
+ onPlayerLeave: (ctx, player, reason) => {
139
+ ctx.emitServerEvent(PlayerLeft, {
140
+ slot: player.slot,
141
+ reason,
142
+ });
143
+ },
144
+
145
+ // Player reconnects after disconnect
146
+ onPlayerReconnect: (ctx, player) => {
147
+ ctx.emitServerEvent(PlayerJoined, {
148
+ slot: player.slot,
149
+ playerId: player.id,
150
+ });
151
+ },
152
+
153
+ // Whether to accept a late-joining player (room already started)
154
+ shouldAcceptLateJoin: (ctx, player) => {
155
+ return true; // or false to reject
156
+ },
157
+
158
+ // Whether to accept a reconnecting player
159
+ shouldAcceptReconnect: (ctx, player) => {
160
+ return true; // or false to reject
161
+ },
162
+
163
+ // Player reports finished (e.g., game over for them)
164
+ onPlayerFinished: (ctx, player, result) => {
165
+ // result is game-specific data
166
+ },
167
+
168
+ // Match ends (all players finished or room timeout)
169
+ onMatchEnd: (ctx, results) => {
170
+ // Persist results to database, etc.
171
+ },
172
+
173
+ // Room is being disposed
174
+ onRoomDisposed: (ctx) => {
175
+ console.log('Room disposed:', ctx.roomId);
176
+ },
177
+ };
178
+ ```
179
+
180
+ ### RoomContext (ctx)
181
+
182
+ The `ctx` parameter provides safe room interaction:
183
+
184
+ ```typescript
185
+ ctx.emitServerEvent(InputClass, data); // Send server-originated RPC
186
+ ctx.getPlayers(); // Get all player info
187
+ ctx.endMatch(results); // End the match
188
+ ctx.roomId; // Room identifier
189
+ ```
190
+
191
+ ## Server Events via emitServerEvent
192
+
193
+ Server events are RPCs that originate from the server, not from players. They're used for authoritative game events.
194
+
195
+ ```typescript
196
+ // In hooks:
197
+ ctx.emitServerEvent(PlayerJoined, { slot: player.slot, playerId: player.id });
198
+ ctx.emitServerEvent(PlayerLeft, { slot: player.slot, reason: 0 });
199
+
200
+ // Custom server events:
201
+ ctx.emitServerEvent(RoundStart, { roundNumber: 1 });
202
+ ctx.emitServerEvent(PowerUpSpawned, { x: 100, y: 200, type: 3 });
203
+ ```
204
+
205
+ Server events have `playerSlot = 255` (SERVER_SLOT) in the RPC metadata.
206
+
207
+ ## State Transfer (Late Join)
208
+
209
+ When a player joins a room that's already running:
210
+
211
+ 1. Server sends `StateRequest` to all connected clients
212
+ 2. Clients export `ArrayBuffer.slice(0)` snapshot + hash + tick
213
+ 3. `StateTransfer` picks majority hash (quorum) — protects against corrupted clients
214
+ 4. Server sends chosen `StateResponse` to joining player
215
+ 5. Server sends **only** events with `tick > stateResult.tick` (events baked into state are not re-sent)
216
+ 6. Client applies state via `ECSSimulation.applyExternalState()` — replaces ArrayBuffer, resets clock + snapshots
217
+
218
+ ### What You Need to Do
219
+
220
+ State transfer works automatically with the framework. However:
221
+ - Ensure all game state is in the ArrayBuffer (components, singletons, player resources)
222
+ - Don't keep simulation state in JavaScript variables or closures
223
+ - Physics: `ColliderEntityMap` must be rebuilt after state transfer (handled by physics runner)
224
+
225
+ ## Reconnect
226
+
227
+ When a player disconnects:
228
+
229
+ 1. Server tracks `PlayerConnection` as `Disconnected` with configurable timeout (`reconnectTimeoutMs`)
230
+ 2. If player reconnects before timeout: state transfer restores their simulation
231
+ 3. If timeout expires: `onPlayerLeave` is called with `TIMEOUT` reason
232
+ 4. `shouldAcceptReconnect` hook can reject reconnection
233
+
234
+ ### Testing Reconnect
235
+
236
+ 1. Open F3 debug panel in game
237
+ 2. Click "Disconnect" button
238
+ 3. Wait a few seconds
239
+ 4. Click "Reconnect" button
240
+ 5. Verify: player resumes with correct state, no permanent desync
241
+
242
+ ## Testing Multiplayer
243
+
244
+ ### Quick Test: Two Browser Tabs
245
+
246
+ 1. Start server: `pnpm dev:backend`
247
+ 2. Start client: `pnpm dev:frontend`
248
+ 3. Open `http://localhost:4200` in two tabs
249
+ 4. Both click "Multiplayer" → they should see each other
250
+
251
+ ### Dev Player Tool
252
+
253
+ The dev-player opens N game instances in an iframe grid with auto-matchmaking:
254
+
255
+ 1. Start everything: `pnpm dev`
256
+ 2. Open `http://localhost:4210`
257
+ 3. Set instance count (2-8)
258
+ 4. Click "Start" — instances auto-matchmake with unique scope
259
+
260
+ Features:
261
+ - Per-instance stats (FPS, tick, rollbacks)
262
+ - Hash timeline for divergence detection
263
+ - Per-player latency sliders
264
+ - Auto-match on load
265
+
266
+ ### Debug Panel (F3)
267
+
268
+ Press F3 in-game to toggle the debug panel:
269
+ - **RTT** — round-trip time to server
270
+ - **Jitter** — RTT variance
271
+ - **Input Delay** — ticks ahead inputs are scheduled
272
+ - **Nudger** — clock offset correction
273
+ - **Tick** — current simulation tick
274
+ - **Rollbacks** — count of rollbacks
275
+ - **FPS** — render frame rate
276
+ - **Hash Table** — state hash comparison (red = divergence)
277
+ - **Disconnect/Reconnect** — buttons for testing