create-smore-game 1.3.0 → 2.0.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/package.json +1 -1
- package/templates.js +162 -193
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 = '^
|
|
6
|
+
const SDK_VERSION = '^2.0.0';
|
|
7
7
|
|
|
8
8
|
export function rootPackageJson(name) {
|
|
9
9
|
return JSON.stringify(
|
|
@@ -164,7 +164,7 @@ import { GameScene } from './scenes/GameScene';
|
|
|
164
164
|
|
|
165
165
|
// Type-safe events example:
|
|
166
166
|
// type MyEvents = { 'player-move': { x: number; y: number } };
|
|
167
|
-
// const screen =
|
|
167
|
+
// const screen = createScreen<MyEvents>({ debug: true });
|
|
168
168
|
|
|
169
169
|
// Testing: Use mock utilities for unit tests
|
|
170
170
|
// import { createMockScreen, createMockController } from '@smoregg/sdk';
|
|
@@ -189,66 +189,56 @@ export function App() {
|
|
|
189
189
|
useEffect(() => {
|
|
190
190
|
let mounted = true;
|
|
191
191
|
|
|
192
|
-
const
|
|
193
|
-
// Alternative: register listeners in config (instead of screen.on())
|
|
194
|
-
// const screen = await createScreen({
|
|
195
|
-
// listeners: {
|
|
196
|
-
// 'tap': (playerIndex, data) => console.log('Player', playerIndex, 'tapped:', data),
|
|
197
|
-
// },
|
|
198
|
-
// });
|
|
199
|
-
// Note: listeners in config are set during initialization and cannot be removed.
|
|
200
|
-
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic listeners.
|
|
201
|
-
|
|
202
|
-
const screen = await createScreen({
|
|
203
|
-
debug: true,
|
|
204
|
-
onControllerJoin: (playerIndex, info) => {
|
|
205
|
-
console.log('Player joined:', playerIndex);
|
|
206
|
-
// Access player character data:
|
|
207
|
-
// const { nickname, appearance } = info;
|
|
208
|
-
// console.log(nickname, appearance?.style, appearance?.seed);
|
|
209
|
-
setControllers([...screen.controllers]);
|
|
210
|
-
},
|
|
211
|
-
onControllerLeave: (playerIndex) => {
|
|
212
|
-
console.log('Player left:', playerIndex);
|
|
213
|
-
setControllers([...screen.controllers]);
|
|
214
|
-
},
|
|
215
|
-
// Advanced callbacks (uncomment to use):
|
|
216
|
-
onControllerDisconnect: (playerIndex) => {
|
|
217
|
-
console.log(\`Player \${playerIndex} disconnected\`);
|
|
218
|
-
},
|
|
219
|
-
// onControllerReconnect: (playerIndex: number, info: ControllerInfo) => void — called when a disconnected player reconnects
|
|
220
|
-
// onCharacterUpdated: (playerIndex: number, appearance: CharacterAppearance | null) => void — called when a player updates their character appearance
|
|
221
|
-
// onRateLimited: (event: string) => void — called when client is rate-limited by server
|
|
222
|
-
// onReady: () => void — called when all participants are ready. Use for countdown/game start.
|
|
223
|
-
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
224
|
-
onError: (error) => {
|
|
225
|
-
console.error('SDK Error:', error.message);
|
|
226
|
-
// Show user-friendly error UI
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
if (!mounted) {
|
|
230
|
-
screen.destroy();
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
192
|
+
const screen = createScreen({ debug: true });
|
|
233
193
|
|
|
234
|
-
|
|
235
|
-
|
|
194
|
+
screen.onControllerJoin((playerIndex, info) => {
|
|
195
|
+
console.log('Player joined:', playerIndex);
|
|
196
|
+
// Access player character data:
|
|
197
|
+
// const { nickname, appearance } = info;
|
|
198
|
+
// console.log(nickname, appearance?.style, appearance?.seed);
|
|
199
|
+
if (!mounted) return;
|
|
236
200
|
setControllers([...screen.controllers]);
|
|
201
|
+
});
|
|
237
202
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
203
|
+
screen.onControllerLeave((playerIndex) => {
|
|
204
|
+
console.log('Player left:', playerIndex);
|
|
205
|
+
if (!mounted) return;
|
|
206
|
+
setControllers([...screen.controllers]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Advanced callbacks (uncomment to use):
|
|
210
|
+
screen.onControllerDisconnect((playerIndex) => {
|
|
211
|
+
console.log(\`Player \${playerIndex} disconnected\`);
|
|
212
|
+
});
|
|
213
|
+
// screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
|
|
214
|
+
// screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
|
|
215
|
+
// screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
|
|
216
|
+
// screen.onAllReady(() => { console.log('All participants ready'); });
|
|
217
|
+
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
218
|
+
|
|
219
|
+
screen.onError((error) => {
|
|
220
|
+
console.error('SDK Error:', error.message);
|
|
221
|
+
// Show user-friendly error UI
|
|
222
|
+
});
|
|
250
223
|
|
|
251
|
-
|
|
224
|
+
screen.onAllReady(() => {
|
|
225
|
+
if (!mounted) return;
|
|
226
|
+
setRoomCode(screen.roomCode);
|
|
227
|
+
setControllers([...screen.controllers]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
screenRef.current = screen;
|
|
231
|
+
|
|
232
|
+
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
233
|
+
// Cleanup example (useful in React useEffect):
|
|
234
|
+
// const handler = (playerIndex, data) => { ... };
|
|
235
|
+
// screen.on('my-event', handler);
|
|
236
|
+
// Later: screen.off('my-event', handler);
|
|
237
|
+
screen.on('tap', (playerIndex, data) => {
|
|
238
|
+
console.log('Player', playerIndex, 'tapped:', data);
|
|
239
|
+
// Forward input to Phaser scene
|
|
240
|
+
gameRef.current?.events.emit('player-tap', { playerIndex, ...data });
|
|
241
|
+
});
|
|
252
242
|
|
|
253
243
|
return () => {
|
|
254
244
|
mounted = false;
|
|
@@ -362,7 +352,7 @@ import { useEffect, useRef, useState } from 'react';
|
|
|
362
352
|
|
|
363
353
|
// Type-safe events example:
|
|
364
354
|
// type MyEvents = { 'player-move': { x: number; y: number } };
|
|
365
|
-
// const screen =
|
|
355
|
+
// const screen = createScreen<MyEvents>({ debug: true });
|
|
366
356
|
|
|
367
357
|
// Testing: Use mock utilities for unit tests
|
|
368
358
|
// import { createMockScreen, createMockController } from '@smoregg/sdk';
|
|
@@ -391,49 +381,49 @@ export function App() {
|
|
|
391
381
|
useEffect(() => {
|
|
392
382
|
let mounted = true;
|
|
393
383
|
|
|
394
|
-
const
|
|
395
|
-
const screen = await createScreen({
|
|
396
|
-
debug: true,
|
|
397
|
-
onControllerJoin: (playerIndex, info) => {
|
|
398
|
-
console.log('Player joined:', playerIndex);
|
|
399
|
-
setControllers([...screen.controllers]);
|
|
400
|
-
},
|
|
401
|
-
onControllerLeave: (playerIndex) => {
|
|
402
|
-
console.log('Player left:', playerIndex);
|
|
403
|
-
setControllers([...screen.controllers]);
|
|
404
|
-
},
|
|
405
|
-
// Advanced callbacks (uncomment to use):
|
|
406
|
-
onControllerDisconnect: (playerIndex) => {
|
|
407
|
-
console.log(\`Player \${playerIndex} disconnected\`);
|
|
408
|
-
},
|
|
409
|
-
// onControllerReconnect: (playerIndex: number, info: ControllerInfo) => void — called when a disconnected player reconnects
|
|
410
|
-
// onCharacterUpdated: (playerIndex: number, appearance: CharacterAppearance | null) => void — called when a player updates their character appearance
|
|
411
|
-
// onRateLimited: (event: string) => void — called when client is rate-limited by server
|
|
412
|
-
// onReady: () => void — called when all participants are ready. Use for countdown/game start.
|
|
413
|
-
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
414
|
-
onError: (error) => {
|
|
415
|
-
console.error('SDK Error:', error.message);
|
|
416
|
-
// Show user-friendly error UI
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
if (!mounted) {
|
|
420
|
-
screen.destroy();
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
384
|
+
const screen = createScreen({ debug: true });
|
|
423
385
|
|
|
424
|
-
|
|
386
|
+
screen.onControllerJoin((playerIndex, info) => {
|
|
387
|
+
console.log('Player joined:', playerIndex);
|
|
388
|
+
if (!mounted) return;
|
|
389
|
+
setControllers([...screen.controllers]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
screen.onControllerLeave((playerIndex) => {
|
|
393
|
+
console.log('Player left:', playerIndex);
|
|
394
|
+
if (!mounted) return;
|
|
395
|
+
setControllers([...screen.controllers]);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Advanced callbacks (uncomment to use):
|
|
399
|
+
screen.onControllerDisconnect((playerIndex) => {
|
|
400
|
+
console.log(\`Player \${playerIndex} disconnected\`);
|
|
401
|
+
});
|
|
402
|
+
// screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
|
|
403
|
+
// screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
|
|
404
|
+
// screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
|
|
405
|
+
// screen.onAllReady(() => { console.log('All participants ready'); });
|
|
406
|
+
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
407
|
+
|
|
408
|
+
screen.onError((error) => {
|
|
409
|
+
console.error('SDK Error:', error.message);
|
|
410
|
+
// Show user-friendly error UI
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
screen.onAllReady(() => {
|
|
414
|
+
if (!mounted) return;
|
|
425
415
|
setRoomCode(screen.roomCode);
|
|
426
416
|
setControllers([...screen.controllers]);
|
|
417
|
+
});
|
|
427
418
|
|
|
428
|
-
|
|
429
|
-
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
430
|
-
// destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
|
|
431
|
-
screen.on('tap', (playerIndex) => {
|
|
432
|
-
setTaps((prev) => [...prev.slice(-9), { playerIndex, time: Date.now() }]);
|
|
433
|
-
});
|
|
434
|
-
};
|
|
419
|
+
screenRef.current = screen;
|
|
435
420
|
|
|
436
|
-
|
|
421
|
+
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
422
|
+
// destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
|
|
423
|
+
screen.on('tap', (playerIndex) => {
|
|
424
|
+
if (!mounted) return;
|
|
425
|
+
setTaps((prev) => [...prev.slice(-9), { playerIndex, time: Date.now() }]);
|
|
426
|
+
});
|
|
437
427
|
|
|
438
428
|
return () => {
|
|
439
429
|
mounted = false;
|
|
@@ -544,7 +534,7 @@ import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
|
|
|
544
534
|
|
|
545
535
|
// Type-safe events example:
|
|
546
536
|
// type MyEvents = { 'player-move': { x: number; y: number } };
|
|
547
|
-
// const screen =
|
|
537
|
+
// const screen = createScreen<MyEvents>({ debug: true });
|
|
548
538
|
|
|
549
539
|
// Testing: Use mock utilities for unit tests
|
|
550
540
|
// import { createMockScreen, createMockController } from '@smoregg/sdk';
|
|
@@ -564,58 +554,55 @@ const statusEl = document.getElementById('status')!;
|
|
|
564
554
|
const roomCodeEl = document.getElementById('room-code')!;
|
|
565
555
|
const logEl = document.getElementById('log')!;
|
|
566
556
|
|
|
567
|
-
|
|
557
|
+
const screen = createScreen({ debug: true });
|
|
568
558
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
});
|
|
559
|
+
screen.onControllerJoin((playerIndex) => {
|
|
560
|
+
console.log('Player joined:', playerIndex);
|
|
561
|
+
updateStatus();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
screen.onControllerLeave((playerIndex) => {
|
|
565
|
+
console.log('Player left:', playerIndex);
|
|
566
|
+
updateStatus();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Advanced callbacks (uncomment to use):
|
|
570
|
+
screen.onControllerDisconnect((playerIndex) => {
|
|
571
|
+
console.log(\`Player \${playerIndex} disconnected\`);
|
|
572
|
+
});
|
|
573
|
+
// screen.onControllerReconnect((playerIndex, info) => { console.log('Player reconnected:', playerIndex); });
|
|
574
|
+
// screen.onCharacterUpdated((playerIndex, appearance) => { console.log('Character updated:', playerIndex); });
|
|
575
|
+
// screen.onRateLimited((event) => { console.warn('Rate limited:', event); });
|
|
576
|
+
// screen.onAllReady(() => { console.log('All participants ready'); });
|
|
577
|
+
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
578
|
+
|
|
579
|
+
screen.onError((error) => {
|
|
580
|
+
console.error('SDK Error:', error.message);
|
|
581
|
+
// Show user-friendly error UI
|
|
582
|
+
});
|
|
594
583
|
|
|
584
|
+
screen.onAllReady(() => {
|
|
595
585
|
roomCodeEl.textContent = \`Room Code: \${screen.roomCode}\`;
|
|
596
586
|
updateStatus();
|
|
587
|
+
});
|
|
597
588
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
});
|
|
610
|
-
}
|
|
589
|
+
// Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
|
|
590
|
+
// destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
|
|
591
|
+
screen.on('tap', (playerIndex: number, data: unknown) => {
|
|
592
|
+
const line = document.createElement('div');
|
|
593
|
+
line.textContent = \`Player \${playerIndex} tapped\`;
|
|
594
|
+
logEl.appendChild(line);
|
|
595
|
+
// Keep last 10 entries
|
|
596
|
+
while (logEl.children.length > 10) {
|
|
597
|
+
logEl.removeChild(logEl.firstChild!);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
611
600
|
|
|
612
601
|
function updateStatus() {
|
|
613
602
|
const count = screen.controllers.length;
|
|
614
603
|
statusEl.textContent = count > 0 ? \`\${count} player(s) connected\` : 'Waiting for players...';
|
|
615
604
|
}
|
|
616
605
|
|
|
617
|
-
init();
|
|
618
|
-
|
|
619
606
|
// Example functions (can be called from console for testing):
|
|
620
607
|
// screen.broadcast('score-update', { score: 100 });
|
|
621
608
|
// screen.sendToController(0, 'message', { text: 'Hello!' });
|
|
@@ -735,11 +722,6 @@ createRoot(document.getElementById('root')!).render(<App />);
|
|
|
735
722
|
import type { Controller, ControllerInfo } from '@smoregg/sdk';
|
|
736
723
|
import { useEffect, useRef, useState } from 'react';
|
|
737
724
|
|
|
738
|
-
// You can also register listeners in config:
|
|
739
|
-
// const controller = await createController({
|
|
740
|
-
// listeners: { 'game-state': (data) => { /* handle state */ } },
|
|
741
|
-
// });
|
|
742
|
-
|
|
743
725
|
export function App() {
|
|
744
726
|
const controllerRef = useRef<Controller | null>(null);
|
|
745
727
|
const [myIndex, setMyIndex] = useState(-1);
|
|
@@ -749,35 +731,31 @@ export function App() {
|
|
|
749
731
|
useEffect(() => {
|
|
750
732
|
let mounted = true;
|
|
751
733
|
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
761
|
-
});
|
|
762
|
-
if (!mounted) {
|
|
763
|
-
controller.destroy();
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
734
|
+
const controller = createController({ debug: true });
|
|
735
|
+
|
|
736
|
+
// Lifecycle callbacks (uncomment to use):
|
|
737
|
+
// controller.onControllerJoin((playerIndex, info) => { console.log('Player joined:', playerIndex); });
|
|
738
|
+
// controller.onControllerLeave((playerIndex) => { console.log('Player left:', playerIndex); });
|
|
739
|
+
// controller.onError((error) => { console.error('SDK Error:', error.message); });
|
|
740
|
+
// controller.onAllReady(() => { console.log('All participants ready'); });
|
|
741
|
+
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
766
742
|
|
|
767
|
-
|
|
743
|
+
controller.onAllReady(() => {
|
|
744
|
+
if (!mounted) return;
|
|
768
745
|
setMyIndex(controller.myIndex);
|
|
769
746
|
setIsReady(true);
|
|
747
|
+
});
|
|
770
748
|
|
|
771
|
-
|
|
772
|
-
setCount(data.score);
|
|
773
|
-
});
|
|
749
|
+
controllerRef.current = controller;
|
|
774
750
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
};
|
|
751
|
+
controller.on('score-update', (data: { score: number }) => {
|
|
752
|
+
if (!mounted) return;
|
|
753
|
+
setCount(data.score);
|
|
754
|
+
});
|
|
779
755
|
|
|
780
|
-
|
|
756
|
+
controller.on('personal-message', (data: { text: string }) => {
|
|
757
|
+
console.log('Received message:', data.text);
|
|
758
|
+
});
|
|
781
759
|
|
|
782
760
|
return () => {
|
|
783
761
|
mounted = false;
|
|
@@ -896,42 +874,33 @@ export default defineConfig({
|
|
|
896
874
|
"src/main.ts": `import { createController } from '@smoregg/sdk';
|
|
897
875
|
import type { Controller, ControllerInfo } from '@smoregg/sdk';
|
|
898
876
|
|
|
899
|
-
// You can also register listeners in config:
|
|
900
|
-
// const controller = await createController({
|
|
901
|
-
// listeners: { 'game-state': (data) => { /* handle state */ } },
|
|
902
|
-
// });
|
|
903
|
-
|
|
904
877
|
let count = 0;
|
|
905
|
-
let controller: Controller;
|
|
906
878
|
|
|
907
879
|
const playerInfoEl = document.getElementById('player-info')!;
|
|
908
880
|
const countEl = document.getElementById('count')!;
|
|
909
881
|
const tapBtn = document.getElementById('tap-btn')!;
|
|
910
882
|
|
|
911
|
-
|
|
912
|
-
controller = await createController({
|
|
913
|
-
debug: true,
|
|
914
|
-
// Lifecycle callbacks (uncomment to use):
|
|
915
|
-
// onControllerJoin: (playerIndex, info) => { console.log('Player joined:', playerIndex); },
|
|
916
|
-
// onControllerLeave: (playerIndex) => { console.log('Player left:', playerIndex); },
|
|
917
|
-
// onError: (error) => { console.error('SDK Error:', error.message); },
|
|
918
|
-
// onReady: () => void — called when all participants are ready
|
|
919
|
-
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
920
|
-
});
|
|
883
|
+
const controller = createController({ debug: true });
|
|
921
884
|
|
|
922
|
-
|
|
885
|
+
// Lifecycle callbacks (uncomment to use):
|
|
886
|
+
// controller.onControllerJoin((playerIndex, info) => { console.log('Player joined:', playerIndex); });
|
|
887
|
+
// controller.onControllerLeave((playerIndex) => { console.log('Player left:', playerIndex); });
|
|
888
|
+
// controller.onError((error) => { console.error('SDK Error:', error.message); });
|
|
889
|
+
// controller.onAllReady(() => { console.log('All participants ready'); });
|
|
890
|
+
// To control autoReady, use: import { configure } from '@smoregg/sdk'; configure({ autoReady: false });
|
|
923
891
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
});
|
|
892
|
+
controller.onAllReady(() => {
|
|
893
|
+
playerInfoEl.textContent = \`Player \${controller.myIndex}\`;
|
|
894
|
+
});
|
|
928
895
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
896
|
+
controller.on('score-update', (data: { score: number }) => {
|
|
897
|
+
count = data.score;
|
|
898
|
+
countEl.textContent = String(count);
|
|
899
|
+
});
|
|
933
900
|
|
|
934
|
-
|
|
901
|
+
controller.on('personal-message', (data: { text: string }) => {
|
|
902
|
+
console.log('Received message:', data.text);
|
|
903
|
+
});
|
|
935
904
|
|
|
936
905
|
tapBtn.addEventListener('pointerdown', () => {
|
|
937
906
|
controller.send('tap', { timestamp: Date.now() });
|