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,401 @@
1
+ import {
2
+ AirJamDebug,
3
+ AirJamOverlay,
4
+ PlayerAvatar,
5
+ useAirJamHost,
6
+ } from "@air-jam/sdk";
7
+ import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { usePongStore } from "./store";
9
+ import { gameInputSchema } from "./types";
10
+
11
+ const FIELD_WIDTH = 1000;
12
+ const FIELD_HEIGHT = 600;
13
+ const PADDLE_HEIGHT = 100;
14
+ const PADDLE_WIDTH = 15;
15
+ const PADDLE_OFFSET = 30;
16
+ const BALL_SIZE = 15;
17
+ const PADDLE_SPEED = 6;
18
+ const BALL_SPEED = 3;
19
+ const TEAM1_COLOR = "#f97316"; // (Solaris)
20
+ const TEAM2_COLOR = "#38bdf8"; // (Nebulon)
21
+
22
+ export function HostView() {
23
+ const host = useAirJamHost<typeof gameInputSchema>();
24
+ const canvasRef = useRef<HTMLCanvasElement>(null);
25
+ const [countdown, setCountdown] = useState<number | null>(null);
26
+
27
+ // 1. Read Game Logic State (UI & Rules)
28
+ // Shared networked state with zustand reducers to minimize re-renders
29
+ const scores = usePongStore((state) => state.scores);
30
+ const teamAssignments = usePongStore((state) => state.teamAssignments);
31
+ const actions = usePongStore((state) => state.actions);
32
+
33
+ // Game state refs (to avoid re-renders in game loop)
34
+ const gameState = useRef({
35
+ paddle1FrontY: FIELD_HEIGHT / 2 - PADDLE_HEIGHT / 2,
36
+ paddle1BackY: FIELD_HEIGHT / 2 - PADDLE_HEIGHT / 2,
37
+ paddle2FrontY: FIELD_HEIGHT / 2 - PADDLE_HEIGHT / 2,
38
+ paddle2BackY: FIELD_HEIGHT / 2 - PADDLE_HEIGHT / 2,
39
+ ballX: FIELD_WIDTH / 2,
40
+ ballY: FIELD_HEIGHT / 2,
41
+ ballVX: BALL_SPEED,
42
+ ballVY: BALL_SPEED,
43
+ lastTouchedTeam: null as "team1" | "team2" | null, // Track which team last touched the ball
44
+ });
45
+
46
+ const resetBall = useCallback(() => {
47
+ const state = gameState.current;
48
+ state.ballX = FIELD_WIDTH / 2;
49
+ state.ballY = FIELD_HEIGHT / 2;
50
+ state.ballVX = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
51
+ state.ballVY = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
52
+ state.lastTouchedTeam = null; // Reset ball color to neutral
53
+ }, []);
54
+
55
+ // Handle countdown timer (only when playing)
56
+ useEffect(() => {
57
+ if (countdown === null) return;
58
+ if (host.gameState !== "playing") return; // Don't progress countdown when paused
59
+
60
+ if (countdown === 0) {
61
+ resetBall();
62
+ // Defer state update to avoid cascading renders
63
+ setTimeout(() => {
64
+ setCountdown(null);
65
+ }, 0);
66
+ return;
67
+ }
68
+
69
+ const timer = setTimeout(() => {
70
+ setCountdown(countdown - 1);
71
+ }, 1000);
72
+
73
+ return () => clearTimeout(timer);
74
+ }, [countdown, host.gameState, resetBall]);
75
+
76
+ // Game loop
77
+ useEffect(() => {
78
+ const canvas = canvasRef.current;
79
+ if (!canvas) return;
80
+ const ctx = canvas.getContext("2d");
81
+ if (!ctx) return;
82
+
83
+ // Handle high DPI displays
84
+ const dpr = window.devicePixelRatio || 1;
85
+ canvas.width = FIELD_WIDTH * dpr;
86
+ canvas.height = FIELD_HEIGHT * dpr;
87
+ canvas.style.width = `${FIELD_WIDTH}px`;
88
+ canvas.style.height = `${FIELD_HEIGHT}px`;
89
+ ctx.scale(dpr, dpr);
90
+
91
+ let animationId: number;
92
+
93
+ const gameLoop = () => {
94
+ const state = gameState.current;
95
+ const players = host.players;
96
+ const isPlaying = host.gameState === "playing";
97
+
98
+ // Only process game logic when playing (not paused)
99
+ if (isPlaying) {
100
+ // Loop through players and apply input based on team and position
101
+ players.forEach((p) => {
102
+ // Get Raw Input (High Frequency)
103
+ const input = host.getInput(p.id);
104
+
105
+ // Get Logic State (Low Frequency)
106
+ const assignment = teamAssignments[p.id];
107
+
108
+ if (input && assignment) {
109
+ const { team, position } = assignment;
110
+ // Apply physics based on team and position!
111
+ if (team === "team1") {
112
+ if (position === "front") {
113
+ state.paddle1FrontY += input.direction * PADDLE_SPEED;
114
+ state.paddle1FrontY = Math.max(
115
+ 0,
116
+ Math.min(FIELD_HEIGHT - PADDLE_HEIGHT, state.paddle1FrontY),
117
+ );
118
+ } else {
119
+ state.paddle1BackY += input.direction * PADDLE_SPEED;
120
+ state.paddle1BackY = Math.max(
121
+ 0,
122
+ Math.min(FIELD_HEIGHT - PADDLE_HEIGHT, state.paddle1BackY),
123
+ );
124
+ }
125
+ }
126
+ if (team === "team2") {
127
+ if (position === "front") {
128
+ state.paddle2FrontY += input.direction * PADDLE_SPEED;
129
+ state.paddle2FrontY = Math.max(
130
+ 0,
131
+ Math.min(FIELD_HEIGHT - PADDLE_HEIGHT, state.paddle2FrontY),
132
+ );
133
+ } else {
134
+ state.paddle2BackY += input.direction * PADDLE_SPEED;
135
+ state.paddle2BackY = Math.max(
136
+ 0,
137
+ Math.min(FIELD_HEIGHT - PADDLE_HEIGHT, state.paddle2BackY),
138
+ );
139
+ }
140
+ }
141
+ }
142
+ });
143
+
144
+ // Move ball (only if not in countdown)
145
+ if (countdown === null) {
146
+ state.ballX += state.ballVX;
147
+ state.ballY += state.ballVY;
148
+ }
149
+
150
+ // Ball collision with top/bottom walls
151
+ if (state.ballY <= 0 || state.ballY >= FIELD_HEIGHT - BALL_SIZE) {
152
+ state.ballVY *= -1;
153
+ }
154
+
155
+ // Ball collision with paddles
156
+ // Team 1 - Left side (Orange)
157
+ const team1Players = players.filter(
158
+ (p) => teamAssignments[p.id]?.team === "team1",
159
+ );
160
+ // Front paddle collision (only if front player exists)
161
+ const team1FrontPlayer = team1Players.find(
162
+ (p) => teamAssignments[p.id]?.position === "front",
163
+ );
164
+ if (team1FrontPlayer) {
165
+ if (
166
+ state.ballX <= PADDLE_OFFSET + PADDLE_WIDTH &&
167
+ state.ballX >= PADDLE_OFFSET &&
168
+ state.ballY + BALL_SIZE >= state.paddle1FrontY &&
169
+ state.ballY <= state.paddle1FrontY + PADDLE_HEIGHT
170
+ ) {
171
+ state.ballVX = Math.abs(state.ballVX);
172
+ state.lastTouchedTeam = "team1";
173
+ }
174
+ }
175
+ // Back paddle collision (only if back player exists)
176
+ const team1BackPlayer = team1Players.find(
177
+ (p) => teamAssignments[p.id]?.position === "back",
178
+ );
179
+ if (team1BackPlayer) {
180
+ const backPaddle1X = PADDLE_OFFSET / 2;
181
+ if (
182
+ state.ballX <= backPaddle1X + PADDLE_WIDTH &&
183
+ state.ballX >= backPaddle1X &&
184
+ state.ballY + BALL_SIZE >= state.paddle1BackY &&
185
+ state.ballY <= state.paddle1BackY + PADDLE_HEIGHT
186
+ ) {
187
+ state.ballVX = Math.abs(state.ballVX);
188
+ state.lastTouchedTeam = "team1";
189
+ }
190
+ }
191
+ // Team 2 - Right side (Blue)
192
+ const team2Players = players.filter(
193
+ (p) => teamAssignments[p.id]?.team === "team2",
194
+ );
195
+ // Front paddle collision (only if front player exists)
196
+ const team2FrontPlayer = team2Players.find(
197
+ (p) => teamAssignments[p.id]?.position === "front",
198
+ );
199
+ if (team2FrontPlayer) {
200
+ if (
201
+ state.ballX >=
202
+ FIELD_WIDTH - PADDLE_OFFSET - PADDLE_WIDTH - BALL_SIZE &&
203
+ state.ballX <= FIELD_WIDTH - PADDLE_OFFSET &&
204
+ state.ballY + BALL_SIZE >= state.paddle2FrontY &&
205
+ state.ballY <= state.paddle2FrontY + PADDLE_HEIGHT
206
+ ) {
207
+ state.ballVX = -Math.abs(state.ballVX);
208
+ state.lastTouchedTeam = "team2";
209
+ }
210
+ }
211
+ // Back paddle collision (only if back player exists)
212
+ const team2BackPlayer = team2Players.find(
213
+ (p) => teamAssignments[p.id]?.position === "back",
214
+ );
215
+ if (team2BackPlayer) {
216
+ const backPaddle2X = FIELD_WIDTH - PADDLE_OFFSET / 2 - PADDLE_WIDTH;
217
+ if (
218
+ state.ballX >= backPaddle2X - BALL_SIZE &&
219
+ state.ballX <= backPaddle2X + PADDLE_WIDTH &&
220
+ state.ballY + BALL_SIZE >= state.paddle2BackY &&
221
+ state.ballY <= state.paddle2BackY + PADDLE_HEIGHT
222
+ ) {
223
+ state.ballVX = -Math.abs(state.ballVX);
224
+ state.lastTouchedTeam = "team2";
225
+ }
226
+ }
227
+
228
+ // Scoring
229
+ if (countdown === null) {
230
+ if (state.ballX <= 0) {
231
+ actions.scorePoint("team2");
232
+ setCountdown(3);
233
+ }
234
+ if (state.ballX >= FIELD_WIDTH - BALL_SIZE) {
235
+ actions.scorePoint("team1");
236
+ setCountdown(3);
237
+ }
238
+ }
239
+ }
240
+
241
+ // Draw
242
+ ctx.fillStyle = "#000";
243
+ ctx.fillRect(0, 0, FIELD_WIDTH, FIELD_HEIGHT);
244
+
245
+ // Paddles - Team 1 (Left side - Orange)
246
+ // Only draw paddles for players that are actually assigned
247
+ const team1Players = players.filter(
248
+ (p) => teamAssignments[p.id]?.team === "team1",
249
+ );
250
+ if (team1Players.length > 0) {
251
+ ctx.fillStyle = TEAM1_COLOR;
252
+ // Check if front position is assigned
253
+ const frontPlayer = team1Players.find(
254
+ (p) => teamAssignments[p.id]?.position === "front",
255
+ );
256
+ if (frontPlayer) {
257
+ ctx.fillRect(
258
+ PADDLE_OFFSET,
259
+ state.paddle1FrontY,
260
+ PADDLE_WIDTH,
261
+ PADDLE_HEIGHT,
262
+ );
263
+ }
264
+ // Check if back position is assigned
265
+ const backPlayer = team1Players.find(
266
+ (p) => teamAssignments[p.id]?.position === "back",
267
+ );
268
+ if (backPlayer) {
269
+ ctx.fillRect(
270
+ PADDLE_OFFSET / 2,
271
+ state.paddle1BackY,
272
+ PADDLE_WIDTH,
273
+ PADDLE_HEIGHT,
274
+ );
275
+ }
276
+ }
277
+
278
+ // Paddles - Team 2 (Right side - Blue)
279
+ // Only draw paddles for players that are actually assigned
280
+ const team2Players = players.filter(
281
+ (p) => teamAssignments[p.id]?.team === "team2",
282
+ );
283
+ if (team2Players.length > 0) {
284
+ ctx.fillStyle = TEAM2_COLOR;
285
+ // Check if front position is assigned
286
+ const frontPlayer = team2Players.find(
287
+ (p) => teamAssignments[p.id]?.position === "front",
288
+ );
289
+ if (frontPlayer) {
290
+ ctx.fillRect(
291
+ FIELD_WIDTH - PADDLE_OFFSET - PADDLE_WIDTH,
292
+ state.paddle2FrontY,
293
+ PADDLE_WIDTH,
294
+ PADDLE_HEIGHT,
295
+ );
296
+ }
297
+ // Check if back position is assigned
298
+ const backPlayer = team2Players.find(
299
+ (p) => teamAssignments[p.id]?.position === "back",
300
+ );
301
+ if (backPlayer) {
302
+ ctx.fillRect(
303
+ FIELD_WIDTH - PADDLE_OFFSET / 2 - PADDLE_WIDTH,
304
+ state.paddle2BackY,
305
+ PADDLE_WIDTH,
306
+ PADDLE_HEIGHT,
307
+ );
308
+ }
309
+ }
310
+ // Ball (color based on last team that touched it)
311
+ ctx.fillStyle =
312
+ state.lastTouchedTeam === "team1"
313
+ ? TEAM1_COLOR
314
+ : state.lastTouchedTeam === "team2"
315
+ ? TEAM2_COLOR
316
+ : "#fff"; // Neutral white if no team has touched it
317
+ ctx.beginPath();
318
+ ctx.arc(
319
+ state.ballX + BALL_SIZE / 2,
320
+ state.ballY + BALL_SIZE / 2,
321
+ BALL_SIZE / 2,
322
+ 0,
323
+ Math.PI * 2,
324
+ );
325
+ ctx.fill();
326
+ // Center line
327
+ ctx.setLineDash([5, 15]);
328
+ ctx.beginPath();
329
+ ctx.moveTo(FIELD_WIDTH / 2, 0);
330
+ ctx.lineTo(FIELD_WIDTH / 2, FIELD_HEIGHT);
331
+ ctx.strokeStyle = "#333";
332
+ ctx.stroke();
333
+
334
+ // Draw countdown
335
+ if (countdown !== null) {
336
+ ctx.fillStyle = "#fff";
337
+ ctx.font = "bold 120px Arial";
338
+ ctx.textAlign = "center";
339
+ ctx.textBaseline = "middle";
340
+ ctx.fillText(countdown.toString(), FIELD_WIDTH / 2, FIELD_HEIGHT / 2);
341
+ }
342
+
343
+ animationId = requestAnimationFrame(gameLoop);
344
+ };
345
+
346
+ gameLoop();
347
+ return () => cancelAnimationFrame(animationId);
348
+ }, [host, resetBall, countdown, teamAssignments, actions]);
349
+
350
+ return (
351
+ <>
352
+ <AirJamOverlay />
353
+
354
+ {/* Debug State Component */}
355
+ <div className="fixed top-20 right-4 z-50">
356
+ <AirJamDebug
357
+ state={usePongStore((state) => state)}
358
+ title="Pong Game State"
359
+ />
360
+ </div>
361
+
362
+ <div className="relative flex min-h-screen flex-col items-center justify-center bg-zinc-900 p-4">
363
+ {/* UI Layer using Store Data */}
364
+ <div className="mb-4 flex w-full max-w-4xl items-center gap-4">
365
+ {/* Left flex spacer */}
366
+ <div className="flex-1" />
367
+
368
+ {/* Team 1 Avatars */}
369
+ <div className="flex w-20 items-center justify-end gap-2">
370
+ {host.players
371
+ .filter((p) => teamAssignments[p.id]?.team === "team1")
372
+ .map((player) => (
373
+ <PlayerAvatar key={player.id} player={player} size="sm" />
374
+ ))}
375
+ </div>
376
+
377
+ {/* Score */}
378
+ <div className="flex items-center gap-2 text-2xl font-bold">
379
+ <span style={{ color: TEAM1_COLOR }}>{scores.team1}</span>
380
+ <span className="text-white">-</span>
381
+ <span style={{ color: TEAM2_COLOR }}>{scores.team2}</span>
382
+ </div>
383
+
384
+ {/* Team 2 Avatars */}
385
+ <div className="flex w-20 items-center gap-2">
386
+ {host.players
387
+ .filter((p) => teamAssignments[p.id]?.team === "team2")
388
+ .map((player) => (
389
+ <PlayerAvatar key={player.id} player={player} size="sm" />
390
+ ))}
391
+ </div>
392
+
393
+ {/* Right flex spacer */}
394
+ <div className="flex-1" />
395
+ </div>
396
+
397
+ <canvas ref={canvasRef} className="rounded-lg border-2 border-white" />
398
+ </div>
399
+ </>
400
+ );
401
+ }
@@ -1,3 +1,4 @@
1
+ import "@air-jam/sdk/styles.css";
1
2
  import { StrictMode } from "react";
