create-smore-game 2.4.0 → 3.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.
@@ -0,0 +1,97 @@
1
+ import { createScreen } from '@smoregg/sdk';
2
+ import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import Phaser from 'phaser';
5
+ import { GameScene } from './scenes/GameScene';
6
+ import type { GameEvents } from '../../types';
7
+
8
+ export function App() {
9
+ const screenRef = useRef<Screen | null>(null);
10
+ const gameRef = useRef<Phaser.Game | null>(null);
11
+ const [roomCode, setRoomCode] = useState('');
12
+ const [controllers, setControllers] = useState<ControllerInfo[]>([]);
13
+ const [tapCount, setTapCount] = useState(0);
14
+
15
+ useEffect(() => {
16
+ let mounted = true;
17
+
18
+ const screen = createScreen<GameEvents>({ debug: true });
19
+
20
+ screen.onControllerJoin((playerIndex, info) => {
21
+ console.log('Player joined:', playerIndex);
22
+ if (!mounted) return;
23
+ setControllers([...screen.controllers]);
24
+ });
25
+
26
+ screen.onControllerLeave((playerIndex) => {
27
+ console.log('Player left:', playerIndex);
28
+ if (!mounted) return;
29
+ setControllers([...screen.controllers]);
30
+ });
31
+
32
+ screen.onControllerDisconnect((playerIndex) => {
33
+ console.log(`Player ${playerIndex} disconnected`);
34
+ });
35
+
36
+ screen.onError((error) => {
37
+ console.error('SDK Error:', error.message);
38
+ });
39
+
40
+ screen.onAllReady(() => {
41
+ if (!mounted) return;
42
+ setRoomCode(screen.roomCode);
43
+ setControllers([...screen.controllers]);
44
+ });
45
+
46
+ screenRef.current = screen;
47
+
48
+ screen.on('tap', (playerIndex, data) => {
49
+ console.log('Player', playerIndex, 'tapped:', data);
50
+ gameRef.current?.events.emit('player-tap', { playerIndex, ...data });
51
+ setTapCount((prev) => {
52
+ const newCount = prev + 1;
53
+ screen.broadcast('score-update', { score: newCount });
54
+ return newCount;
55
+ });
56
+ });
57
+
58
+ return () => {
59
+ mounted = false;
60
+ screenRef.current?.destroy();
61
+ screenRef.current = null;
62
+ };
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ if (gameRef.current) return;
67
+
68
+ gameRef.current = new Phaser.Game({
69
+ type: Phaser.AUTO,
70
+ parent: 'phaser-container',
71
+ width: 1280,
72
+ height: 720,
73
+ backgroundColor: '#0f0f0f',
74
+ scale: {
75
+ mode: Phaser.Scale.FIT,
76
+ autoCenter: Phaser.Scale.CENTER_BOTH,
77
+ },
78
+ scene: [GameScene],
79
+ });
80
+
81
+ return () => {
82
+ gameRef.current?.destroy(true);
83
+ gameRef.current = null;
84
+ };
85
+ }, []);
86
+
87
+ return (
88
+ <div style={{ width: '100%', height: '100%' }}>
89
+ <div id="phaser-container" style={{ width: '100%', height: '100%' }} />
90
+ {roomCode && (
91
+ <div style={{ position: 'absolute', top: '20px', left: '20px', fontSize: '24px' }}>
92
+ Room: {roomCode} | Players: {controllers.length}
93
+ </div>
94
+ )}
95
+ </div>
96
+ );
97
+ }
@@ -0,0 +1,22 @@
1
+ import Phaser from 'phaser';
2
+
3
+ export class GameScene extends Phaser.Scene {
4
+ private label!: Phaser.GameObjects.Text;
5
+
6
+ constructor() {
7
+ super('GameScene');
8
+ }
9
+
10
+ create() {
11
+ this.label = this.add
12
+ .text(640, 360, 'Waiting for players...', {
13
+ fontSize: '32px',
14
+ color: '#ffffff',
15
+ })
16
+ .setOrigin(0.5);
17
+
18
+ this.game.events.on('player-tap', (data: { playerIndex: number }) => {
19
+ this.label.setText(`Player ${data.playerIndex} tapped!`);
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Game event types - shared between Screen and Controller.
3
+ * Define your custom events here. Both sides will have type-safe access.
4
+ *
5
+ * Convention:
6
+ * Screen -> Controller: view state (Screen pushes what to display)
7
+ * Controller -> Screen: user input (player actions only)
8
+ */
9
+ export interface GameEvents {
10
+ // Screen -> Controller (view state)
11
+ 'score-update': { score: number };
12
+ 'personal-message': { text: string };
13
+ // Controller -> Screen (input)
14
+ 'tap': { timestamp: number };
15
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { createMockController } from '@smoregg/sdk/testing';
3
+ import type { GameEvents } from '../../types';
4
+
5
+ describe('Game Controller', () => {
6
+ let controller: ReturnType<typeof createMockController<GameEvents>>;
7
+
8
+ beforeEach(() => {
9
+ controller = createMockController<GameEvents>({ autoReady: true });
10
+ });
11
+
12
+ it('should send tap event on user input', async () => {
13
+ await controller.ready;
14
+ controller.send('tap', { timestamp: Date.now() });
15
+ const sends = controller.getSends();
16
+ expect(sends).toHaveLength(1);
17
+ expect(sends[0].event).toBe('tap');
18
+ });
19
+
20
+ it('should update display when Screen pushes score', async () => {
21
+ await controller.ready;
22
+ controller.simulateEvent('score-update', { score: 42 });
23
+ });
24
+ });
@@ -0,0 +1,33 @@
1
+ import { createController } from '@smoregg/sdk';
2
+ import type { GameEvents } from '../../types';
3
+
4
+ /**
5
+ * ARCHITECTURE: Stateless Controller Pattern
6
+ *
7
+ * The controller is a stateless display + input device:
8
+ * - Render ONLY what the Screen sends (via controller.on())
9
+ * - Send ONLY user input to Screen (via controller.send())
10
+ * - Do NOT store or compute game state here
11
+ */
12
+
13
+ const playerInfoEl = document.getElementById('player-info')!;
14
+ const countEl = document.getElementById('count')!;
15
+ const tapBtn = document.getElementById('tap-btn')!;
16
+
17
+ const controller = createController<GameEvents>({ debug: true });
18
+
19
+ controller.onAllReady(() => {
20
+ playerInfoEl.textContent = controller.me?.nickname ?? `Player ${controller.myPlayerIndex}`;
21
+ });
22
+
23
+ controller.on('score-update', (data) => {
24
+ countEl.textContent = String(data.score);
25
+ });
26
+
27
+ controller.on('personal-message', (data) => {
28
+ console.log('Received message:', data.text);
29
+ });
30
+
31
+ tapBtn.addEventListener('pointerdown', () => {
32
+ controller.send('tap', { timestamp: Date.now() });
33
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, it, beforeEach } from 'vitest';
2
+ import { createMockScreen } from '@smoregg/sdk/testing';
3
+ import type { GameEvents } from '../../types';
4
+
5
+ describe('Game Screen', () => {
6
+ let screen: ReturnType<typeof createMockScreen<GameEvents>>;
7
+
8
+ beforeEach(() => {
9
+ screen = createMockScreen<GameEvents>({ autoReady: true });
10
+ });
11
+
12
+ it('should broadcast score-update when player taps', async () => {
13
+ await screen.ready;
14
+ screen.simulateEvent('tap', 0, { timestamp: Date.now() });
15
+ });
16
+
17
+ it('should handle player reconnection', async () => {
18
+ await screen.ready;
19
+ screen.simulateControllerReconnect(0);
20
+ });
21
+ });
@@ -0,0 +1,50 @@
1
+ import { createScreen } from '@smoregg/sdk';
2
+ import type { GameResults } from '@smoregg/sdk';
3
+ import type { GameEvents } from '../../types';
4
+
5
+ const statusEl = document.getElementById('status')!;
6
+ const roomCodeEl = document.getElementById('room-code')!;
7
+ const logEl = document.getElementById('log')!;
8
+
9
+ const screen = createScreen<GameEvents>({ debug: true });
10
+
11
+ screen.onControllerJoin((playerIndex) => {
12
+ console.log('Player joined:', playerIndex);
13
+ updateStatus();
14
+ });
15
+
16
+ screen.onControllerLeave((playerIndex) => {
17
+ console.log('Player left:', playerIndex);
18
+ updateStatus();
19
+ });
20
+
21
+ screen.onControllerDisconnect((playerIndex) => {
22
+ console.log(`Player ${playerIndex} disconnected`);
23
+ });
24
+
25
+ screen.onError((error) => {
26
+ console.error('SDK Error:', error.message);
27
+ });
28
+
29
+ screen.onAllReady(() => {
30
+ roomCodeEl.textContent = `Room Code: ${screen.roomCode}`;
31
+ updateStatus();
32
+ });
33
+
34
+ let tapCount = 0;
35
+
36
+ screen.on('tap', (playerIndex, data) => {
37
+ const line = document.createElement('div');
38
+ line.textContent = `Player ${playerIndex} tapped`;
39
+ logEl.appendChild(line);
40
+ while (logEl.children.length > 10) {
41
+ logEl.removeChild(logEl.firstChild!);
42
+ }
43
+ tapCount++;
44
+ screen.broadcast('score-update', { score: tapCount });
45
+ });
46
+
47
+ function updateStatus() {
48
+ const count = screen.controllers.length;
49
+ statusEl.textContent = count > 0 ? `${count} player(s) connected` : 'Waiting for players...';
50
+ }