create-airjam 0.1.0 → 0.1.2

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 (64) hide show
  1. package/dist/index.js +11 -3
  2. package/package.json +6 -3
  3. package/templates/pong/.env.example +11 -0
  4. package/templates/pong/.env.local +10 -0
  5. package/templates/pong/AI_INSTRUCTIONS.md +44 -0
  6. package/templates/pong/README.md +111 -0
  7. package/templates/pong/airjam-docs/getting-started/architecture/page.md +165 -0
  8. package/templates/pong/airjam-docs/getting-started/game-ideas/page.md +114 -0
  9. package/templates/pong/airjam-docs/getting-started/introduction/page.md +122 -0
  10. package/templates/pong/airjam-docs/how-it-works/host-system/page.md +241 -0
  11. package/templates/pong/airjam-docs/sdk/hooks/page.md +403 -0
  12. package/templates/pong/airjam-docs/sdk/input-system/page.md +336 -0
  13. package/templates/pong/airjam-docs/sdk/networked-state/page.md +575 -0
  14. package/templates/pong/dist/assets/index-B9l0NKly.js +269 -0
  15. package/templates/pong/dist/assets/index-CHKqdIQG.css +1 -0
  16. package/templates/pong/dist/index.html +14 -0
  17. package/templates/pong/eslint.config.js +33 -0
  18. package/templates/pong/index.html +6 -1
  19. package/templates/pong/node_modules/.bin/air-jam-server +17 -0
  20. package/templates/pong/node_modules/.bin/eslint +17 -0
  21. package/templates/pong/node_modules/.bin/eslint-config-prettier +17 -0
  22. package/templates/pong/node_modules/.bin/jiti +17 -0
  23. package/templates/pong/node_modules/.bin/tsc +17 -0
  24. package/templates/pong/node_modules/.bin/tsserver +17 -0
  25. package/templates/pong/node_modules/.bin/tsx +17 -0
  26. package/templates/pong/node_modules/.bin/vite +17 -0
  27. package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js +66143 -0
  28. package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js.map +7 -0
  29. package/templates/pong/node_modules/.vite/deps/_metadata.json +73 -0
  30. package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js +292 -0
  31. package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js.map +7 -0
  32. package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js +38 -0
  33. package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
  34. package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js +280 -0
  35. package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js.map +7 -0
  36. package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js +13810 -0
  37. package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js.map +7 -0
  38. package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js +1004 -0
  39. package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js.map +7 -0
  40. package/templates/pong/node_modules/.vite/deps/package.json +3 -0
  41. package/templates/pong/node_modules/.vite/deps/react-dom.js +6 -0
  42. package/templates/pong/node_modules/.vite/deps/react-dom.js.map +7 -0
  43. package/templates/pong/node_modules/.vite/deps/react-dom_client.js +20217 -0
  44. package/templates/pong/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  45. package/templates/pong/node_modules/.vite/deps/react-router-dom.js +13900 -0
  46. package/templates/pong/node_modules/.vite/deps/react-router-dom.js.map +7 -0
  47. package/templates/pong/node_modules/.vite/deps/react.js +5 -0
  48. package/templates/pong/node_modules/.vite/deps/react.js.map +7 -0
  49. package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  50. package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  51. package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  52. package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  53. package/templates/pong/node_modules/.vite/deps/zod.js +476 -0
  54. package/templates/pong/node_modules/.vite/deps/zod.js.map +7 -0
  55. package/templates/pong/package.json +12 -1
  56. package/templates/pong/src/App.tsx +2 -2
  57. package/templates/pong/src/controller-view.tsx +143 -0
  58. package/templates/pong/src/host-view.tsx +401 -0
  59. package/templates/pong/src/main.tsx +2 -1
  60. package/templates/pong/src/store.ts +80 -0
  61. package/templates/pong/tsconfig.json +3 -2
  62. package/templates/pong/vite.config.ts +3 -0
  63. package/templates/pong/src/ControllerView.tsx +0 -64
  64. package/templates/pong/src/HostView.tsx +0 -148