2
3
  import { createRoot } from "react-dom/client";
3
4
  import { BrowserRouter } from "react-router-dom";
@@ -9,5 +10,5 @@ createRoot(document.getElementById("root")!).render(
9
10
  <BrowserRouter>
10
11
  <App />
11
12
  </BrowserRouter>
12
- </StrictMode>
13
+ </StrictMode>,
13
14
  );
@@ -0,0 +1,80 @@
1
+ import { createAirJamStore } from "@air-jam/sdk";
2
+
3
+ export interface TeamAssignment {
4
+ team: "team1" | "team2";
5
+ position: "front" | "back";
6
+ }
7
+
8
+ export interface PongState {
9
+ scores: { team1: number; team2: number };
10
+ // Map controllerId -> { team, position }
11
+ teamAssignments: Record<string, TeamAssignment>;
12
+
13
+ actions: {
14
+ joinTeam: (team: "team1" | "team2", playerId?: string) => void;
15
+ resetGame: () => void;
16
+ scorePoint: (team: "team1" | "team2") => void;
17
+ };
18
+ }
19
+
20
+ // This store is automatically synced between the host and all controllers.
21
+ export const usePongStore = createAirJamStore<PongState>((set) => ({
22
+ scores: { team1: 0, team2: 0 },
23
+ teamAssignments: {},
24
+
25
+ actions: {
26
+ // Note: playerId is injected by the SDK on the Host side automatically
27
+ joinTeam: (team, playerId) => {
28
+ if (!playerId) return;
29
+ set((state) => {
30
+ const newAssignments = { ...state.teamAssignments };
31
+ const currentAssignment = newAssignments[playerId];
32
+
33
+ // If player is already on this team, don't change anything
34
+ if (currentAssignment && currentAssignment.team === team) {
35
+ return state;
36
+ }
37
+
38
+ // Remove player from their current team if they're switching
39
+ if (currentAssignment && currentAssignment.team !== team) {
40
+ delete newAssignments[playerId];
41
+ }
42
+
43
+ // Count players in the target team (excluding the current player)
44
+ const teamPlayers = Object.values(newAssignments).filter(
45
+ (assignment) => assignment.team === team,
46
+ );
47
+
48
+ // Enforce max 2 players per team
49
+ if (teamPlayers.length >= 2) {
50
+ // Team is full, don't allow assignment
51
+ return state;
52
+ }
53
+
54
+ // Assign position: first player = front, second = back
55
+ const position: "front" | "back" =
56
+ teamPlayers.length === 0 ? "front" : "back";
57
+
58
+ newAssignments[playerId] = { team, position };
59
+
60
+ return {
61
+ teamAssignments: newAssignments,
62
+ };
63
+ });
64
+ },
65
+
66
+ resetGame: () =>
67
+ set({
68
+ scores: { team1: 0, team2: 0 },
69
+ teamAssignments: {},
70
+ }),
71
+
72
+ scorePoint: (team) =>
73
+ set((state) => ({
74
+ scores: {
75
+ ...state.scores,
76
+ [team]: state.scores[team] + 1,
77
+ },
78
+ })),
79
+ },
80
+ }));
@@ -14,7 +14,8 @@
14
14
  "baseUrl": ".",
