create-smore-game 2.0.0 → 2.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/templates.js +116 -59
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smore-game",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-smore-game": "./index.js"
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.0.0';
6
+ const SDK_VERSION = '^2.2.0';
7
7
 
8
8
  export function rootPackageJson(name) {
9
9
  return JSON.stringify(
@@ -119,7 +119,7 @@ function screenIndexHtml(title, isReact) {
119
119
 
120
120
  // Screen: React + Phaser
121
121
  // Test utilities: createMockScreen(), createMockController() can be imported in test files
122
- // Example: import { createMockScreen } from '@smoregg/sdk';
122
+ // Example: import { createMockScreen } from '@smoregg/sdk/testing';
123
123
  export function screenReactPhaser(gameId) {
124
124
  return {
125
125
  "package.json": JSON.stringify(
@@ -162,12 +162,14 @@ import { useEffect, useRef, useState } from 'react';
162
162
  import Phaser from 'phaser';
163
163
  import { GameScene } from './scenes/GameScene';
164
164
 
165
- // Type-safe events example:
166
- // type MyEvents = { 'player-move': { x: number; y: number } };
167
- // const screen = createScreen<MyEvents>({ debug: true });
165
+ interface GameEvents {
166
+ 'score-update': { score: number };
167
+ 'personal-message': { text: string };
168
+ 'tap': { timestamp: number };
169
+ }
168
170
 
169
171
  // Testing: Use mock utilities for unit tests
170
- // import { createMockScreen, createMockController } from '@smoregg/sdk';
172
+ // import { createMockScreen, createMockController } from '@smoregg/sdk/testing';
171
173
  // const mockScreen = createMockScreen();
172
174
  // mockScreen.simulateControllerJoin({ playerIndex: 0, name: 'Test' });
173
175
 
@@ -175,10 +177,8 @@ import { GameScene } from './scenes/GameScene';
175
177
  // screen/controller onError callback handles initialization and runtime errors.
176
178
  // Wrap critical game logic in try/catch for graceful error recovery.
177
179
 
178
- // Import GameMetadata type for game.json schema:
179
- // import type { GameMetadata } from '@smoregg/sdk';
180
+ // Game metadata is defined in game.json (see S'MORE platform docs)
180
181
  // game.json fields: { id, title, description, minPlayers, maxPlayers, version }
181
- // See docs for full GameMetadata schema.
182
182
 
183
183
  export function App() {
184
184
  const screenRef = useRef<Screen | null>(null);
@@ -189,7 +189,7 @@ export function App() {
189
189
  useEffect(() => {
190
190
  let mounted = true;
191
191
 
192
- const screen = createScreen({ debug: true });
192
+ const screen = createScreen<GameEvents>({ debug: true });
193
193
 
194
194
  screen.onControllerJoin((playerIndex, info) => {
195
195
  console.log('Player joined:', playerIndex);
@@ -212,9 +212,9 @@ export function App() {
212
212
  });
213
213
  // screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
214
214
  // screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
215
- // screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
215
+ // Rate limiting is delivered through onError() with code 'RATE_LIMITED'
216
216
  // screen.onAllReady(() => { console.log('All participants ready'); });
217
- // To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
217
+ // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
218
218
 
219
219
  screen.onError((error) => {
220
220
  console.error('SDK Error:', error.message);
@@ -310,7 +310,7 @@ export class GameScene extends Phaser.Scene {
310
310
 
311
311
  // Screen: React only
312
312
  // Test utilities: createMockScreen(), createMockController() can be imported in test files
313
- // Example: import { createMockScreen } from '@smoregg/sdk';
313
+ // Example: import { createMockScreen } from '@smoregg/sdk/testing';
314
314
  export function screenReact(gameId) {
315
315
  return {
316
316
  "package.json": JSON.stringify(
@@ -350,12 +350,14 @@ createRoot(document.getElementById('root')!).render(<App />);
350
350
  import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
351
351
  import { useEffect, useRef, useState } from 'react';
352
352
 
353
- // Type-safe events example:
354
- // type MyEvents = { 'player-move': { x: number; y: number } };
355
- // const screen = createScreen<MyEvents>({ debug: true });
353
+ interface GameEvents {
354
+ 'score-update': { score: number };
355
+ 'personal-message': { text: string };
356
+ 'tap': { timestamp: number };
357
+ }
356
358
 
357
359
  // Testing: Use mock utilities for unit tests
358
- // import { createMockScreen, createMockController } from '@smoregg/sdk';
360
+ // import { createMockScreen, createMockController } from '@smoregg/sdk/testing';
359
361
  // const mockScreen = createMockScreen();
360
362
  // mockScreen.simulateControllerJoin({ playerIndex: 0, name: 'Test' });
361
363
 
@@ -363,25 +365,20 @@ import { useEffect, useRef, useState } from 'react';
363
365
  // screen/controller onError callback handles initialization and runtime errors.
364
366
  // Wrap critical game logic in try/catch for graceful error recovery.
365
367
 
366
- // Import GameMetadata type for game.json schema:
367
- // import type { GameMetadata } from '@smoregg/sdk';
368
+ // Game metadata is defined in game.json (see S'MORE platform docs)
368
369
  // game.json fields: { id, title, description, minPlayers, maxPlayers, version }
369
- // See docs for full GameMetadata schema.
370
-
371
- // Advanced: Validate custom event names before sending
372
- // import { validateEventName } from '@smoregg/sdk';
373
- // validateEventName('my-event'); // throws if contains ':'
374
370
 
375
371
  export function App() {
376
372
  const screenRef = useRef<Screen | null>(null);
377
373
  const [roomCode, setRoomCode] = useState('');
378
374
  const [controllers, setControllers] = useState<ControllerInfo[]>([]);
379
375
  const [taps, setTaps] = useState<{ playerIndex: number; time: number }[]>([]);
376
+ const [customStates, setCustomStates] = useState<Record<number, Record<string, unknown>>>({});
380
377
 
381
378
  useEffect(() => {
382
379
  let mounted = true;
383
380
 
384
- const screen = createScreen({ debug: true });
381
+ const screen = createScreen<GameEvents>({ debug: true });
385
382
 
386
383
  screen.onControllerJoin((playerIndex, info) => {
387
384
  console.log('Player joined:', playerIndex);
@@ -401,9 +398,9 @@ export function App() {
401
398
  });
402
399
  // screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
403
400
  // screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
404
- // screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
401
+ // Rate limiting is delivered through onError() with code 'RATE_LIMITED'
405
402
  // screen.onAllReady(() => { console.log('All participants ready'); });
406
- // To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
403
+ // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
407
404
 
408
405
  screen.onError((error) => {
409
406
  console.error('SDK Error:', error.message);
@@ -416,11 +413,16 @@ export function App() {
416
413
  setControllers([...screen.controllers]);
417
414
  });
418
415
 
416
+ screen.onCustomStateChange((playerIndex, state) => {
417
+ if (!mounted) return;
418
+ setCustomStates((prev) => ({ ...prev, [playerIndex]: state }));
419
+ });
420
+
419
421
  screenRef.current = screen;
420
422
 
421
423
  // Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
422
424
  // destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
423
- screen.on('tap', (playerIndex) => {
425
+ screen.on('tap', (playerIndex, data) => {
424
426
  if (!mounted) return;
425
427
  setTaps((prev) => [...prev.slice(-9), { playerIndex, time: Date.now() }]);
426
428
  });
@@ -448,11 +450,11 @@ export function App() {
448
450
  controllers.forEach((_, idx) => {
449
451
  scores[idx] = Math.floor(Math.random() * 100);
450
452
  });
451
- // GameResults fields: scores (required), winner (optional), reason (optional)
453
+ // GameResults fields: scores (optional), winner (optional), rankings (optional), custom (optional)
452
454
  const results: GameResults = {
453
455
  scores,
454
456
  // winner: 0, // optional — playerIndex of winner
455
- // reason: 'time-up', // optional custom game-over reason
457
+ // custom: { reason: 'time-up' }, // optional -- custom game-specific data
456
458
  };
457
459
  screenRef.current?.gameOver(results);
458
460
  };
@@ -479,7 +481,7 @@ export function App() {
479
481
 
480
482
  // Screen: Vanilla JS
481
483
  // Test utilities: createMockScreen(), createMockController() can be imported in test files
482
- // Example: import { createMockScreen } from '@smoregg/sdk';
484
+ // Example: import { createMockScreen } from '@smoregg/sdk/testing';
483
485
  export function screenVanilla(gameId) {
484
486
  return {
485
487
  "package.json": JSON.stringify(
@@ -530,14 +532,16 @@ export function screenVanilla(gameId) {
530
532
  </html>
531
533
  `,
532
534
  "src/main.ts": `import { createScreen } from '@smoregg/sdk';
533
- import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
535
+ import type { GameResults } from '@smoregg/sdk';
534
536
 
535
- // Type-safe events example:
536
- // type MyEvents = { 'player-move': { x: number; y: number } };
537
- // const screen = createScreen<MyEvents>({ debug: true });
537
+ interface GameEvents {
538
+ 'score-update': { score: number };
539
+ 'personal-message': { text: string };
540
+ 'tap': { timestamp: number };
541
+ }
538
542
 
539
543
  // Testing: Use mock utilities for unit tests
540
- // import { createMockScreen, createMockController } from '@smoregg/sdk';
544
+ // import { createMockScreen, createMockController } from '@smoregg/sdk/testing';
541
545
  // const mockScreen = createMockScreen();
542
546
  // mockScreen.simulateControllerJoin({ playerIndex: 0, name: 'Test' });
543
547
 
@@ -545,16 +549,14 @@ import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
545
549
  // screen/controller onError callback handles initialization and runtime errors.
546
550
  // Wrap critical game logic in try/catch for graceful error recovery.
547
551
 
548
- // Import GameMetadata type for game.json schema:
549
- // import type { GameMetadata } from '@smoregg/sdk';
552
+ // Game metadata is defined in game.json (see S'MORE platform docs)
550
553
  // game.json fields: { id, title, description, minPlayers, maxPlayers, version }
551
- // See docs for full GameMetadata schema.
552
554
 
553
555
  const statusEl = document.getElementById('status')!;
554
556
  const roomCodeEl = document.getElementById('room-code')!;
555
557
  const logEl = document.getElementById('log')!;
556
558
 
557
- const screen = createScreen({ debug: true });
559
+ const screen = createScreen<GameEvents>({ debug: true });
558
560
 
559
561
  screen.onControllerJoin((playerIndex) => {
560
562
  console.log('Player joined:', playerIndex);
@@ -572,9 +574,9 @@ screen.onControllerDisconnect((playerIndex) => {
572
574
  });
573
575
  // screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
574
576
  // screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
575
- // screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
577
+ // Rate limiting is delivered through onError() with code 'RATE_LIMITED'
576
578
  // screen.onAllReady(() => { console.log('All participants ready'); });
577
- // To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
579
+ // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
578
580
 
579
581
  screen.onError((error) => {
580
582
  console.error('SDK Error:', error.message);
@@ -588,7 +590,7 @@ screen.onAllReady(() => {
588
590
 
589
591
  // Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
590
592
  // destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
591
- screen.on('tap', (playerIndex: number, data: unknown) => {
593
+ screen.on('tap', (playerIndex, data) => {
592
594
  const line = document.createElement('div');
593
595
  line.textContent = \`Player \${playerIndex} tapped\`;
594
596
  logEl.appendChild(line);
@@ -609,7 +611,7 @@ function updateStatus() {
609
611
  // const results: GameResults = {
610
612
  // scores: { 0: 50, 1: 75 },
611
613
  // // winner: 0,
612
- // // reason: 'time-up',
614
+ // // custom: { reason: 'time-up' },
613
615
  // };
614
616
  // screen.gameOver(results);
615
617
  `,
@@ -722,38 +724,46 @@ createRoot(document.getElementById('root')!).render(<App />);
722
724
  import type { Controller, ControllerInfo } from '@smoregg/sdk';
723
725
  import { useEffect, useRef, useState } from 'react';
724
726
 
727
+ interface GameEvents {
728
+ 'score-update': { score: number };
729
+ 'personal-message': { text: string };
730
+ 'tap': { timestamp: number };
731
+ }
732
+
725
733
  export function App() {
726
734
  const controllerRef = useRef<Controller | null>(null);
727
735
  const [myIndex, setMyIndex] = useState(-1);
736
+ const [me, setMe] = useState<ControllerInfo | null>(null);
728
737
  const [count, setCount] = useState(0);
729
738
  const [isReady, setIsReady] = useState(false);
730
739
 
731
740
  useEffect(() => {
732
741
  let mounted = true;
733
742
 
734
- const controller = createController({ debug: true });
743
+ const controller = createController<GameEvents>({ debug: true });
735
744
 
736
745
  // Lifecycle callbacks (uncomment to use):
737
746
  // controller.onControllerJoin((playerIndex, info) => { console.log('Player joined:', playerIndex); });
738
747
  // controller.onControllerLeave((playerIndex) => { console.log('Player left:', playerIndex); });
739
748
  // controller.onError((error) => { console.error('SDK Error:', error.message); });
740
749
  // controller.onAllReady(() => { console.log('All participants ready'); });
741
- // To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
750
+ // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
742
751
 
743
752
  controller.onAllReady(() => {
744
753
  if (!mounted) return;
745
- setMyIndex(controller.myIndex);
754
+ setMyIndex(controller.myPlayerIndex);
755
+ setMe(controller.me);
746
756
  setIsReady(true);
747
757
  });
748
758
 
749
759
  controllerRef.current = controller;
750
760
 
751
- controller.on('score-update', (data: { score: number }) => {
761
+ controller.on('score-update', (data) => {
752
762
  if (!mounted) return;
753
763
  setCount(data.score);
754
764
  });
755
765
 
756
- controller.on('personal-message', (data: { text: string }) => {
766
+ controller.on('personal-message', (data) => {
757
767
  console.log('Received message:', data.text);
758
768
  });
759
769
 
@@ -777,7 +787,7 @@ export function App() {
777
787
  touchAction: 'manipulation', userSelect: 'none',
778
788
  }}>
779
789
  {isReady && (
780
- <div style={{ fontSize: '16px', opacity: 0.6 }}>Player {myIndex}</div>
790
+ <div style={{ fontSize: '16px', opacity: 0.6 }}>{me?.name ?? \`Player \${myIndex}\`}</div>
781
791
  )}
782
792
  <div style={{ fontSize: '48px', fontWeight: 'bold' }}>{count}</div>
783
793
  <button
@@ -872,7 +882,12 @@ export default defineConfig({
872
882
  </html>
873
883
  `,
874
884
  "src/main.ts": `import { createController } from '@smoregg/sdk';
875
- import type { Controller, ControllerInfo } from '@smoregg/sdk';
885
+
886
+ interface GameEvents {
887
+ 'score-update': { score: number };
888
+ 'personal-message': { text: string };
889
+ 'tap': { timestamp: number };
890
+ }
876
891
 
877
892
  let count = 0;
878
893
 
@@ -880,25 +895,25 @@ const playerInfoEl = document.getElementById('player-info')!;
880
895
  const countEl = document.getElementById('count')!;
881
896
  const tapBtn = document.getElementById('tap-btn')!;
882
897
 
883
- const controller = createController({ debug: true });
898
+ const controller = createController<GameEvents>({ debug: true });
884
899
 
885
900
  // Lifecycle callbacks (uncomment to use):
886
901
  // controller.onControllerJoin((playerIndex, info) => { console.log('Player joined:', playerIndex); });
887
902
  // controller.onControllerLeave((playerIndex) => { console.log('Player left:', playerIndex); });
888
903
  // controller.onError((error) => { console.error('SDK Error:', error.message); });
889
904
  // controller.onAllReady(() => { console.log('All participants ready'); });
890
- // To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
905
+ // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
891
906
 
892
907
  controller.onAllReady(() => {
893
- playerInfoEl.textContent = \`Player \${controller.myIndex}\`;
908
+ playerInfoEl.textContent = controller.me?.name ?? \`Player \${controller.myPlayerIndex}\`;
894
909
  });
895
910
 
896
- controller.on('score-update', (data: { score: number }) => {
911
+ controller.on('score-update', (data) => {
897
912
  count = data.score;
898
913
  countEl.textContent = String(count);
899
914
  });
900
915
 
901
- controller.on('personal-message', (data: { text: string }) => {
916
+ controller.on('personal-message', (data) => {
902
917
  console.log('Received message:', data.text);
903
918
  });
904
919
 
@@ -989,6 +1004,7 @@ const room = {
989
1004
  gameId: '',
990
1005
  status: 'waiting',
991
1006
  readyIds: new Set(),
1007
+ customStates: new Map(), // playerIndex -> state object
992
1008
  };
993
1009
 
994
1010
  function toPlayerDTO(p) {
@@ -1135,6 +1151,20 @@ async function main() {
1135
1151
  io.to(room.code).emit('smore:game-over', data);
1136
1152
  });
1137
1153
 
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
+
1138
1168
  socket.on('smore:return-to-selection', () => {
1139
1169
  room.status = 'waiting';
1140
1170
  room.gameId = '';
@@ -1156,6 +1186,7 @@ async function main() {
1156
1186
  room.gameId = '';
1157
1187
  room.status = 'waiting';
1158
1188
  room.readyIds = new Set();
1189
+ room.customStates = new Map();
1159
1190
  console.log(' [reset] Room reset by host');
1160
1191
  if (typeof callback === 'function') callback({ success: true });
1161
1192
  });
@@ -1529,7 +1560,8 @@ export function devHarness(gameId) {
1529
1560
  // Forward specific smore: system events to screen iframe
1530
1561
  var screenSysEvents = [
1531
1562
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1532
- 'smore:player-character-updated', 'smore:rate-limited', 'smore:game-over', 'smore:all-ready'
1563
+ 'smore:player-character-updated', 'smore:rate-limited', 'smore:game-over', 'smore:all-ready',
1564
+ 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
1533
1565
  ];
1534
1566
  screenSysEvents.forEach(function(sysEvent) {
1535
1567
  screenSocket.on(sysEvent, function(data) {
@@ -1610,7 +1642,8 @@ export function devHarness(gameId) {
1610
1642
  var ctrlSysEvents = [
1611
1643
  'smore:game-over',
1612
1644
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1613
- 'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready'
1645
+ 'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
1646
+ 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
1614
1647
  ];
1615
1648
  ctrlSysEvents.forEach(function(sysEvent) {
1616
1649
  controllerSocket.on(sysEvent, function(data) {
@@ -1647,6 +1680,7 @@ export function devHarness(gameId) {
1647
1680
  side: 'host',
1648
1681
  roomCode: roomCode,
1649
1682
  players: getPlayerList(),
1683
+ protocolVersion: 1,
1650
1684
  },
1651
1685
  }, '*');
1652
1686
  return;
@@ -1700,6 +1734,7 @@ export function devHarness(gameId) {
1700
1734
  roomCode: roomCode,
1701
1735
  players: getPlayerList(),
1702
1736
  myIndex: player.playerIndex,
1737
+ protocolVersion: 1,
1703
1738
  },
1704
1739
  }, '*');
1705
1740
  return;
@@ -1724,6 +1759,16 @@ export function devHarness(gameId) {
1724
1759
  return;
1725
1760
  }
1726
1761
 
1762
+ // Relay custom state events to server
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');
1769
+ return;
1770
+ }
1771
+
1727
1772
  // Block smore:* events
1728
1773
  if (event.startsWith('smore:')) return;
1729
1774
 
@@ -1936,7 +1981,8 @@ export function devControllerPage(gameId) {
1936
1981
  var sysEvents = [
1937
1982
  'smore:game-over',
1938
1983
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1939
- 'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready'
1984
+ 'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
1985
+ 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
1940
1986
  ];
1941
1987
  sysEvents.forEach(function(sysEvent) {
1942
1988
  socket.on(sysEvent, function(data) {
@@ -1982,6 +2028,7 @@ export function devControllerPage(gameId) {
1982
2028
  roomCode: roomCode,
1983
2029
  players: players,
1984
2030
  myIndex: playerIndex,
2031
+ protocolVersion: 1,
1985
2032
  },
1986
2033
  }, '*');
1987
2034
  }
@@ -1997,6 +2044,16 @@ export function devControllerPage(gameId) {
1997
2044
  return;
1998
2045
  }
1999
2046
 
2047
+ // Relay custom state events to server
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');
2054
+ return;
2055
+ }
2056
+
2000
2057
  // Block smore:* events
2001
2058
  if (event.startsWith('smore:')) return;
2002
2059