@@ -0,0 +1,575 @@
1
+ # Networked State
2
+
3
+ The Air Jam SDK provides a powerful networked state system built on Zustand that automatically syncs game state between the host and all controllers. This page explains how it works and how to use it effectively.
4
+
5
+ ## Overview
6
+
7
+ The networked state system enables:
8
+
9
+ - **Shared State** - Game state (scores, phases, team assignments) synced across all clients
10
+ - **Action RPCs** - Controllers can trigger actions on the host (source of truth)
11
+ - **Automatic Sync** - State changes on host automatically broadcast to all controllers
12
+ - **Type Safety** - Full TypeScript support with Zustand selectors
13
+
14
+ ## Creating a Networked Store
15
+
16
+ Use `createAirJamStore` instead of Zustand's `create`:
17
+
18
+ ```tsx filename="src/store.ts"
19
+ import { createAirJamStore } from "@air-jam/sdk";
20
+
21
+ export interface GameState {
22
+ phase: "lobby" | "playing" | "gameover";
23
+ scores: { team1: number; team2: number };
24
+ teamAssignments: Record<string, "team1" | "team2">;
25
+
26
+ // Actions must be in an 'actions' object
27
+ actions: {
28
+ joinTeam: (team: "team1" | "team2", playerId?: string) => void;
29
+ setPhase: (phase: "lobby" | "playing" | "gameover") => void;
30
+ scorePoint: (team: "team1" | "team2") => void;
31
+ resetGame: () => void;
32
+ };
33
+ }
34
+
35
+ export const useGameStore = createAirJamStore<GameState>((set) => ({
36
+ phase: "lobby",
37
+ scores: { team1: 0, team2: 0 },
38
+ teamAssignments: {},
39
+
40
+ actions: {
41
+ // playerId is automatically injected by SDK on host side
42
+ joinTeam: (team, playerId) => {
43
+ if (!playerId) return;
44
+ set((state) => ({
45
+ teamAssignments: {
46
+ ...state.teamAssignments,
47
+ [playerId]: team,
48
+ },
49
+ }));
50
+ },
51
+
52
+ setPhase: (phase) => set({ phase }),
53
+
54
+ scorePoint: (team) =>
55
+ set((state) => ({
56
+ scores: {
57
+ ...state.scores,
58
+ [team]: state.scores[team] + 1,
59
+ },
60
+ })),
61
+
62
+ resetGame: () =>
63
+ set({
64
+ phase: "lobby",
65
+ scores: { team1: 0, team2: 0 },
66
+ teamAssignments: {},
67
+ }),
68
+ },
69
+ }));
70
+ ```
71
+
72
+ **Important Requirements:**
73
+
74
+ 1. **State must have an `actions` object** - All action functions must be nested under `actions`
75
+ 2. **Actions receive `controllerId` automatically** - The SDK injects the controller ID as the last argument on the host side
76
+ 3. **Use Zustand's `set` function** - Standard Zustand patterns work as expected
77
+
78
+ ## Using the Store
79
+
80
+ ### Reading State
81
+
82
+ Use standard Zustand selectors to read state:
83
+
84
+ ```tsx filename="src/components/HostView.tsx"
85
+ import { useGameStore } from "../store";
86
+
87
+ const HostView = () => {
88
+ // Select specific fields (recommended - minimizes re-renders)
89
+ const phase = useGameStore((state) => state.phase);
90
+ const scores = useGameStore((state) => state.scores);
91
+ const teamAssignments = useGameStore((state) => state.teamAssignments);
92
+
93
+ // Or select the entire state
94
+ const state = useGameStore((state) => state);
95
+
96
+ return (
97
+ <div>
98
+ <p>Phase: {phase}</p>
99
+ <p>
100
+ Team 1: {scores.team1} | Team 2: {scores.team2}
101
+ </p>
102
+ </div>
103
+ );
104
+ };
105
+ ```
106
+
107
+ **On Controllers:**
108
+
109
+ ```tsx filename="src/components/ControllerView.tsx"
110
+ import { useGameStore } from "../store";
111
+
112
+ const ControllerView = () => {
113
+ const phase = useGameStore((state) => state.phase);
114
+ const teamAssignments = useGameStore((state) => state.teamAssignments);
115
+ const actions = useGameStore((state) => state.actions);
116
+
117
+ const myTeam = teamAssignments[controllerId];
118
+
119
+ return (
120
+ <div>
121
+ {phase === "lobby" && (
122
+ <button onClick={() => actions.joinTeam("team1")}>Join Team 1</button>
123
+ )}
124
+ </div>
125
+ );
126
+ };
127
+ ```
128
+
129
+ ### Calling Actions
130
+
131
+ **On Host:**
132
+
133
+ Actions execute directly on the host:
134
+
135
+ ```tsx filename="src/components/HostView.tsx"
136
+ const HostView = () => {
137
+ const actions = useGameStore((state) => state.actions);
138
+
139
+ const handleGameStart = () => {
140
+ // Executes immediately on host
141
+ actions.setPhase("playing");
142
+ };
143
+
144
+ const handleScore = (team: "team1" | "team2") => {
145
+ // Executes immediately on host
146
+ actions.scorePoint(team);
147
+ };
148
+ };
149
+ ```
150
+
151
+ **On Controllers:**
152
+
153
+ Actions are automatically proxied to the host:
154
+
155
+ ```tsx filename="src/components/ControllerView.tsx"
156
+ const ControllerView = () => {
157
+ const actions = useGameStore((state) => state.actions);
158
+
159
+ const handleJoinTeam = (team: "team1" | "team2") => {
160
+ // Sends RPC to host → host executes → state syncs back
161
+ actions.joinTeam(team);
162
+ // Note: controllerId is automatically injected by SDK
163
+ };
164
+ };
165
+ ```
166
+
167
+ **What Happens:**
168
+
169
+ 1. Controller calls `actions.joinTeam("team1")`
170
+ 2. SDK intercepts and sends `controller:action_rpc` to server
171
+ 3. Server forwards to host as `airjam:action_rpc`
172
+ 4. Host executes action: `joinTeam("team1", controllerId)`
173
+ 5. Host state updates
174
+ 6. Host broadcasts state change to all controllers
175
+ 7. All controllers receive updated state
176
+
177
+ ### Accessing Actions
178
+
179
+ You can access actions in two ways:
180
+
181
+ ```tsx
182
+ // Method 1: Select just actions
183
+ const actions = useGameStore((state) => state.actions);
184
+ actions.joinTeam("team1");
185
+
186
+ // Method 2: Select full state
187
+ const state = useGameStore((state) => state);
188
+ state.actions.joinTeam("team1");
189
+ ```
190
+
191
+ Both work identically - the SDK handles proxy creation for both selector patterns.
192
+
193
+ ## How It Works
194
+
195
+ ### Architecture
196
+
197
+ ```
198
+ ┌─────────────┐ ┌──────────────┐
199
+ │ Host │ │ Controller │
200
+ │ │ │ │
201
+ │ Store │◀─── State Sync ────│ Store │
202
+ │ (Source) │ │ (Replica) │
203
+ │ │ │ │
204
+ │ Actions │◀─── Action RPC ────│ Proxy │
205
+ │ Execute │ │ Actions │
206
+ └─────────────┘ └──────────────┘
207
+ ```
208
+
209
+ **Flow:**
210
+
211
+ 1. **Host** maintains the source of truth (Zustand store)
212
+ 2. **Controllers** receive state updates via WebSocket
213
+ 3. **Controllers** call actions → RPC to host → host executes → state syncs back
214
+ 4. All state changes automatically broadcast to all connected controllers
215
+
216
+ ### Key Concepts
217
+
218
+ - **Host**: Owns the store, executes actions, broadcasts changes
219
+ - **Controller**: Receives state, proxies actions to host via RPC
220
+ - **Actions**: Functions that modify state (only execute on host)
221
+ - **State**: Read-only data synced from host to controllers
222
+
223
+ ## Advanced Patterns
224
+
225
+ ### Passing Controller ID to Actions
226
+
227
+ The SDK automatically injects `controllerId` as the last argument to actions on the host side. Your action signatures should include it:
228
+
229
+ ```tsx filename="src/store.ts"
230
+ actions: {
231
+ // ✅ Correct: playerId is optional, SDK injects it
232
+ joinTeam: (team: "team1" | "team2", playerId?: string) => void;
233
+
234
+ // ✅ Also works: playerId required
235
+ assignRole: (role: string, playerId: string) => void;
236
+
237
+ // ❌ Wrong: Don't require controllerId in controller calls
238
+ // This won't work because controllers don't know their ID
239
+ // joinTeam: (playerId: string, team: string) => void;
240
+ }
241
+ ```
242
+
243
+ ### State Updates Trigger Re-renders
244
+
245
+ Standard Zustand behavior applies - components re-render when selected state changes:
246
+
247
+ ```tsx
248
+ // This component re-renders when phase changes
249
+ const PhaseDisplay = () => {
250
+ const phase = useGameStore((state) => state.phase);
251
+ return <div>Phase: {phase}</div>;
252
+ };
253
+
254
+ // This component re-renders when scores change
255
+ const ScoreDisplay = () => {
256
+ const scores = useGameStore((state) => state.scores);
257
+ return (
258
+ <div>
259
+ {scores.team1} - {scores.team2}
260
+ </div>
261
+ );
262
+ };
263
+
264
+ // This component re-renders when ANY state changes
265
+ const FullStateDisplay = () => {
266
+ const state = useGameStore((state) => state);
267
+ return <div>{JSON.stringify(state)}</div>;
268
+ };
269
+ ```
270
+
271
+ ### Avoiding Re-renders
272
+
273
+ For performance-critical components, use refs or select minimal state:
274
+
275
+ ```tsx
276
+ // ✅ Good: Only re-renders when phase changes
277
+ const GameLogic = () => {
278
+ const phase = useGameStore((state) => state.phase);
279
+ // ...
280
+ };
281
+
282
+ // ✅ Also good: Use refs for game loop state
283
+ const GameLoop = () => {
284
+ const scoresRef = useRef(useGameStore.getState().scores);
285
+
286
+ useEffect(() => {
287
+ const unsubscribe = useGameStore.subscribe((state) => {
288
+ scoresRef.current = state.scores;
289
+ });
290
+ return unsubscribe;
291
+ }, []);
292
+
293
+ useFrame(() => {
294
+ // Access scoresRef.current without re-renders
295
+ });
296
+ };
297
+ ```
298
+
299
+ ### Complex State Updates
300
+
301
+ Use Zustand's functional updates for complex state:
302
+
303
+ ```tsx filename="src/store.ts"
304
+ actions: {
305
+ updatePlayer: (playerId: string, updates: Partial<Player>) =>
306
+ set((state) => ({
307
+ players: {
308
+ ...state.players,
309
+ [playerId]: {
310
+ ...state.players[playerId],
311
+ ...updates,
312
+ },
313
+ },
314
+ })),
315
+
316
+ removePlayer: (playerId: string) =>
317
+ set((state) => {
318
+ const { [playerId]: removed, ...rest } = state.players;
319
+ return { players: rest };
320
+ }),
321
+ }
322
+ ```
323
+
324
+ ## Best Practices
325
+
326
+ ### 1. Keep Actions Simple
327
+
328
+ Actions should be pure state updates. Complex logic should live in your game code:
329
+
330
+ ```tsx
331
+ // ✅ Good: Simple state update
332
+ actions: {
333
+ setScore: (team: string, score: number) => set((state) => ({
334
+ scores: { ...state.scores, [team]: score },
335
+ })),
336
+ }
337
+
338
+ // ❌ Avoid: Complex logic in actions
339
+ actions: {
340
+ processScore: (team: string) => {
341
+ // Don't do game logic here
342
+ const newScore = calculateComplexScore(team);
343
+ set((state) => ({ ... }));
344
+ },
345
+ }
346
+ ```
347
+
348
+ ### 2. Use Selectors for Performance
349
+
350
+ Always use selectors to minimize re-renders:
351
+
352
+ ```tsx
353
+ // ✅ Good: Only re-renders when phase changes
354
+ const phase = useGameStore((state) => state.phase);
355
+
356
+ // ❌ Avoid: Re-renders on every state change
357
+ const state = useGameStore((state) => state);
358
+ const phase = state.phase;
359
+ ```
360
+
361
+ ### 3. Handle Missing State
362
+
363
+ State may be empty when controllers first connect:
364
+
365
+ ```tsx
366
+ const ControllerView = () => {
367
+ const teamAssignments = useGameStore((state) => state.teamAssignments);
368
+ const myTeam = teamAssignments[controllerId];
369
+
370
+ if (!myTeam) {
371
+ return <div>Select a team...</div>;
372
+ }
373
+
374
+ return <div>You're on {myTeam}</div>;
375
+ };
376
+ ```
377
+
378
+ ### 4. Type Your State
379
+
380
+ Always define TypeScript interfaces:
381
+
382
+ ```tsx
383
+ export interface GameState {
384
+ // State fields
385
+ phase: "lobby" | "playing" | "gameover";
386
+ scores: Record<string, number>;
387
+
388
+ // Actions
389
+ actions: {
390
+ setPhase: (phase: GameState["phase"]) => void;
391
+ updateScore: (playerId: string, score: number) => void;
392
+ };
393
+ }
394
+ ```
395
+
396
+ ### 5. Separate Game State from Networked State
397
+
398
+ Not everything needs to be networked:
399
+
400
+ ```tsx
401
+ // ✅ Networked: Shared across all clients
402
+ const phase = useGameStore((state) => state.phase);
403
+ const scores = useGameStore((state) => state.scores);
404
+
405
+ // ✅ Local: Host-only game loop state
406
+ const gameState = useRef({
407
+ ballX: 0,
408
+ ballY: 0,
409
+ ballVX: 1,
410
+ ballVY: 1,
411
+ });
412
+ ```
413
+
414
+ ## Common Patterns
415
+
416
+ ### Team Assignment
417
+
418
+ ```tsx filename="src/store.ts"
419
+ actions: {
420
+ joinTeam: (team: "team1" | "team2", playerId?: string) => {
421
+ if (!playerId) return;
422
+ set((state) => ({
423
+ teamAssignments: {
424
+ ...state.teamAssignments,
425
+ [playerId]: team,
426
+ },
427
+ }));
428
+ },
429
+ }
430
+ ```
431
+
432
+ ### Score Tracking
433
+
434
+ ```tsx filename="src/store.ts"
435
+ actions: {
436
+ scorePoint: (team: "team1" | "team2") =>
437
+ set((state) => ({
438
+ scores: {
439
+ ...state.scores,
440
+ [team]: state.scores[team] + 1,
441
+ },
442
+ })),
443
+ }
444
+ ```
445
+
446
+ ### Game Phase Management
447
+
448
+ ```tsx filename="src/store.ts"
449
+ actions: {
450
+ setPhase: (phase: "lobby" | "playing" | "gameover") =>
451
+ set({ phase }),
452
+
453
+ startGame: () => {
454
+ set({ phase: "playing" });
455
+ // Reset scores, etc.
456
+ },
457
+
458
+ endGame: (winner: string) => {
459
+ set({ phase: "gameover", winner });
460
+ },
461
+ }
462
+ ```
463
+
464
+ ## Troubleshooting
465
+
466
+ ### Actions Not Executing
467
+
468
+ 1. **Check connection status:**
469
+
470
+ ```tsx
471
+ const { connectionStatus } = useAirJamController();
472
+ console.log(connectionStatus); // Should be "connected"
473
+ ```
474
+
475
+ 2. **Verify actions are in the `actions` object:**
476
+
477
+ ```tsx
478
+ // ✅ Correct
479
+ actions: {
480
+ joinTeam: (team) => { ... }
481
+ }
482
+
483
+ // ❌ Wrong
484
+ joinTeam: (team) => { ... } // Not in actions object
485
+ ```
486
+
487
+ 3. **Ensure you're calling actions, not state:**
488
+
489
+ ```tsx
490
+ // ✅ Correct
491
+ const actions = useGameStore((state) => state.actions);
492
+ actions.joinTeam("team1");
493
+
494
+ // ❌ Wrong
495
+ const state = useGameStore((state) => state);
496
+ state.joinTeam("team1"); // joinTeam is in actions, not state
497
+ ```
498
+
499
+ ### State Not Syncing
500
+
501
+ 1. **Check host is broadcasting:**
502
+ - Host must be connected and role must be "host"
503
+ - State changes must use Zustand's `set` function
504
+
505
+ 2. **Verify controller is receiving:**
506
+ - Controller must be connected to same room
507
+ - Check WebSocket connection in browser dev tools
508
+
509
+ 3. **Check selector usage:**
510
+
511
+ ```tsx
512
+ // ✅ This works
513
+ const phase = useGameStore((state) => state.phase);
514
+
515
+ // ✅ This also works
516
+ const actions = useGameStore((state) => state.actions);
517
+
518
+ // ❌ This won't sync (not using the hook)
519
+ const state = useGameStore.getState();
520
+ ```
521
+
522
+ ### Type Errors
523
+
524
+ Ensure your state interface matches the store:
525
+
526
+ ```tsx
527
+ // ✅ Correct
528
+ interface GameState {
529
+ phase: string;
530
+ actions: {
531
+ setPhase: (phase: string) => void;
532
+ };
533
+ }
534
+
535
+ // ❌ Wrong: Missing actions
536
+ interface GameState {
537
+ phase: string;
538
+ // Missing actions object!
539
+ }
540
+ ```
541
+
542
+ ## API Reference
543
+
544
+ ### `createAirJamStore<T>(initializer)`
545
+
546
+ Creates a networked Zustand store that automatically syncs between host and controllers.
547
+
548
+ **Type Parameters:**
549
+
550
+ - `T` - State interface that must include an `actions` object
551
+
552
+ **Parameters:**
553
+
554
+ - `initializer` - Zustand state creator function `(set, get, api) => T`
555
+
556
+ **Returns:**
557
+
558
+ - Hook function `useSyncedStore<U>(selector?) => U` - React hook for accessing store
559
+
560
+ **Example:**
561
+
562
+ ```tsx
563
+ const useGameStore = createAirJamStore<GameState>((set) => ({
564
+ phase: "lobby",
565
+ actions: {
566
+ setPhase: (phase) => set({ phase }),
567
+ },
568
+ }));
569
+ ```
570
+
571
+ ## See Also
572
+
573
+ - [SDK Hooks](/docs/sdk/hooks) - Other SDK hooks and utilities
574
+ - [Input System](/docs/sdk/input-system) - Handling controller input
575
+ - [Architecture](/docs/getting-started/architecture) - System overview