15
15
  "paths": {
16
16
  "@/*": ["./src/*"]
17
- }
17
+ },
18
+ "types": ["node"]
18
19
  },
19
- "include": ["src"]
20
+ "include": ["src", "vite.config.ts"]
20
21
  }
@@ -1,8 +1,11 @@
1
1
  import tailwindcss from "@tailwindcss/vite";
2
2
  import react from "@vitejs/plugin-react";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { defineConfig } from "vite";
5
6
 
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
6
9
  export default defineConfig({
7
10
  plugins: [react(), tailwindcss()],
8
11
  resolve: {
@@ -1,64 +0,0 @@
1
- import { ControllerShell, useAirJamController } from "@air-jam/sdk";
2
- import { useRef, useEffect } from "react";
3
-
4
- export function ControllerView() {
5
- const controller = useAirJamController();
6
- const directionRef = useRef(0);
7
-
8
- // Send input loop
9
- useEffect(() => {
10
- if (controller.connectionStatus !== "connected") return;
11
-
12
- let animationId: number;
13
- const loop = () => {
14
- controller.sendInput({
15
- direction: directionRef.current,
16
- action: false,
17
- });
18
- animationId = requestAnimationFrame(loop);
19
- };
20
- loop();
21
- return () => cancelAnimationFrame(animationId);
22
- }, [controller.connectionStatus, controller]);
23
-
24
- return (
25
- <ControllerShell
26
- connectionStatus={controller.connectionStatus}
27
- roomId={controller.roomId}
28
- >
29
- <div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-gray-900 p-4">
30
- <h2 className="text-2xl font-bold text-white">Your Controller</h2>
31
-
32
- <div className="flex flex-col gap-4">
33
- {/* Up button */}
34
- <button
35
- type="button"
36
- className="rounded-lg bg-blue-600 px-12 py-8 text-2xl font-bold text-white active:bg-blue-700"
37
- onTouchStart={() => (directionRef.current = -1)}
38
- onTouchEnd={() => (directionRef.current = 0)}
39
- onMouseDown={() => (directionRef.current = -1)}
40
- onMouseUp={() => (directionRef.current = 0)}
41
- >
42
- ▲ UP
43
- </button>
44
-
45
- {/* Down button */}
46
- <button
47
- type="button"
48
- className="rounded-lg bg-blue-600 px-12 py-8 text-2xl font-bold text-white active:bg-blue-700"
49
- onTouchStart={() => (directionRef.current = 1)}
50
- onTouchEnd={() => (directionRef.current = 0)}
51
- onMouseDown={() => (directionRef.current = 1)}
52
- onMouseUp={() => (directionRef.current = 0)}
53
- >
54
- ▼ DOWN
55
- </button>
56
- </div>
57
-
58
- <p className="text-sm text-gray-400">
59
- Hold to move your paddle
60
- </p>
61
- </div>
62
- </ControllerShell>
63
- );
64
- }