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,241 @@
1
+ # Host System
2
+
3
+ The Air Jam host system manages game sessions and input routing. This page explains how hosts work in different modes and how input flows through the system.
4
+
5
+ ## Host Modes
6
+
7
+ ### Standalone Host
8
+
9
+ The simplest mode—your game connects directly to the Air Jam server.
10
+
11
+ **Usage:**
12
+
13
+ ```tsx filename="src/components/HostView.tsx"
14
+ const host = useAirJamHost({
15
+ onPlayerJoin: (player) => spawnPlayer(player),
16
+ onPlayerLeave: (id) => removePlayer(id),
17
+ });
18
+
19
+ // Host receives all input directly
20
+ useFrame(() => {
21
+ host.players.forEach((p) => {
22
+ const input = host.getInput(p.id);
23
+ processInput(p.id, input);
24
+ });
25
+ });
26
+ ```
27
+
28
+ ### Arcade Mode (Two-Host Model)
29
+
30
+ In arcade mode, the platform runs a "master" host and your game runs as a "child" host inside an iframe.
31
+
32
+ ## Server-Authoritative Focus System
33
+
34
+ The server maintains authoritative control over which host receives controller inputs through a **focus** state:
35
+
36
+ ### Why Server-Authoritative?
37
+
38
+ 1. **Security** - Prevents rogue games from stealing input
39
+ 2. **Reliability** - Single source of truth for focus state
40
+ 3. **Consistency** - All controllers route to same host
41
+
42
+ ## Connection Flow
43
+
44
+ ### 1. Arcade Launch
45
+
46
+ ```
47
+ [Platform] [Server] [Controller]
48
+ │ │ │
49
+ │ host:register │ │
50
+ │ { mode: "master" } │ │
51
+ │ ───────────────────────────▶│ │
52
+ │ │ │
53
+ │ ack: { ok, roomId: "ABCD" } │ │
54
+ │ ◀───────────────────────────│ │
55
+ │ │ │
56
+ │ Display QR Code │ │
57
+ │ with room code │ Scan QR Code │
58
+ │ │ ◀──────────────────────────│
59
+ │ │ │
60
+ │ │ controller:join │
61
+ │ │ ◀──────────────────────────│
62
+ │ │ │
63
+ │ server:controllerJoined │ server:welcome │
64
+ │ ◀───────────────────────────│ ──────────────────────────▶│
65
+ ```
66
+
67
+ ### 2. Game Launch
68
+
69
+ ```
70
+ [Arcade] [Server] [Your Game] [Controller]
71
+ │ │ │ │
72
+ │ system:launchGame │ │ │
73
+ │ ───────────────────▶│ │ │
74
+ │ │ │ │
75
+ │ ack: { joinToken } │ │ │
76
+ │ ◀────────────────── │ │ │
77
+ │ │ │ │
78
+ │ Load iframe ───────────────────────────────▶│ │
79
+ │ │ │ │
80
+ │ │ host:joinAsChild │ │
81
+ │ │◀───────────────────── │ │
82
+ │ │ │ │
83
+ │ │ Focus → GAME │ │
84
+ │ │ │ │
85
+ │ │ Redirect controller │ │
86
+ │ │ ─────────────────────────────────────────▶│
87
+ │ │ │ │
88
+ │ │ │ Load game UI │
89
+ ```
90
+
91
+ ### 3. Active Gameplay
92
+
93
+ With focus set to `GAME`, all controller input routes to your game:
94
+
95
+ ```tsx
96
+ // Your game receives input normally
97
+ const host = useAirJamHost({
98
+ onPlayerJoin: (player) => {
99
+ // Existing players synced on launch
100
+ // New players join during gameplay
101
+ spawnPlayer(player);
102
+ },
103
+ });
104
+
105
+ useFrame(() => {
106
+ host.players.forEach((player) => {
107
+ const input = host.getInput(player.id);
108
+ // Process gameplay...
109
+ });
110
+ });
111
+ ```
112
+
113
+ ### 4. Game Exit
114
+
115
+ ```
116
+ [Your Game] [Server] [Arcade] [Controller]
117
+ │ │ │ │
118
+ │ host:exit │ │ │
119
+ │ ───────────────────▶│ │ │
120
+ │ │ │ │
121
+ │ │ Focus → SYSTEM │ │
122
+ │ │ │ │
123
+ │ │ server:gameEnded │ │
124
+ │ │ ─────────────────────▶│ │
125
+ │ │ │ │
126
+ │ Destroy iframe ◀────────────── │ │
127
+ │ │ │ │
128
+ │ │ Restore arcade UI │ │
129
+ │ │ ─────────────────────────────────────────▶│
130
+ ```
131
+
132
+ ## Host Registration
133
+
134
+ ### Standalone Mode
135
+
136
+ ```tsx
137
+ // Automatic registration when useAirJamHost is called
138
+ const host = useAirJamHost({
139
+ roomId: "GAME", // Optional custom room code
140
+ maxPlayers: 4, // Optional limit
141
+ });
142
+
143
+ // host.roomId contains the room code
144
+ // host.joinUrl contains full URL for QR code
145
+ ```
146
+
147
+ ### Child Mode (Arcade)
148
+
149
+ ```tsx
150
+ // SDK auto-detects arcade mode from URL params
151
+ // ?aj_room=ABCD&aj_token=xxxxx
152
+
153
+ const host = useAirJamHost({
154
+ onPlayerJoin: (player) => {
155
+ // Existing players synced automatically
156
+ },
157
+ onChildClose: () => {
158
+ // Called when arcade closes the game
159
+ cleanupGame();
160
+ },
161
+ });
162
+
163
+ // host.isChildMode === true
164
+ ```
165
+
166
+ ## Player Management
167
+
168
+ ### Player Lifecycle
169
+
170
+ ```tsx
171
+ const host = useAirJamHost({
172
+ onPlayerJoin: (player) => {
173
+ // player.id - Unique identifier
174
+ // player.label - Display name ("Player 1", etc.)
175
+ // player.color - Assigned color ("#FF5733")
176
+ // player.nickname - Optional custom name
177
+
178
+ spawnPlayerEntity(player);
179
+ host.sendSignal(
180
+ "TOAST",
181
+ {
182
+ title: `Welcome ${player.label}!`,
183
+ },
184
+ player.id,
185
+ );
186
+ },
187
+
188
+ onPlayerLeave: (controllerId) => {
189
+ removePlayerEntity(controllerId);
190
+ },
191
+ });
192
+ ```
193
+
194
+ ### Accessing Players
195
+
196
+ ```tsx
197
+ // Current player list
198
+ host.players.forEach((player) => {
199
+ console.log(player.label, player.color);
200
+ });
201
+
202
+ // Player count
203
+ const playerCount = host.players.length;
204
+
205
+ // Find specific player
206
+ const player = host.players.find((p) => p.id === targetId);
207
+ ```
208
+
209
+ ## State Broadcasting
210
+
211
+ Send state updates to all controllers:
212
+
213
+ ```tsx
214
+ // Update game state display
215
+ host.sendState({
216
+ gameState: "playing", // "playing" | "paused"
217
+ message: "Round 3 - Fight!",
218
+ });
219
+
220
+ // Controllers receive via onState callback
221
+ // or controller.gameState / controller.stateMessage
222
+ ```
223
+
224
+ ## Error Handling
225
+
226
+ ```tsx
227
+ const host = useAirJamHost();
228
+
229
+ // Check connection
230
+ if (host.connectionStatus === "disconnected") {
231
+ showReconnectUI();
232
+ }
233
+
234
+ // Check for errors
235
+ if (host.lastError) {
236
+ showError(host.lastError);
237
+ }
238
+
239
+ // Force reconnection
240
+ host.reconnect();
241
+ ```
@@ -0,0 +1,403 @@
1
+ # SDK Hooks
2
+
3
+ The Air Jam SDK provides React hooks for building multiplayer games. This page documents all available hooks and their usage.
4
+
5
+ ## Provider
6
+
7
+ ### `AirJamProvider`
8
+
9
+ The root provider that must wrap your application. Manages WebSocket connections, state, and input processing.
10
+
11
+ ```tsx
12
+ import { AirJamProvider } from "@air-jam/sdk";
13
+ import { z } from "zod";
14
+
15
+ const inputSchema = z.object({
16
+ vector: z.object({ x: z.number(), y: z.number() }),
17
+ action: z.boolean(),
18
+ timestamp: z.number(),
19
+ });
20
+
21
+ <AirJamProvider
22
+ // Optional: WebSocket server URL (auto-detects from env)
23
+ serverUrl="wss://your-server.com"
24
+ // Optional: API key for production
25
+ apiKey="your-api-key"
26
+ // Optional: Path for controller page (default: "/joypad")
27
+ controllerPath="/controller"
28
+ // Optional: Max players (default: 8)
29
+ maxPlayers={4}
30
+ // Optional: Input configuration with schema and latching
31
+ input={{
32
+ schema: inputSchema,
33
+ latch: {
34
+ booleanFields: ["action"],
35
+ vectorFields: ["vector"],
36
+ },
37
+ }}
38
+ >
39
+ <App />
40
+ </AirJamProvider>;
41
+ ```
42
+
43
+ **Environment Variables:**
44
+
45
+ The provider automatically reads from these environment variables if props aren't provided:
46
+
47
+ - `VITE_AIR_JAM_SERVER_URL` / `NEXT_PUBLIC_AIR_JAM_SERVER_URL` - WebSocket server URL
48
+ - `VITE_AIR_JAM_API_KEY` / `NEXT_PUBLIC_AIR_JAM_API_KEY` - API key
49
+
50
+ ---
51
+
52
+ ## Host Hooks
53
+
54
+ ### `useAirJamHost`
55
+
56
+ The primary hook for game hosts. Connects to the server, manages players, and provides input access.
57
+
58
+ ```tsx
59
+ import { useAirJamHost } from "@air-jam/sdk";
60
+
61
+ const HostView = () => {
62
+ const host = useAirJamHost({
63
+ // Optional: Custom room code (auto-generated if not provided)
64
+ roomId: "GAME",
65
+
66
+ // Called when a player joins
67
+ onPlayerJoin: (player) => {
68
+ console.log(`${player.label} joined with color ${player.color}`);
69
+ },
70
+
71
+ // Called when a player leaves
72
+ onPlayerLeave: (controllerId) => {
73
+ console.log(`Player ${controllerId} left`);
74
+ },
75
+ });
76
+
77
+ // Return values
78
+ const {
79
+ roomId, // "ABCD" - The room code
80
+ joinUrl, // Full URL for controllers to join
81
+ connectionStatus, // "connected" | "connecting" | "disconnected" | "idle"
82
+ players, // Array of PlayerProfile
83
+ gameState, // "playing" | "paused"
84
+ lastError, // Error message if any
85
+ mode, // "standalone" | "arcade" | "platform"
86
+
87
+ // Functions
88
+ getInput, // (controllerId: string) => Input | undefined
89
+ sendSignal, // Send haptics/toasts to controllers
90
+ sendState, // Broadcast state to all controllers
91
+ toggleGameState, // Toggle between playing/paused
92
+ reconnect, // Force reconnection
93
+ } = host;
94
+
95
+ return (
96
+ <div>
97
+ <h1>Room: {roomId}</h1>
98
+ <img
99
+ src={`https://api.qrserver.com/v1/create-qr-code/?data=${joinUrl}`}
100
+ />
101
+ <p>Players: {players.length}</p>
102
+ </div>
103
+ );
104
+ };
105
+ ```
106
+
107
+ **Reading Input in Game Loops:**
108
+
109
+ ```tsx
110
+ // In a React Three Fiber component
111
+ useFrame(() => {
112
+ host.players.forEach((player) => {
113
+ const input = host.getInput(player.id);
114
+ if (!input) return;
115
+
116
+ // Move player based on joystick
117
+ movePlayer(player.id, input.vector);
118
+
119
+ // Handle button press (automatically latched)
120
+ if (input.action) {
121
+ playerShoot(player.id);
122
+ }
123
+ });
124
+ });
125
+ ```
126
+
127
+ **Sending Haptic Feedback:**
128
+
129
+ ```tsx
130
+ // Vibrate a specific player's phone
131
+ host.sendSignal("HAPTIC", { pattern: "heavy" }, playerId);
132
+
133
+ // Available patterns: "light", "medium", "heavy", "success", "failure", "custom"
134
+ host.sendSignal(
135
+ "HAPTIC",
136
+ {
137
+ pattern: "custom",
138
+ sequence: [50, 100, 50], // Vibrate 50ms, pause 100ms, vibrate 50ms
139
+ },
140
+ playerId,
141
+ );
142
+ ```
143
+
144
+ **Sending Toast Notifications:**
145
+
146
+ ```tsx
147
+ // Show notification on a player's controller
148
+ host.sendSignal(
149
+ "TOAST",
150
+ {
151
+ title: "Achievement Unlocked!",
152
+ message: "First blood",
153
+ variant: "success", // "default" | "success" | "destructive"
154
+ },
155
+ playerId,
156
+ );
157
+
158
+ // Broadcast to all players (omit targetId)
159
+ host.sendSignal("TOAST", {
160
+ title: "Round Start!",
161
+ message: "Get ready to fight",
162
+ });
163
+ ```
164
+
165
+ ---
166
+
167
+ ### `useGetInput`
168
+
169
+ Lightweight hook for accessing input without triggering re-renders. Use in performance-critical components.
170
+
171
+ ```tsx
172
+ import { useGetInput } from "@air-jam/sdk";
173
+
174
+ const Ship = ({ playerId }: { playerId: string }) => {
175
+ const getInput = useGetInput();
176
+
177
+ // This component won't re-render when connection state changes
178
+ useFrame(() => {
179
+ const input = getInput(playerId);
180
+ if (!input) return;
181
+
182
+ // Update ship position
183
+ shipRef.current.position.x += input.vector.x * SPEED;
184
+ shipRef.current.position.y += input.vector.y * SPEED;
185
+ });
186
+
187
+ return <mesh ref={shipRef}>...</mesh>;
188
+ };
189
+ ```
190
+
191
+ **When to use `useGetInput` vs `useAirJamHost().getInput`:**
192
+
193
+ ---
194
+
195
+ ### `useSendSignal`
196
+
197
+ Lightweight hook for sending signals without triggering re-renders. Use in collision handlers.
198
+
199
+ ```tsx
200
+ import { useSendSignal } from "@air-jam/sdk";
201
+
202
+ const Laser = ({ ownerId }: { ownerId: string }) => {
203
+ const sendSignal = useSendSignal();
204
+
205
+ const handleHit = (targetId: string) => {
206
+ // Vibrate the player who got hit
207
+ sendSignal("HAPTIC", { pattern: "heavy" }, targetId);
208
+
209
+ // Light feedback for the shooter
210
+ sendSignal("HAPTIC", { pattern: "light" }, ownerId);
211
+ };
212
+
213
+ // Collision detection...
214
+ };
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Controller Hooks
220
+
221
+ ### `useAirJamController`
222
+
223
+ Hook for building mobile controllers that connect to game hosts.
224
+
225
+ ```tsx
226
+ import { useAirJamController } from "@air-jam/sdk";
227
+
228
+ const ControllerView = () => {
229
+ const controller = useAirJamController({
230
+ // Optional: Room from URL query param takes precedence
231
+ roomId: "ABCD",
232
+
233
+ // Optional: Player nickname
234
+ nickname: "Player1",
235
+
236
+ // Optional: Called when host sends state updates
237
+ onState: (state) => {
238
+ if (state.message) {
239
+ showNotification(state.message);
240
+ }
241
+ },
242
+ });
243
+
244
+ const {
245
+ roomId, // Room code (from URL or props)
246
+ controllerId, // This controller's unique ID
247
+ connectionStatus, // Connection state
248
+ gameState, // "playing" | "paused"
249
+ stateMessage, // Optional message from host
250
+
251
+ // Functions
252
+ sendInput, // Send input to host
253
+ sendSystemCommand, // "exit" | "ready" | "toggle_pause"
254
+ setNickname, // Update nickname
255
+ reconnect, // Force reconnection
256
+ } = controller;
257
+
258
+ if (connectionStatus === "connecting") {
259
+ return <div>Connecting to room {roomId}...</div>;
260
+ }
261
+
262
+ if (connectionStatus === "disconnected") {
263
+ return (
264
+ <div>
265
+ Disconnected. <button onClick={reconnect}>Retry</button>
266
+ </div>
267
+ );
268
+ }
269
+
270
+ return (
271
+ <div>
272
+ <Joystick
273
+ onMove={(x, y) => {
274
+ controller.sendInput({
275
+ vector: { x, y },
276
+ action: false,
277
+ timestamp: Date.now(),
278
+ });
279
+ }}
280
+ />
281
+ <FireButton
282
+ onPress={() => {
283
+ controller.sendInput({
284
+ vector: { x: 0, y: 0 },
285
+ action: true,
286
+ timestamp: Date.now(),
287
+ });
288
+ }}
289
+ />
290
+ </div>
291
+ );
292
+ };
293
+ ```
294
+
295
+ **Auto Room Join from URL:**
296
+
297
+ Controllers automatically join rooms from URL query parameters:
298
+
299
+ ```
300
+ https://yourgame.com/joypad?room=ABCD
301
+ ```
302
+
303
+ This is how QR code scanning works—the host generates a URL with the room code embedded.
304
+
305
+ ---
306
+
307
+ ## Utility Hooks
308
+
309
+ ### `useAirJamContext`
310
+
311
+ Low-level hook for accessing the raw context. Most apps don't need this.
312
+
313
+ ```tsx
314
+ import { useAirJamContext } from "@air-jam/sdk";
315
+
316
+ const { config, store, inputManager } = useAirJamContext();
317
+ ```
318
+
319
+ ### `useAirJamConfig`
320
+
321
+ Access the resolved configuration.
322
+
323
+ ```tsx
324
+ import { useAirJamConfig } from "@air-jam/sdk";
325
+
326
+ const config = useAirJamConfig();
327
+ console.log(config.serverUrl, config.maxPlayers);
328
+ ```
329
+
330
+ ### `useAirJamState`
331
+
332
+ Subscribe to specific state with optimal re-rendering.
333
+
334
+ ```tsx
335
+ import { useAirJamState } from "@air-jam/sdk";
336
+
337
+ const { players, gameState } = useAirJamState((state) => ({
338
+ players: state.players,
339
+ gameState: state.gameState,
340
+ }));
341
+ ```
342
+
343
+ ### `useAirJamSocket`
344
+
345
+ Get the raw Socket.IO instance for advanced usage.
346
+
347
+ ```tsx
348
+ import { useAirJamSocket } from "@air-jam/sdk";
349
+
350
+ const socket = useAirJamSocket("host");
351
+ socket.emit("custom:event", { data: "value" });
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Types
357
+
358
+ ### `PlayerProfile`
359
+
360
+ ```typescript
361
+ interface PlayerProfile {
362
+ id: string; // Unique controller ID
363
+ label: string; // Display name (e.g., "Player 1")
364
+ color: string; // Assigned color (e.g., "#FF5733")
365
+ nickname?: string; // Optional player-provided nickname
366
+ }
367
+ ```
368
+
369
+ ### `ConnectionStatus`
370
+
371
+ ```typescript
372
+ type ConnectionStatus =
373
+ | "idle" // Not yet connected
374
+ | "connecting" // Connection in progress
375
+ | "connected" // Successfully connected
376
+ | "disconnected" // Connection lost
377
+ | "reconnecting"; // Attempting to reconnect
378
+ ```
379
+
380
+ ### `GameState`
381
+
382
+ ```typescript
383
+ type GameState = "playing" | "paused";
384
+ ```
385
+
386
+ ### `HapticSignalPayload`
387
+
388
+ ```typescript
389
+ interface HapticSignalPayload {
390
+ pattern: "light" | "medium" | "heavy" | "success" | "failure" | "custom";
391
+ sequence?: number | number[]; // For "custom" pattern
392
+ }
393
+ ```
394
+
395
+ ### `ToastSignalPayload`
396
+
397
+ ```typescript
398
+ interface ToastSignalPayload {
399
+ title: string;
400
+ message?: string;
401
+ variant?: "default" | "success" | "destructive";
402
+ }
403
+ ```