create-smore-game 2.2.0 → 2.4.0
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/README.md +159 -0
- package/package.json +1 -1
- package/templates.js +320 -67
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# create-smore-game
|
|
2
|
+
|
|
3
|
+
Scaffold a multiplayer party game for the S'MORE platform.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-smore-game my-game
|
|
11
|
+
cd my-game
|
|
12
|
+
npm install
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The CLI prompts you for two choices:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
create-smore-game
|
|
20
|
+
|
|
21
|
+
Screen (TV) template:
|
|
22
|
+
React + Phaser
|
|
23
|
+
React only
|
|
24
|
+
Vanilla JS
|
|
25
|
+
|
|
26
|
+
Controller (phone) template:
|
|
27
|
+
React
|
|
28
|
+
Vanilla JS
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After answering, your project is ready to run.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## What You Get
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
my-game/
|
|
39
|
+
├── screen/ # TV/display — game logic lives here
|
|
40
|
+
│ ├── src/
|
|
41
|
+
│ │ ├── App.tsx # Main game component
|
|
42
|
+
│ │ └── __tests__/ # Game tests with vitest
|
|
43
|
+
│ ├── package.json
|
|
44
|
+
│ └── vite.config.ts
|
|
45
|
+
├── controller/ # Phone/player input — stateless display + input
|
|
46
|
+
│ ├── src/
|
|
47
|
+
│ │ ├── App.tsx # Controller UI
|
|
48
|
+
│ │ └── __tests__/ # Controller tests
|
|
49
|
+
│ ├── package.json
|
|
50
|
+
│ └── vite.config.ts
|
|
51
|
+
├── dev/
|
|
52
|
+
│ ├── server.js # Local dev server with Socket.IO
|
|
53
|
+
│ ├── harness.html # Test screen + controllers together
|
|
54
|
+
│ └── controller-page.html
|
|
55
|
+
├── game.json # Game metadata (title, player count, etc.)
|
|
56
|
+
├── .env.example
|
|
57
|
+
└── package.json # npm workspaces root
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
### Commands
|
|
65
|
+
|
|
66
|
+
| Command | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `npm run dev` | Start dev server (screen + controller + harness) |
|
|
69
|
+
| `npm run dev:screen` | Screen only |
|
|
70
|
+
| `npm run dev:controller` | Controller only |
|
|
71
|
+
| `npm run build` | Production build |
|
|
72
|
+
| `npm run zip` | Build + package as `game.zip` for deployment |
|
|
73
|
+
|
|
74
|
+
### Dev Harness
|
|
75
|
+
|
|
76
|
+
Running `npm run dev` starts a local Socket.IO server and opens a harness page with the screen and one or more controller iframes side by side. You can add and remove players directly in the browser to test the full game flow without a real device.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Architecture: Stateless Controller Pattern
|
|
81
|
+
|
|
82
|
+
S'MORE games follow a strict separation of concerns:
|
|
83
|
+
|
|
84
|
+
- **Screen (TV)** — owns all game state and logic. It is the single source of truth.
|
|
85
|
+
- **Controller (Phone)** — a stateless input device. It only renders what the Screen tells it to render, and only sends raw user input back.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Screen → "Show vote UI with options A, B, C" → Controller renders buttons
|
|
89
|
+
Controller → "Player tapped A" → Screen processes vote, updates state
|
|
90
|
+
Screen → "Show results: A won" → Controller renders results
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This pattern keeps game logic centralized and makes controllers trivially simple to implement. For full API details, see the `@smoregg/sdk` documentation.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Templates
|
|
98
|
+
|
|
99
|
+
### Screen Templates
|
|
100
|
+
|
|
101
|
+
| Template | Best for |
|
|
102
|
+
|---|---|
|
|
103
|
+
| **React + Phaser** | Graphics-heavy games — sprites, animations, physics |
|
|
104
|
+
| **React** | UI-based games — cards, voting, text prompts |
|
|
105
|
+
| **Vanilla JS** | Minimal projects with no framework dependency |
|
|
106
|
+
|
|
107
|
+
### Controller Templates
|
|
108
|
+
|
|
109
|
+
| Template | Best for |
|
|
110
|
+
|---|---|
|
|
111
|
+
| **React** | Recommended for most games |
|
|
112
|
+
| **Vanilla JS** | Lightweight alternative |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Game Metadata
|
|
117
|
+
|
|
118
|
+
`game.json` at the project root describes your game to the S'MORE platform:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"id": "my-game",
|
|
123
|
+
"title": "My Game",
|
|
124
|
+
"description": "",
|
|
125
|
+
"version": "0.1.0",
|
|
126
|
+
"minPlayers": 2,
|
|
127
|
+
"maxPlayers": 8,
|
|
128
|
+
"categories": ["party"]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Edit this file before submitting. The `id` must be unique and contain only lowercase letters, numbers, and hyphens.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Building and Deploying
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm run zip
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This builds both the screen and controller workspaces, copies `game.json` into the output, and packages everything as `game.zip`. Submit this file to the S'MORE platform to deploy your game.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## SDK Documentation
|
|
147
|
+
|
|
148
|
+
The scaffolded project uses `@smoregg/sdk` for all platform communication. See the SDK package for the full API reference, including:
|
|
149
|
+
|
|
150
|
+
- Connecting screen and controller
|
|
151
|
+
- Sending and receiving typed events
|
|
152
|
+
- Managing player sessions
|
|
153
|
+
- Testing utilities (`createMockScreen`, `createMockController`)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/package.json
CHANGED
package/templates.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// pnpm-workspace.yaml is no longer generated.
|
|
4
4
|
// npm workspaces are configured in root package.json instead.
|
|
5
5
|
|
|
6
|
-
const SDK_VERSION = '^2.
|
|
6
|
+
const SDK_VERSION = '^2.3.0';
|
|
7
7
|
|
|
8
8
|
export function rootPackageJson(name) {
|
|
9
9
|
return JSON.stringify(
|
|
@@ -35,6 +35,160 @@ export function envExample() {
|
|
|
35
35
|
`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// ─── Test file templates ───
|
|
39
|
+
|
|
40
|
+
export function screenTestFile() {
|
|
41
|
+
return `import { describe, it, expect, beforeEach } from 'vitest';
|
|
42
|
+
import { createMockScreen } from '@smoregg/sdk/testing';
|
|
43
|
+
import type { GameEvents } from '../App';
|
|
44
|
+
|
|
45
|
+
describe('Game Screen', () => {
|
|
46
|
+
let screen: ReturnType<typeof createMockScreen<GameEvents>>;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
screen = createMockScreen<GameEvents>({ autoReady: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should broadcast score-update when player taps', async () => {
|
|
53
|
+
await screen.ready;
|
|
54
|
+
|
|
55
|
+
// Simulate a tap from player 0
|
|
56
|
+
screen.simulateEvent('tap', 0, { timestamp: Date.now() });
|
|
57
|
+
|
|
58
|
+
// Verify the screen would broadcast the score
|
|
59
|
+
// (Your game logic goes here)
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle player reconnection', async () => {
|
|
63
|
+
await screen.ready;
|
|
64
|
+
|
|
65
|
+
// Simulate player reconnect
|
|
66
|
+
screen.simulateControllerReconnect(0);
|
|
67
|
+
|
|
68
|
+
// In a stateless controller pattern, Screen should re-push
|
|
69
|
+
// the current view state to the reconnecting controller
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function controllerTestFile() {
|
|
76
|
+
return `import { describe, it, expect, beforeEach } from 'vitest';
|
|
77
|
+
import { createMockController } from '@smoregg/sdk/testing';
|
|
78
|
+
import type { GameEvents } from '../App';
|
|
79
|
+
|
|
80
|
+
describe('Game Controller', () => {
|
|
81
|
+
let controller: ReturnType<typeof createMockController<GameEvents>>;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
controller = createMockController<GameEvents>({ autoReady: true });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should send tap event on user input', async () => {
|
|
88
|
+
await controller.ready;
|
|
89
|
+
|
|
90
|
+
controller.send('tap', { timestamp: Date.now() });
|
|
91
|
+
|
|
92
|
+
const sends = controller.getSends();
|
|
93
|
+
expect(sends).toHaveLength(1);
|
|
94
|
+
expect(sends[0].event).toBe('tap');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should update display when Screen pushes score', async () => {
|
|
98
|
+
await controller.ready;
|
|
99
|
+
|
|
100
|
+
// Simulate Screen pushing a score update
|
|
101
|
+
controller.simulateEvent('score-update', { score: 42 });
|
|
102
|
+
|
|
103
|
+
// Your rendering logic would update based on this event
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function screenTestFileVanilla() {
|
|
110
|
+
return `import { describe, it, beforeEach } from 'vitest';
|
|
111
|
+
import { createMockScreen } from '@smoregg/sdk/testing';
|
|
112
|
+
|
|
113
|
+
interface GameEvents {
|
|
114
|
+
// Screen → Controller (view state)
|
|
115
|
+
'score-update': { score: number };
|
|
116
|
+
'personal-message': { text: string };
|
|
117
|
+
// Controller → Screen (input)
|
|
118
|
+
'tap': { timestamp: number };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
describe('Game Screen', () => {
|
|
122
|
+
let screen: ReturnType<typeof createMockScreen<GameEvents>>;
|
|
123
|
+
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
screen = createMockScreen<GameEvents>({ autoReady: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should broadcast score-update when player taps', async () => {
|
|
129
|
+
await screen.ready;
|
|
130
|
+
|
|
131
|
+
// Simulate a tap from player 0
|
|
132
|
+
screen.simulateEvent('tap', 0, { timestamp: Date.now() });
|
|
133
|
+
|
|
134
|
+
// Verify the screen would broadcast the score
|
|
135
|
+
// (Your game logic goes here)
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle player reconnection', async () => {
|
|
139
|
+
await screen.ready;
|
|
140
|
+
|
|
141
|
+
// Simulate player reconnect
|
|
142
|
+
screen.simulateControllerReconnect(0);
|
|
143
|
+
|
|
144
|
+
// In a stateless controller pattern, Screen should re-push
|
|
145
|
+
// the current view state to the reconnecting controller
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function controllerTestFileVanilla() {
|
|
152
|
+
return `import { describe, it, expect, beforeEach } from 'vitest';
|
|
153
|
+
import { createMockController } from '@smoregg/sdk/testing';
|
|
154
|
+
|
|
155
|
+
interface GameEvents {
|
|
156
|
+
// Screen → Controller (view state)
|
|
157
|
+
'score-update': { score: number };
|
|
158
|
+
'personal-message': { text: string };
|
|
159
|
+
// Controller → Screen (input)
|
|
160
|
+
'tap': { timestamp: number };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
describe('Game Controller', () => {
|
|
164
|
+
let controller: ReturnType<typeof createMockController<GameEvents>>;
|
|
165
|
+
|
|
166
|
+
beforeEach(() => {
|
|
167
|
+
controller = createMockController<GameEvents>({ autoReady: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should send tap event on user input', async () => {
|
|
171
|
+
await controller.ready;
|
|
172
|
+
|
|
173
|
+
controller.send('tap', { timestamp: Date.now() });
|
|
174
|
+
|
|
175
|
+
const sends = controller.getSends();
|
|
176
|
+
expect(sends).toHaveLength(1);
|
|
177
|
+
expect(sends[0].event).toBe('tap');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should update display when Screen pushes score', async () => {
|
|
181
|
+
await controller.ready;
|
|
182
|
+
|
|
183
|
+
// Simulate Screen pushing a score update
|
|
184
|
+
controller.simulateEvent('score-update', { score: 42 });
|
|
185
|
+
|
|
186
|
+
// Your rendering logic would update based on this event
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
38
192
|
// ─── Screen templates ───
|
|
39
193
|
|
|
40
194
|
const screenTsconfig = JSON.stringify(
|
|
@@ -130,6 +284,7 @@ export function screenReactPhaser(gameId) {
|
|
|
130
284
|
scripts: {
|
|
131
285
|
dev: "vite",
|
|
132
286
|
build: "tsc && vite build",
|
|
287
|
+
test: "vitest run",
|
|
133
288
|
},
|
|
134
289
|
dependencies: {
|
|
135
290
|
react: "^18.3.1",
|
|
@@ -143,6 +298,7 @@ export function screenReactPhaser(gameId) {
|
|
|
143
298
|
"@vitejs/plugin-react": "^4.3.0",
|
|
144
299
|
typescript: "^5.5.0",
|
|
145
300
|
vite: "^5.4.0",
|
|
301
|
+
vitest: "^1.6.0",
|
|
146
302
|
},
|
|
147
303
|
},
|
|
148
304
|
null,
|
|
@@ -163,8 +319,10 @@ import Phaser from 'phaser';
|
|
|
163
319
|
import { GameScene } from './scenes/GameScene';
|
|
164
320
|
|
|
165
321
|
interface GameEvents {
|
|
322
|
+
// Screen → Controller (view state)
|
|
166
323
|
'score-update': { score: number };
|
|
167
324
|
'personal-message': { text: string };
|
|
325
|
+
// Controller → Screen (input)
|
|
168
326
|
'tap': { timestamp: number };
|
|
169
327
|
}
|
|
170
328
|
|
|
@@ -185,6 +343,7 @@ export function App() {
|
|
|
185
343
|
const gameRef = useRef<Phaser.Game | null>(null);
|
|
186
344
|
const [roomCode, setRoomCode] = useState('');
|
|
187
345
|
const [controllers, setControllers] = useState<ControllerInfo[]>([]);
|
|
346
|
+
const [tapCount, setTapCount] = useState(0);
|
|
188
347
|
|
|
189
348
|
useEffect(() => {
|
|
190
349
|
let mounted = true;
|
|
@@ -238,6 +397,12 @@ export function App() {
|
|
|
238
397
|
console.log('Player', playerIndex, 'tapped:', data);
|
|
239
398
|
// Forward input to Phaser scene
|
|
240
399
|
gameRef.current?.events.emit('player-tap', { playerIndex, ...data });
|
|
400
|
+
// Broadcast updated score back to controllers (Screen is source of truth)
|
|
401
|
+
setTapCount((prev) => {
|
|
402
|
+
const newCount = prev + 1;
|
|
403
|
+
screen.broadcast('score-update', { score: newCount });
|
|
404
|
+
return newCount;
|
|
405
|
+
});
|
|
241
406
|
});
|
|
242
407
|
|
|
243
408
|
return () => {
|
|
@@ -305,6 +470,7 @@ export class GameScene extends Phaser.Scene {
|
|
|
305
470
|
}
|
|
306
471
|
}
|
|
307
472
|
`,
|
|
473
|
+
"src/__tests__/game.test.ts": screenTestFile(),
|
|
308
474
|
};
|
|
309
475
|
}
|
|
310
476
|
|
|
@@ -321,6 +487,7 @@ export function screenReact(gameId) {
|
|
|
321
487
|
scripts: {
|
|
322
488
|
dev: "vite",
|
|
323
489
|
build: "tsc && vite build",
|
|
490
|
+
test: "vitest run",
|
|
324
491
|
},
|
|
325
492
|
dependencies: {
|
|
326
493
|
react: "^18.3.1",
|
|
@@ -333,6 +500,7 @@ export function screenReact(gameId) {
|
|
|
333
500
|
"@vitejs/plugin-react": "^4.3.0",
|
|
334
501
|
typescript: "^5.5.0",
|
|
335
502
|
vite: "^5.4.0",
|
|
503
|
+
vitest: "^1.6.0",
|
|
336
504
|
},
|
|
337
505
|
},
|
|
338
506
|
null,
|
|
@@ -351,8 +519,10 @@ import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
|
|
|
351
519
|
import { useEffect, useRef, useState } from 'react';
|
|
352
520
|
|
|
353
521
|
interface GameEvents {
|
|
522
|
+
// Screen → Controller (view state)
|
|
354
523
|
'score-update': { score: number };
|
|
355
524
|
'personal-message': { text: string };
|
|
525
|
+
// Controller → Screen (input)
|
|
356
526
|
'tap': { timestamp: number };
|
|
357
527
|
}
|
|
358
528
|
|
|
@@ -373,7 +543,7 @@ export function App() {
|
|
|
373
543
|
const [roomCode, setRoomCode] = useState('');
|
|
374
544
|
const [controllers, setControllers] = useState<ControllerInfo[]>([]);
|
|
375
545
|
const [taps, setTaps] = useState<{ playerIndex: number; time: number }[]>([]);
|
|
376
|
-
const [
|
|
546
|
+
const [tapCount, setTapCount] = useState(0);
|
|
377
547
|
|
|
378
548
|
useEffect(() => {
|
|
379
549
|
let mounted = true;
|
|
@@ -413,11 +583,6 @@ export function App() {
|
|
|
413
583
|
setControllers([...screen.controllers]);
|
|
414
584
|
});
|
|
415
585
|
|
|
416
|
-
screen.onCustomStateChange((playerIndex, state) => {
|
|
417
|
-
if (!mounted) return;
|
|
418
|
-
setCustomStates((prev) => ({ ...prev, [playerIndex]: state }));
|
|
419
|
-
});
|
|
420
|
-
|
|
421
586
|
screenRef.current = screen;
|
|
422
587
|
|
|
423
588
|
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
@@ -425,6 +590,12 @@ export function App() {
|
|
|
425
590
|
screen.on('tap', (playerIndex, data) => {
|
|
426
591
|
if (!mounted) return;
|
|
427
592
|
setTaps((prev) => [...prev.slice(-9), { playerIndex, time: Date.now() }]);
|
|
593
|
+
// Broadcast updated score back to controllers (Screen is source of truth)
|
|
594
|
+
setTapCount((prev) => {
|
|
595
|
+
const newCount = prev + 1;
|
|
596
|
+
screen.broadcast('score-update', { score: newCount });
|
|
597
|
+
return newCount;
|
|
598
|
+
});
|
|
428
599
|
});
|
|
429
600
|
|
|
430
601
|
return () => {
|
|
@@ -434,11 +605,6 @@ export function App() {
|
|
|
434
605
|
};
|
|
435
606
|
}, []);
|
|
436
607
|
|
|
437
|
-
// Example: send score update to all players
|
|
438
|
-
const handleBroadcastScore = () => {
|
|
439
|
-
screenRef.current?.broadcast('score-update', { score: 100 });
|
|
440
|
-
};
|
|
441
|
-
|
|
442
608
|
// Example: send to specific player
|
|
443
609
|
const handleSendToPlayer = (playerIndex: number) => {
|
|
444
610
|
screenRef.current?.sendToController(playerIndex, 'personal-message', { text: 'Hello!' });
|
|
@@ -476,6 +642,7 @@ export function App() {
|
|
|
476
642
|
);
|
|
477
643
|
}
|
|
478
644
|
`,
|
|
645
|
+
"src/__tests__/game.test.ts": screenTestFile(),
|
|
479
646
|
};
|
|
480
647
|
}
|
|
481
648
|
|
|
@@ -492,6 +659,7 @@ export function screenVanilla(gameId) {
|
|
|
492
659
|
scripts: {
|
|
493
660
|
dev: "vite",
|
|
494
661
|
build: "tsc && vite build",
|
|
662
|
+
test: "vitest run",
|
|
495
663
|
},
|
|
496
664
|
dependencies: {
|
|
497
665
|
"@smoregg/sdk": SDK_VERSION, // TODO: auto-detect or update on release
|
|
@@ -499,6 +667,7 @@ export function screenVanilla(gameId) {
|
|
|
499
667
|
devDependencies: {
|
|
500
668
|
typescript: "^5.5.0",
|
|
501
669
|
vite: "^5.4.0",
|
|
670
|
+
vitest: "^1.6.0",
|
|
502
671
|
},
|
|
503
672
|
},
|
|
504
673
|
null,
|
|
@@ -535,8 +704,10 @@ export function screenVanilla(gameId) {
|
|
|
535
704
|
import type { GameResults } from '@smoregg/sdk';
|
|
536
705
|
|
|
537
706
|
interface GameEvents {
|
|
707
|
+
// Screen → Controller (view state)
|
|
538
708
|
'score-update': { score: number };
|
|
539
709
|
'personal-message': { text: string };
|
|
710
|
+
// Controller → Screen (input)
|
|
540
711
|
'tap': { timestamp: number };
|
|
541
712
|
}
|
|
542
713
|
|
|
@@ -590,6 +761,8 @@ screen.onAllReady(() => {
|
|
|
590
761
|
|
|
591
762
|
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
592
763
|
// destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
|
|
764
|
+
let tapCount = 0;
|
|
765
|
+
|
|
593
766
|
screen.on('tap', (playerIndex, data) => {
|
|
594
767
|
const line = document.createElement('div');
|
|
595
768
|
line.textContent = \`Player \${playerIndex} tapped\`;
|
|
@@ -598,6 +771,9 @@ screen.on('tap', (playerIndex, data) => {
|
|
|
598
771
|
while (logEl.children.length > 10) {
|
|
599
772
|
logEl.removeChild(logEl.firstChild!);
|
|
600
773
|
}
|
|
774
|
+
// Broadcast updated score back to controllers (Screen is source of truth)
|
|
775
|
+
tapCount++;
|
|
776
|
+
screen.broadcast('score-update', { score: tapCount });
|
|
601
777
|
});
|
|
602
778
|
|
|
603
779
|
function updateStatus() {
|
|
@@ -606,8 +782,7 @@ function updateStatus() {
|
|
|
606
782
|
}
|
|
607
783
|
|
|
608
784
|
// Example functions (can be called from console for testing):
|
|
609
|
-
// screen.
|
|
610
|
-
// screen.sendToController(0, 'message', { text: 'Hello!' });
|
|
785
|
+
// screen.sendToController(0, 'personal-message', { text: 'Hello!' });
|
|
611
786
|
// const results: GameResults = {
|
|
612
787
|
// scores: { 0: 50, 1: 75 },
|
|
613
788
|
// // winner: 0,
|
|
@@ -615,6 +790,7 @@ function updateStatus() {
|
|
|
615
790
|
// };
|
|
616
791
|
// screen.gameOver(results);
|
|
617
792
|
`,
|
|
793
|
+
"src/__tests__/game.test.ts": screenTestFileVanilla(),
|
|
618
794
|
};
|
|
619
795
|
}
|
|
620
796
|
|
|
@@ -666,6 +842,7 @@ export function controllerReact(gameId) {
|
|
|
666
842
|
scripts: {
|
|
667
843
|
dev: "vite",
|
|
668
844
|
build: "tsc && vite build",
|
|
845
|
+
test: "vitest run",
|
|
669
846
|
},
|
|
670
847
|
dependencies: {
|
|
671
848
|
react: "^18.3.1",
|
|
@@ -678,6 +855,7 @@ export function controllerReact(gameId) {
|
|
|
678
855
|
"@vitejs/plugin-react": "^4.3.0",
|
|
679
856
|
typescript: "^5.5.0",
|
|
680
857
|
vite: "^5.4.0",
|
|
858
|
+
vitest: "^1.6.0",
|
|
681
859
|
},
|
|
682
860
|
},
|
|
683
861
|
null,
|
|
@@ -725,11 +903,27 @@ import type { Controller, ControllerInfo } from '@smoregg/sdk';
|
|
|
725
903
|
import { useEffect, useRef, useState } from 'react';
|
|
726
904
|
|
|
727
905
|
interface GameEvents {
|
|
906
|
+
// Screen → Controller (view state)
|
|
728
907
|
'score-update': { score: number };
|
|
729
908
|
'personal-message': { text: string };
|
|
909
|
+
// Controller → Screen (input)
|
|
730
910
|
'tap': { timestamp: number };
|
|
731
911
|
}
|
|
732
912
|
|
|
913
|
+
/**
|
|
914
|
+
* ARCHITECTURE: Stateless Controller Pattern
|
|
915
|
+
*
|
|
916
|
+
* The controller is a stateless display + input device:
|
|
917
|
+
* - Render ONLY what the Screen sends (via controller.on())
|
|
918
|
+
* - Send ONLY user input to Screen (via controller.send())
|
|
919
|
+
* - Do NOT store or compute game state here
|
|
920
|
+
*
|
|
921
|
+
* Data flow:
|
|
922
|
+
* Controller → Screen: controller.send('tap', { timestamp }) (input only)
|
|
923
|
+
* Screen → Controller: 'score-update' { score } (view state)
|
|
924
|
+
* Controller renders: score from Screen (source of truth)
|
|
925
|
+
*/
|
|
926
|
+
|
|
733
927
|
export function App() {
|
|
734
928
|
const controllerRef = useRef<Controller | null>(null);
|
|
735
929
|
const [myIndex, setMyIndex] = useState(-1);
|
|
@@ -776,7 +970,6 @@ export function App() {
|
|
|
776
970
|
|
|
777
971
|
const handleTap = () => {
|
|
778
972
|
controllerRef.current?.send('tap', { timestamp: Date.now() });
|
|
779
|
-
setCount((c) => c + 1);
|
|
780
973
|
};
|
|
781
974
|
|
|
782
975
|
return (
|
|
@@ -787,7 +980,7 @@ export function App() {
|
|
|
787
980
|
touchAction: 'manipulation', userSelect: 'none',
|
|
788
981
|
}}>
|
|
789
982
|
{isReady && (
|
|
790
|
-
<div style={{ fontSize: '16px', opacity: 0.6 }}>{me?.
|
|
983
|
+
<div style={{ fontSize: '16px', opacity: 0.6 }}>{me?.nickname ?? \`Player \${myIndex}\`}</div>
|
|
791
984
|
)}
|
|
792
985
|
<div style={{ fontSize: '48px', fontWeight: 'bold' }}>{count}</div>
|
|
793
986
|
<button
|
|
@@ -805,6 +998,7 @@ export function App() {
|
|
|
805
998
|
);
|
|
806
999
|
}
|
|
807
1000
|
`,
|
|
1001
|
+
"src/__tests__/game.test.ts": controllerTestFile(),
|
|
808
1002
|
};
|
|
809
1003
|
}
|
|
810
1004
|
|
|
@@ -819,6 +1013,7 @@ export function controllerVanilla(gameId) {
|
|
|
819
1013
|
scripts: {
|
|
820
1014
|
dev: "vite",
|
|
821
1015
|
build: "tsc && vite build",
|
|
1016
|
+
test: "vitest run",
|
|
822
1017
|
},
|
|
823
1018
|
dependencies: {
|
|
824
1019
|
"@smoregg/sdk": SDK_VERSION, // TODO: auto-detect or update on release
|
|
@@ -826,6 +1021,7 @@ export function controllerVanilla(gameId) {
|
|
|
826
1021
|
devDependencies: {
|
|
827
1022
|
typescript: "^5.5.0",
|
|
828
1023
|
vite: "^5.4.0",
|
|
1024
|
+
vitest: "^1.6.0",
|
|
829
1025
|
},
|
|
830
1026
|
},
|
|
831
1027
|
null,
|
|
@@ -884,12 +1080,26 @@ export default defineConfig({
|
|
|
884
1080
|
"src/main.ts": `import { createController } from '@smoregg/sdk';
|
|
885
1081
|
|
|
886
1082
|
interface GameEvents {
|
|
1083
|
+
// Screen → Controller (view state)
|
|
887
1084
|
'score-update': { score: number };
|
|
888
1085
|
'personal-message': { text: string };
|
|
1086
|
+
// Controller → Screen (input)
|
|
889
1087
|
'tap': { timestamp: number };
|
|
890
1088
|
}
|
|
891
1089
|
|
|
892
|
-
|
|
1090
|
+
/**
|
|
1091
|
+
* ARCHITECTURE: Stateless Controller Pattern
|
|
1092
|
+
*
|
|
1093
|
+
* The controller is a stateless display + input device:
|
|
1094
|
+
* - Render ONLY what the Screen sends (via controller.on())
|
|
1095
|
+
* - Send ONLY user input to Screen (via controller.send())
|
|
1096
|
+
* - Do NOT store or compute game state here
|
|
1097
|
+
*
|
|
1098
|
+
* Data flow:
|
|
1099
|
+
* Controller → Screen: controller.send('tap', { timestamp }) (input only)
|
|
1100
|
+
* Screen → Controller: 'score-update' { score } (view state)
|
|
1101
|
+
* Controller renders: score from Screen (source of truth)
|
|
1102
|
+
*/
|
|
893
1103
|
|
|
894
1104
|
const playerInfoEl = document.getElementById('player-info')!;
|
|
895
1105
|
const countEl = document.getElementById('count')!;
|
|
@@ -905,12 +1115,11 @@ const controller = createController<GameEvents>({ debug: true });
|
|
|
905
1115
|
// To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
|
|
906
1116
|
|
|
907
1117
|
controller.onAllReady(() => {
|
|
908
|
-
playerInfoEl.textContent = controller.me?.
|
|
1118
|
+
playerInfoEl.textContent = controller.me?.nickname ?? \`Player \${controller.myPlayerIndex}\`;
|
|
909
1119
|
});
|
|
910
1120
|
|
|
911
1121
|
controller.on('score-update', (data) => {
|
|
912
|
-
|
|
913
|
-
countEl.textContent = String(count);
|
|
1122
|
+
countEl.textContent = String(data.score);
|
|
914
1123
|
});
|
|
915
1124
|
|
|
916
1125
|
controller.on('personal-message', (data) => {
|
|
@@ -919,10 +1128,9 @@ controller.on('personal-message', (data) => {
|
|
|
919
1128
|
|
|
920
1129
|
tapBtn.addEventListener('pointerdown', () => {
|
|
921
1130
|
controller.send('tap', { timestamp: Date.now() });
|
|
922
|
-
count++;
|
|
923
|
-
countEl.textContent = String(count);
|
|
924
1131
|
});
|
|
925
1132
|
`,
|
|
1133
|
+
"src/__tests__/game.test.ts": controllerTestFileVanilla(),
|
|
926
1134
|
};
|
|
927
1135
|
}
|
|
928
1136
|
|
|
@@ -1004,7 +1212,6 @@ const room = {
|
|
|
1004
1212
|
gameId: '',
|
|
1005
1213
|
status: 'waiting',
|
|
1006
1214
|
readyIds: new Set(),
|
|
1007
|
-
customStates: new Map(), // playerIndex -> state object
|
|
1008
1215
|
};
|
|
1009
1216
|
|
|
1010
1217
|
function toPlayerDTO(p) {
|
|
@@ -1055,6 +1262,16 @@ async function main() {
|
|
|
1055
1262
|
app.get('/', servePage(join(__dirname, 'index.html')));
|
|
1056
1263
|
app.get('/controller', servePage(join(__dirname, 'controller.html')));
|
|
1057
1264
|
|
|
1265
|
+
// Serve game.json for dev harness (minPlayers, maxPlayers, title, etc.)
|
|
1266
|
+
app.get('/game.json', (_req, res) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const gameJson = JSON.parse(readFileSync(join(__dirname, '..', 'game.json'), 'utf-8'));
|
|
1269
|
+
res.json(gameJson);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
res.json({ id: 'dev-game', title: 'Dev Game', minPlayers: 1, maxPlayers: 8 });
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1058
1275
|
// ── Socket handling ──
|
|
1059
1276
|
|
|
1060
1277
|
io.on('connection', (socket) => {
|
|
@@ -1151,20 +1368,6 @@ async function main() {
|
|
|
1151
1368
|
io.to(room.code).emit('smore:game-over', data);
|
|
1152
1369
|
});
|
|
1153
1370
|
|
|
1154
|
-
socket.on('smore:set-custom-state', (data) => {
|
|
1155
|
-
if (socket.role !== 'player') return;
|
|
1156
|
-
const prev = room.customStates.get(socket.playerIndex) || {};
|
|
1157
|
-
const next = Object.assign({}, prev, data && typeof data === 'object' ? data : {});
|
|
1158
|
-
room.customStates.set(socket.playerIndex, next);
|
|
1159
|
-
io.to(room.code).emit('smore:custom-state-changed', { playerIndex: socket.playerIndex, state: next });
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
socket.on('smore:get-custom-states', () => {
|
|
1163
|
-
const states = {};
|
|
1164
|
-
room.customStates.forEach((state, playerIndex) => { states[playerIndex] = state; });
|
|
1165
|
-
socket.emit('smore:custom-states', { states });
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
1371
|
socket.on('smore:return-to-selection', () => {
|
|
1169
1372
|
room.status = 'waiting';
|
|
1170
1373
|
room.gameId = '';
|
|
@@ -1186,7 +1389,6 @@ async function main() {
|
|
|
1186
1389
|
room.gameId = '';
|
|
1187
1390
|
room.status = 'waiting';
|
|
1188
1391
|
room.readyIds = new Set();
|
|
1189
|
-
room.customStates = new Map();
|
|
1190
1392
|
console.log(' [reset] Room reset by host');
|
|
1191
1393
|
if (typeof callback === 'function') callback({ success: true });
|
|
1192
1394
|
});
|
|
@@ -1343,6 +1545,10 @@ export function devHarness(gameId) {
|
|
|
1343
1545
|
.btn-primary:disabled:hover { background: #4f46e5; }
|
|
1344
1546
|
.btn-danger { background: #dc2626; border-color: #dc2626; color: #fff; }
|
|
1345
1547
|
.btn-danger:hover { background: #b91c1c; }
|
|
1548
|
+
.btn-start { background: #16a34a; border-color: #16a34a; color: #fff; font-size: 13px; padding: 5px 16px; }
|
|
1549
|
+
.btn-start:hover { background: #15803d; }
|
|
1550
|
+
.btn-start:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
1551
|
+
.btn-start:disabled:hover { background: #16a34a; }
|
|
1346
1552
|
|
|
1347
1553
|
/* Main layout */
|
|
1348
1554
|
.main { display: flex; height: calc(100% - 48px); }
|
|
@@ -1418,6 +1624,7 @@ export function devHarness(gameId) {
|
|
|
1418
1624
|
<div class="badge-muted" id="playerCount">0 players</div>
|
|
1419
1625
|
<div class="phase-badge phase-lobby" id="phaseBadge">Lobby</div>
|
|
1420
1626
|
<div class="topbar-spacer"></div>
|
|
1627
|
+
<button class="btn btn-start" id="startGameBtn" disabled>▶ Start</button>
|
|
1421
1628
|
<button class="btn btn-danger" id="endGameBtn" style="display:none;">End Game</button>
|
|
1422
1629
|
<button class="btn" id="resetBtn">Reset</button>
|
|
1423
1630
|
<button class="btn btn-primary" id="addPlayerBtn">+ Player</button>
|
|
@@ -1456,6 +1663,10 @@ export function devHarness(gameId) {
|
|
|
1456
1663
|
var screenIframe = null;
|
|
1457
1664
|
var screenReady = false;
|
|
1458
1665
|
var gamePhase = 'lobby';
|
|
1666
|
+
var gameStarted = false;
|
|
1667
|
+
var pendingGameReady = []; // { socket, data }
|
|
1668
|
+
var minPlayers = 1;
|
|
1669
|
+
var gameConfig = {};
|
|
1459
1670
|
|
|
1460
1671
|
// ── DOM refs ──
|
|
1461
1672
|
var roomCodeEl = document.getElementById('roomCode');
|
|
@@ -1467,6 +1678,7 @@ export function devHarness(gameId) {
|
|
|
1467
1678
|
var controllerGrid = document.getElementById('controllerGrid');
|
|
1468
1679
|
var emptyState = document.getElementById('emptyState');
|
|
1469
1680
|
var addPlayerBtn = document.getElementById('addPlayerBtn');
|
|
1681
|
+
var startGameBtn = document.getElementById('startGameBtn');
|
|
1470
1682
|
var endGameBtn = document.getElementById('endGameBtn');
|
|
1471
1683
|
var resetBtn = document.getElementById('resetBtn');
|
|
1472
1684
|
|
|
@@ -1478,12 +1690,19 @@ export function devHarness(gameId) {
|
|
|
1478
1690
|
phaseBadgeEl.className = 'phase-badge phase-' + phase;
|
|
1479
1691
|
addPlayerBtn.disabled = (phase === 'playing');
|
|
1480
1692
|
endGameBtn.style.display = (phase === 'playing') ? 'inline-block' : 'none';
|
|
1693
|
+
startGameBtn.style.display = (phase === 'lobby') ? 'inline-block' : 'none';
|
|
1694
|
+
updateStartButton();
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function updateStartButton() {
|
|
1698
|
+
startGameBtn.disabled = gameStarted || serverPlayers.length < minPlayers;
|
|
1481
1699
|
}
|
|
1482
1700
|
|
|
1483
1701
|
// ── Helpers ──
|
|
1484
1702
|
function updatePlayerCount() {
|
|
1485
1703
|
var count = serverPlayers.length;
|
|
1486
1704
|
playerCountEl.textContent = count + ' player' + (count !== 1 ? 's' : '');
|
|
1705
|
+
updateStartButton();
|
|
1487
1706
|
}
|
|
1488
1707
|
|
|
1489
1708
|
function updateEmptyState() {
|
|
@@ -1561,7 +1780,7 @@ export function devHarness(gameId) {
|
|
|
1561
1780
|
var screenSysEvents = [
|
|
1562
1781
|
'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
|
|
1563
1782
|
'smore:player-character-updated', 'smore:rate-limited', 'smore:game-over', 'smore:all-ready',
|
|
1564
|
-
'smore:self-disconnected', 'smore:self-reconnected'
|
|
1783
|
+
'smore:self-disconnected', 'smore:self-reconnected'
|
|
1565
1784
|
];
|
|
1566
1785
|
screenSysEvents.forEach(function(sysEvent) {
|
|
1567
1786
|
screenSocket.on(sysEvent, function(data) {
|
|
@@ -1643,7 +1862,7 @@ export function devHarness(gameId) {
|
|
|
1643
1862
|
'smore:game-over',
|
|
1644
1863
|
'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
|
|
1645
1864
|
'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
|
|
1646
|
-
'smore:self-disconnected', 'smore:self-reconnected'
|
|
1865
|
+
'smore:self-disconnected', 'smore:self-reconnected'
|
|
1647
1866
|
];
|
|
1648
1867
|
ctrlSysEvents.forEach(function(sysEvent) {
|
|
1649
1868
|
controllerSocket.on(sysEvent, function(data) {
|
|
@@ -1705,9 +1924,13 @@ export function devHarness(gameId) {
|
|
|
1705
1924
|
return;
|
|
1706
1925
|
}
|
|
1707
1926
|
|
|
1708
|
-
//
|
|
1927
|
+
// Buffer game-ready until game is started (prevents auto-start)
|
|
1709
1928
|
if (event === 'smore:game-ready') {
|
|
1710
|
-
|
|
1929
|
+
if (gameStarted) {
|
|
1930
|
+
screenSocket.emit('smore:game-ready', data || {});
|
|
1931
|
+
} else {
|
|
1932
|
+
pendingGameReady.push({ socket: screenSocket, data: data });
|
|
1933
|
+
}
|
|
1711
1934
|
return;
|
|
1712
1935
|
}
|
|
1713
1936
|
|
|
@@ -1753,19 +1976,13 @@ export function devHarness(gameId) {
|
|
|
1753
1976
|
setPhase('lobby');
|
|
1754
1977
|
}
|
|
1755
1978
|
|
|
1756
|
-
//
|
|
1979
|
+
// Buffer game-ready until game is started (prevents auto-start)
|
|
1757
1980
|
if (event === 'smore:game-ready') {
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
if (event === 'smore:set-custom-state') {
|
|
1764
|
-
player.socket.emit('smore:set-custom-state', data || {});
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
if (event === 'smore:get-custom-states') {
|
|
1768
|
-
player.socket.emit('smore:get-custom-states');
|
|
1981
|
+
if (gameStarted) {
|
|
1982
|
+
player.socket.emit('smore:game-ready', data || {});
|
|
1983
|
+
} else {
|
|
1984
|
+
pendingGameReady.push({ socket: player.socket, data: data });
|
|
1985
|
+
}
|
|
1769
1986
|
return;
|
|
1770
1987
|
}
|
|
1771
1988
|
|
|
@@ -1784,6 +2001,9 @@ export function devHarness(gameId) {
|
|
|
1784
2001
|
|
|
1785
2002
|
// ── End Game ──
|
|
1786
2003
|
function endGame() {
|
|
2004
|
+
gameStarted = false;
|
|
2005
|
+
pendingGameReady = [];
|
|
2006
|
+
|
|
1787
2007
|
// Broadcast phase-change to ALL controllers (including phone) via server relay
|
|
1788
2008
|
screenSocket.emit('phase-change', { phase: 'lobby' });
|
|
1789
2009
|
|
|
@@ -1804,6 +2024,9 @@ export function devHarness(gameId) {
|
|
|
1804
2024
|
|
|
1805
2025
|
// ── Reset ──
|
|
1806
2026
|
function resetAll() {
|
|
2027
|
+
gameStarted = false;
|
|
2028
|
+
pendingGameReady = [];
|
|
2029
|
+
|
|
1807
2030
|
// 1. Disconnect all local iframe player sockets
|
|
1808
2031
|
players.forEach(function(p) {
|
|
1809
2032
|
if (p.socket) p.socket.disconnect();
|
|
@@ -1852,6 +2075,24 @@ export function devHarness(gameId) {
|
|
|
1852
2075
|
if (!addPlayerBtn.disabled) addController();
|
|
1853
2076
|
});
|
|
1854
2077
|
|
|
2078
|
+
startGameBtn.addEventListener('click', function() {
|
|
2079
|
+
if (gameStarted || serverPlayers.length < minPlayers) return;
|
|
2080
|
+
gameStarted = true;
|
|
2081
|
+
|
|
2082
|
+
// Tell server to start the game (triggers smore:game-selected + smore:game-started)
|
|
2083
|
+
screenSocket.emit('smore:select-game', { gameId: gameConfig.id || 'dev-game' });
|
|
2084
|
+
|
|
2085
|
+
// Flush all buffered game-ready messages after server processes select-game
|
|
2086
|
+
setTimeout(function() {
|
|
2087
|
+
pendingGameReady.forEach(function(entry) {
|
|
2088
|
+
entry.socket.emit('smore:game-ready', entry.data || {});
|
|
2089
|
+
});
|
|
2090
|
+
pendingGameReady = [];
|
|
2091
|
+
}, 50);
|
|
2092
|
+
|
|
2093
|
+
setPhase('playing');
|
|
2094
|
+
});
|
|
2095
|
+
|
|
1855
2096
|
endGameBtn.addEventListener('click', endGame);
|
|
1856
2097
|
resetBtn.addEventListener('click', resetAll);
|
|
1857
2098
|
|
|
@@ -1871,6 +2112,13 @@ export function devHarness(gameId) {
|
|
|
1871
2112
|
});
|
|
1872
2113
|
|
|
1873
2114
|
// ── Boot ──
|
|
2115
|
+
// Fetch game.json for minPlayers config
|
|
2116
|
+
fetch('/game.json').then(function(r) { return r.json(); }).then(function(cfg) {
|
|
2117
|
+
gameConfig = cfg;
|
|
2118
|
+
minPlayers = cfg.minPlayers || 1;
|
|
2119
|
+
updateStartButton();
|
|
2120
|
+
}).catch(function() {});
|
|
2121
|
+
|
|
1874
2122
|
initScreen();
|
|
1875
2123
|
setTimeout(function() { addController(); }, 500);
|
|
1876
2124
|
})();
|
|
@@ -1945,6 +2193,8 @@ export function devControllerPage(gameId) {
|
|
|
1945
2193
|
var roomCode = '';
|
|
1946
2194
|
var players = [];
|
|
1947
2195
|
var iframeReady = false;
|
|
2196
|
+
var gameStarted = false;
|
|
2197
|
+
var pendingGameReady = null;
|
|
1948
2198
|
|
|
1949
2199
|
// Connect and join room
|
|
1950
2200
|
socket = io();
|
|
@@ -1977,12 +2227,21 @@ export function devControllerPage(gameId) {
|
|
|
1977
2227
|
}
|
|
1978
2228
|
});
|
|
1979
2229
|
|
|
2230
|
+
// Listen for game-started to flush pending game-ready
|
|
2231
|
+
socket.on('smore:game-started', function() {
|
|
2232
|
+
gameStarted = true;
|
|
2233
|
+
if (pendingGameReady !== null) {
|
|
2234
|
+
socket.emit('smore:game-ready', pendingGameReady);
|
|
2235
|
+
pendingGameReady = null;
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
|
|
1980
2239
|
// Forward system events to iframe
|
|
1981
2240
|
var sysEvents = [
|
|
1982
2241
|
'smore:game-over',
|
|
1983
2242
|
'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
|
|
1984
2243
|
'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
|
|
1985
|
-
'smore:self-disconnected', 'smore:self-reconnected'
|
|
2244
|
+
'smore:self-disconnected', 'smore:self-reconnected'
|
|
1986
2245
|
];
|
|
1987
2246
|
sysEvents.forEach(function(sysEvent) {
|
|
1988
2247
|
socket.on(sysEvent, function(data) {
|
|
@@ -2038,19 +2297,13 @@ export function devControllerPage(gameId) {
|
|
|
2038
2297
|
var data = msg.payload.data;
|
|
2039
2298
|
var ackId = msg.payload.ackId;
|
|
2040
2299
|
|
|
2041
|
-
//
|
|
2300
|
+
// Buffer game-ready until game is started (prevents auto-start)
|
|
2042
2301
|
if (event === 'smore:game-ready') {
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
if (event === 'smore:set-custom-state') {
|
|
2049
|
-
socket.emit('smore:set-custom-state', data || {});
|
|
2050
|
-
return;
|
|
2051
|
-
}
|
|
2052
|
-
if (event === 'smore:get-custom-states') {
|
|
2053
|
-
socket.emit('smore:get-custom-states');
|
|
2302
|
+
if (gameStarted) {
|
|
2303
|
+
socket.emit('smore:game-ready', data || {});
|
|
2304
|
+
} else {
|
|
2305
|
+
pendingGameReady = data || {};
|
|
2306
|
+
}
|
|
2054
2307
|
return;
|
|
2055
2308
|
}
|
|
2056
2309
|
|