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,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
|
+
```
|