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.
- package/dist/index.js +11 -3
- package/package.json +6 -3
- package/templates/pong/.env.example +11 -0
- package/templates/pong/.env.local +10 -0
- package/templates/pong/AI_INSTRUCTIONS.md +44 -0
- package/templates/pong/README.md +111 -0
- package/templates/pong/airjam-docs/getting-started/architecture/page.md +165 -0
- package/templates/pong/airjam-docs/getting-started/game-ideas/page.md +114 -0
- package/templates/pong/airjam-docs/getting-started/introduction/page.md +122 -0
- package/templates/pong/airjam-docs/how-it-works/host-system/page.md +241 -0
- package/templates/pong/airjam-docs/sdk/hooks/page.md +403 -0
- package/templates/pong/airjam-docs/sdk/input-system/page.md +336 -0
- package/templates/pong/airjam-docs/sdk/networked-state/page.md +575 -0
- package/templates/pong/dist/assets/index-B9l0NKly.js +269 -0
- package/templates/pong/dist/assets/index-CHKqdIQG.css +1 -0
- package/templates/pong/dist/index.html +14 -0
- package/templates/pong/eslint.config.js +33 -0
- package/templates/pong/index.html +6 -1
- package/templates/pong/node_modules/.bin/air-jam-server +17 -0
- package/templates/pong/node_modules/.bin/eslint +17 -0
- package/templates/pong/node_modules/.bin/eslint-config-prettier +17 -0
- package/templates/pong/node_modules/.bin/jiti +17 -0
- package/templates/pong/node_modules/.bin/tsc +17 -0
- package/templates/pong/node_modules/.bin/tsserver +17 -0
- package/templates/pong/node_modules/.bin/tsx +17 -0
- package/templates/pong/node_modules/.bin/vite +17 -0
- package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js +66143 -0
- package/templates/pong/node_modules/.vite/deps/@air-jam_sdk.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/_metadata.json +73 -0
- package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js +292 -0
- package/templates/pong/node_modules/.vite/deps/chunk-3TUQC5ZT.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/templates/pong/node_modules/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js +280 -0
- package/templates/pong/node_modules/.vite/deps/chunk-QUPSG5AV.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js +13810 -0
- package/templates/pong/node_modules/.vite/deps/chunk-TYOCAO5S.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js +1004 -0
- package/templates/pong/node_modules/.vite/deps/chunk-YG4BJP3V.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/package.json +3 -0
- package/templates/pong/node_modules/.vite/deps/react-dom.js +6 -0
- package/templates/pong/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react-dom_client.js +20217 -0
- package/templates/pong/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react-router-dom.js +13900 -0
- package/templates/pong/node_modules/.vite/deps/react-router-dom.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react.js +5 -0
- package/templates/pong/node_modules/.vite/deps/react.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
- package/templates/pong/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/templates/pong/node_modules/.vite/deps/zod.js +476 -0
- package/templates/pong/node_modules/.vite/deps/zod.js.map +7 -0
- package/templates/pong/package.json +12 -1
- package/templates/pong/src/App.tsx +2 -2
- package/templates/pong/src/controller-view.tsx +143 -0
- package/templates/pong/src/host-view.tsx +401 -0
- package/templates/pong/src/main.tsx +2 -1
- package/templates/pong/src/store.ts +80 -0
- package/templates/pong/tsconfig.json +3 -2
- package/templates/pong/vite.config.ts +3 -0
- package/templates/pong/src/ControllerView.tsx +0 -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
|