create-smore-game 0.1.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 (3) hide show
  1. package/index.js +109 -0
  2. package/package.json +15 -0
  3. package/templates.js +618 -0
package/index.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import prompts from "prompts";
6
+ import {
7
+ pnpmWorkspace,
8
+ rootPackageJson,
9
+ screenReactPhaser,
10
+ screenReact,
11
+ screenVanilla,
12
+ playerReact,
13
+ playerVanilla,
14
+ } from "./templates.js";
15
+
16
+ const args = process.argv.slice(2);
17
+ const argName = args.find((a) => !a.startsWith("-"));
18
+
19
+ async function main() {
20
+ console.log("\n create-smore-game\n");
21
+
22
+ const response = await prompts(
23
+ [
24
+ {
25
+ type: argName ? null : "text",
26
+ name: "name",
27
+ message: "Project name:",
28
+ initial: "my-game",
29
+ validate: (v) => (v.trim() ? true : "Name is required"),
30
+ },
31
+ {
32
+ type: "select",
33
+ name: "screen",
34
+ message: "Screen (host/TV) template:",
35
+ choices: [
36
+ { title: "React + Phaser", value: "react-phaser" },
37
+ { title: "React only", value: "react" },
38
+ { title: "Vanilla JS", value: "vanilla" },
39
+ ],
40
+ },
41
+ {
42
+ type: "select",
43
+ name: "player",
44
+ message: "Player (phone) template:",
45
+ choices: [
46
+ { title: "React", value: "react" },
47
+ { title: "Vanilla JS", value: "vanilla" },
48
+ ],
49
+ },
50
+ ],
51
+ { onCancel: () => (process.exit(0), undefined) },
52
+ );
53
+
54
+ const projectName = (argName || response.name).trim();
55
+ const gameId = projectName.replace(/[^a-z0-9-]/gi, "-").toLowerCase();
56
+ const root = path.resolve(process.cwd(), projectName);
57
+
58
+ if (fs.existsSync(root)) {
59
+ console.error(`\n Error: Directory "${projectName}" already exists.\n`);
60
+ process.exit(1);
61
+ }
62
+
63
+ // Get templates
64
+ const screenFiles =
65
+ response.screen === "react-phaser"
66
+ ? screenReactPhaser(gameId)
67
+ : response.screen === "react"
68
+ ? screenReact(gameId)
69
+ : screenVanilla(gameId);
70
+
71
+ const playerFiles =
72
+ response.player === "react" ? playerReact(gameId) : playerVanilla(gameId);
73
+
74
+ // Write root files
75
+ writeFile(root, "package.json", rootPackageJson(projectName));
76
+ writeFile(root, "pnpm-workspace.yaml", pnpmWorkspace);
77
+ writeFile(
78
+ root,
79
+ ".gitignore",
80
+ "node_modules\ndist\n*.local\n.DS_Store\n",
81
+ );
82
+
83
+ // Write screen files
84
+ for (const [filePath, content] of Object.entries(screenFiles)) {
85
+ writeFile(path.join(root, "screen"), filePath, content);
86
+ }
87
+
88
+ // Write player files
89
+ for (const [filePath, content] of Object.entries(playerFiles)) {
90
+ writeFile(path.join(root, "player"), filePath, content);
91
+ }
92
+
93
+ console.log(`\n Done! Created ${projectName}/\n`);
94
+ console.log(" Next steps:\n");
95
+ console.log(` cd ${projectName}`);
96
+ console.log(" pnpm install");
97
+ console.log(" pnpm dev\n");
98
+ }
99
+
100
+ function writeFile(dir, filePath, content) {
101
+ const full = path.join(dir, filePath);
102
+ fs.mkdirSync(path.dirname(full), { recursive: true });
103
+ fs.writeFileSync(full, content);
104
+ }
105
+
106
+ main().catch((err) => {
107
+ console.error(err);
108
+ process.exit(1);
109
+ });
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "create-smore-game",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "create-smore-game": "./index.js"
7
+ },
8
+ "files": [
9
+ "index.js",
10
+ "templates.js"
11
+ ],
12
+ "dependencies": {
13
+ "prompts": "^2.4.2"
14
+ }
15
+ }
package/templates.js ADDED
@@ -0,0 +1,618 @@
1
+ // ─── Shared ───
2
+
3
+ export const pnpmWorkspace = `packages:
4
+ - screen
5
+ - player
6
+ `;
7
+
8
+ export function rootPackageJson(name) {
9
+ return JSON.stringify(
10
+ {
11
+ name,
12
+ private: true,
13
+ scripts: {
14
+ "dev": "pnpm -r --parallel dev",
15
+ "dev:screen": "pnpm --filter screen dev",
16
+ "dev:player": "pnpm --filter player dev",
17
+ "build": "pnpm -r build",
18
+ "zip": "pnpm build && node -e \"const{execSync:e}=require('child_process');e('cd dist && zip -r ../'+process.env.npm_package_name+'.zip .');\""
19
+ },
20
+ },
21
+ null,
22
+ 2,
23
+ );
24
+ }
25
+
26
+ // ─── Screen templates ───
27
+
28
+ const screenTsconfig = JSON.stringify(
29
+ {
30
+ compilerOptions: {
31
+ target: "ES2020",
32
+ module: "ESNext",
33
+ moduleResolution: "bundler",
34
+ jsx: "react-jsx",
35
+ strict: true,
36
+ esModuleInterop: true,
37
+ skipLibCheck: true,
38
+ outDir: "dist",
39
+ },
40
+ include: ["src"],
41
+ },
42
+ null,
43
+ 2,
44
+ );
45
+
46
+ const screenTsconfigVanilla = JSON.stringify(
47
+ {
48
+ compilerOptions: {
49
+ target: "ES2020",
50
+ module: "ESNext",
51
+ moduleResolution: "bundler",
52
+ strict: true,
53
+ esModuleInterop: true,
54
+ skipLibCheck: true,
55
+ outDir: "dist",
56
+ },
57
+ include: ["src"],
58
+ },
59
+ null,
60
+ 2,
61
+ );
62
+
63
+ function screenViteConfig(isReact) {
64
+ if (isReact) {
65
+ return `import { defineConfig } from 'vite';
66
+ import react from '@vitejs/plugin-react';
67
+
68
+ export default defineConfig({
69
+ plugins: [react()],
70
+ server: { port: 5173 },
71
+ build: { outDir: '../dist/screen' },
72
+ });
73
+ `;
74
+ }
75
+ return `import { defineConfig } from 'vite';
76
+
77
+ export default defineConfig({
78
+ server: { port: 5173 },
79
+ build: { outDir: '../dist/screen' },
80
+ });
81
+ `;
82
+ }
83
+
84
+ function screenIndexHtml(title, isReact) {
85
+ const entry = isReact ? "/src/main.tsx" : "/src/main.ts";
86
+ const root = isReact ? '\n <div id="root"></div>' : "";
87
+ return `<!DOCTYPE html>
88
+ <html lang="en">
89
+ <head>
90
+ <meta charset="UTF-8" />
91
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
92
+ <title>${title} - Screen</title>
93
+ <style>
94
+ * { margin: 0; padding: 0; box-sizing: border-box; }
95
+ html, body { width: 100%; height: 100%; background: #0f0f0f; color: #fff; font-family: sans-serif; overflow: hidden; }
96
+ #root, #app { width: 100%; height: 100%; }
97
+ </style>
98
+ </head>
99
+ <body>${root}
100
+ <script type="module" src="${entry}"></script>
101
+ </body>
102
+ </html>
103
+ `;
104
+ }
105
+
106
+ // Screen: React + Phaser
107
+ export function screenReactPhaser(gameId) {
108
+ return {
109
+ "package.json": JSON.stringify(
110
+ {
111
+ name: "screen",
112
+ private: true,
113
+ type: "module",
114
+ scripts: {
115
+ dev: "vite",
116
+ build: "tsc && vite build",
117
+ },
118
+ dependencies: {
119
+ react: "^18.3.1",
120
+ "react-dom": "^18.3.1",
121
+ phaser: "^3.80.1",
122
+ "@smoregg/sdk": "^0.1.0",
123
+ },
124
+ devDependencies: {
125
+ "@types/react": "^18.3.0",
126
+ "@types/react-dom": "^18.3.0",
127
+ "@vitejs/plugin-react": "^4.3.0",
128
+ typescript: "^5.5.0",
129
+ vite: "^5.4.0",
130
+ },
131
+ },
132
+ null,
133
+ 2,
134
+ ),
135
+ "tsconfig.json": screenTsconfig,
136
+ "vite.config.ts": screenViteConfig(true),
137
+ "index.html": screenIndexHtml(gameId, true),
138
+ "src/main.tsx": `import { createRoot } from 'react-dom/client';
139
+ import { IframeRoomProvider } from '@smoregg/sdk/iframe';
140
+ import { App } from './App';
141
+
142
+ createRoot(document.getElementById('root')!).render(
143
+ <IframeRoomProvider>
144
+ <App />
145
+ </IframeRoomProvider>
146
+ );
147
+ `,
148
+ "src/App.tsx": `import { useGameHost } from '@smoregg/sdk/iframe';
149
+ import { useEffect, useRef } from 'react';
150
+ import Phaser from 'phaser';
151
+ import { GameScene } from './scenes/GameScene';
152
+
153
+ export function App() {
154
+ const gameRef = useRef<Phaser.Game | null>(null);
155
+ const { room, broadcast } = useGameHost({
156
+ gameId: '${gameId}',
157
+ onInput: {
158
+ tap: (playerId, data) => {
159
+ console.log('Player tapped:', playerId, data);
160
+ // Forward input to Phaser scene
161
+ gameRef.current?.events.emit('player-tap', { playerId, ...data });
162
+ },
163
+ },
164
+ onPlayerJoin: (playerId) => {
165
+ console.log('Player joined:', playerId);
166
+ },
167
+ onPlayerLeave: (playerId) => {
168
+ console.log('Player left:', playerId);
169
+ },
170
+ });
171
+
172
+ useEffect(() => {
173
+ if (gameRef.current) return;
174
+
175
+ gameRef.current = new Phaser.Game({
176
+ type: Phaser.AUTO,
177
+ parent: 'phaser-container',
178
+ width: 1280,
179
+ height: 720,
180
+ backgroundColor: '#0f0f0f',
181
+ scale: {
182
+ mode: Phaser.Scale.FIT,
183
+ autoCenter: Phaser.Scale.CENTER_BOTH,
184
+ },
185
+ scene: [GameScene],
186
+ });
187
+
188
+ return () => {
189
+ gameRef.current?.destroy(true);
190
+ gameRef.current = null;
191
+ };
192
+ }, []);
193
+
194
+ return (
195
+ <div style={{ width: '100%', height: '100%' }}>
196
+ <div id="phaser-container" style={{ width: '100%', height: '100%' }} />
197
+ </div>
198
+ );
199
+ }
200
+ `,
201
+ "src/scenes/GameScene.ts": `import Phaser from 'phaser';
202
+
203
+ export class GameScene extends Phaser.Scene {
204
+ private label!: Phaser.GameObjects.Text;
205
+
206
+ constructor() {
207
+ super('GameScene');
208
+ }
209
+
210
+ create() {
211
+ this.label = this.add
212
+ .text(640, 360, 'Waiting for players...', {
213
+ fontSize: '32px',
214
+ color: '#ffffff',
215
+ })
216
+ .setOrigin(0.5);
217
+
218
+ // Listen for player taps forwarded from React
219
+ this.game.events.on('player-tap', (data: { playerId: string }) => {
220
+ this.label.setText(\`Player \${data.playerId.slice(0, 6)} tapped!\`);
221
+ });
222
+ }
223
+ }
224
+ `,
225
+ };
226
+ }
227
+
228
+ // Screen: React only
229
+ export function screenReact(gameId) {
230
+ return {
231
+ "package.json": JSON.stringify(
232
+ {
233
+ name: "screen",
234
+ private: true,
235
+ type: "module",
236
+ scripts: {
237
+ dev: "vite",
238
+ build: "tsc && vite build",
239
+ },
240
+ dependencies: {
241
+ react: "^18.3.1",
242
+ "react-dom": "^18.3.1",
243
+ "@smoregg/sdk": "^0.1.0",
244
+ },
245
+ devDependencies: {
246
+ "@types/react": "^18.3.0",
247
+ "@types/react-dom": "^18.3.0",
248
+ "@vitejs/plugin-react": "^4.3.0",
249
+ typescript: "^5.5.0",
250
+ vite: "^5.4.0",
251
+ },
252
+ },
253
+ null,
254
+ 2,
255
+ ),
256
+ "tsconfig.json": screenTsconfig,
257
+ "vite.config.ts": screenViteConfig(true),
258
+ "index.html": screenIndexHtml(gameId, true),
259
+ "src/main.tsx": `import { createRoot } from 'react-dom/client';
260
+ import { IframeRoomProvider } from '@smoregg/sdk/iframe';
261
+ import { App } from './App';
262
+
263
+ createRoot(document.getElementById('root')!).render(
264
+ <IframeRoomProvider>
265
+ <App />
266
+ </IframeRoomProvider>
267
+ );
268
+ `,
269
+ "src/App.tsx": `import { useGameHost } from '@smoregg/sdk/iframe';
270
+ import { useState } from 'react';
271
+
272
+ export function App() {
273
+ const [taps, setTaps] = useState<{ playerId: string; time: number }[]>([]);
274
+
275
+ const { room, broadcast } = useGameHost({
276
+ gameId: '${gameId}',
277
+ onInput: {
278
+ tap: (playerId, data) => {
279
+ setTaps((prev) => [...prev.slice(-9), { playerId, time: Date.now() }]);
280
+ },
281
+ },
282
+ onPlayerJoin: (playerId) => {
283
+ console.log('Player joined:', playerId);
284
+ },
285
+ onPlayerLeave: (playerId) => {
286
+ console.log('Player left:', playerId);
287
+ },
288
+ });
289
+
290
+ return (
291
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: '2vmin' }}>
292
+ <h1 style={{ fontSize: '5vmin' }}>
293
+ {room?.players?.length ? \`\${room.players.length} player(s) connected\` : 'Waiting for players...'}
294
+ </h1>
295
+ <div style={{ fontSize: '3vmin', opacity: 0.6 }}>
296
+ {taps.map((t, i) => (
297
+ <div key={i}>Player {t.playerId.slice(0, 6)} tapped</div>
298
+ ))}
299
+ </div>
300
+ </div>
301
+ );
302
+ }
303
+ `,
304
+ };
305
+ }
306
+
307
+ // Screen: Vanilla JS
308
+ export function screenVanilla(gameId) {
309
+ return {
310
+ "package.json": JSON.stringify(
311
+ {
312
+ name: "screen",
313
+ private: true,
314
+ type: "module",
315
+ scripts: {
316
+ dev: "vite",
317
+ build: "tsc && vite build",
318
+ },
319
+ dependencies: {
320
+ "@smoregg/sdk": "^0.1.0",
321
+ },
322
+ devDependencies: {
323
+ typescript: "^5.5.0",
324
+ vite: "^5.4.0",
325
+ },
326
+ },
327
+ null,
328
+ 2,
329
+ ),
330
+ "tsconfig.json": screenTsconfigVanilla,
331
+ "vite.config.ts": screenViteConfig(false),
332
+ "index.html": `<!DOCTYPE html>
333
+ <html lang="en">
334
+ <head>
335
+ <meta charset="UTF-8" />
336
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
337
+ <title>${gameId} - Screen</title>
338
+ <style>
339
+ * { margin: 0; padding: 0; box-sizing: border-box; }
340
+ html, body { width: 100%; height: 100%; background: #0f0f0f; color: #fff; font-family: sans-serif; overflow: hidden; }
341
+ #app { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 2vmin; }
342
+ h1 { font-size: 5vmin; }
343
+ #log { font-size: 3vmin; opacity: 0.6; }
344
+ </style>
345
+ </head>
346
+ <body>
347
+ <div id="app">
348
+ <h1 id="status">Waiting for players...</h1>
349
+ <div id="log"></div>
350
+ </div>
351
+ <script type="module" src="/src/main.ts"></script>
352
+ </body>
353
+ </html>
354
+ `,
355
+ "src/main.ts": `import { createIframeHost } from '@smoregg/sdk/iframe';
356
+
357
+ const host = createIframeHost({
358
+ gameId: '${gameId}',
359
+ onInput: {
360
+ tap: (playerId: string, data: unknown) => {
361
+ const log = document.getElementById('log')!;
362
+ const line = document.createElement('div');
363
+ line.textContent = \`Player \${playerId.slice(0, 6)} tapped\`;
364
+ log.appendChild(line);
365
+ // Keep last 10
366
+ while (log.children.length > 10) log.removeChild(log.firstChild!);
367
+ },
368
+ },
369
+ onPlayerJoin: (playerId: string) => {
370
+ document.getElementById('status')!.textContent = 'Player connected!';
371
+ },
372
+ });
373
+ `,
374
+ };
375
+ }
376
+
377
+ // ─── Player templates ───
378
+
379
+ const playerTsconfig = JSON.stringify(
380
+ {
381
+ compilerOptions: {
382
+ target: "ES2020",
383
+ module: "ESNext",
384
+ moduleResolution: "bundler",
385
+ jsx: "react-jsx",
386
+ strict: true,
387
+ esModuleInterop: true,
388
+ skipLibCheck: true,
389
+ outDir: "dist",
390
+ },
391
+ include: ["src"],
392
+ },
393
+ null,
394
+ 2,
395
+ );
396
+
397
+ const playerTsconfigVanilla = JSON.stringify(
398
+ {
399
+ compilerOptions: {
400
+ target: "ES2020",
401
+ module: "ESNext",
402
+ moduleResolution: "bundler",
403
+ strict: true,
404
+ esModuleInterop: true,
405
+ skipLibCheck: true,
406
+ outDir: "dist",
407
+ },
408
+ include: ["src"],
409
+ },
410
+ null,
411
+ 2,
412
+ );
413
+
414
+ // Player: React
415
+ export function playerReact(gameId) {
416
+ return {
417
+ "package.json": JSON.stringify(
418
+ {
419
+ name: "player",
420
+ private: true,
421
+ type: "module",
422
+ scripts: {
423
+ dev: "vite",
424
+ build: "tsc && vite build",
425
+ },
426
+ dependencies: {
427
+ react: "^18.3.1",
428
+ "react-dom": "^18.3.1",
429
+ "@smoregg/sdk": "^0.1.0",
430
+ },
431
+ devDependencies: {
432
+ "@types/react": "^18.3.0",
433
+ "@types/react-dom": "^18.3.0",
434
+ "@vitejs/plugin-react": "^4.3.0",
435
+ typescript: "^5.5.0",
436
+ vite: "^5.4.0",
437
+ },
438
+ },
439
+ null,
440
+ 2,
441
+ ),
442
+ "tsconfig.json": playerTsconfig,
443
+ "vite.config.ts": `import { defineConfig } from 'vite';
444
+ import react from '@vitejs/plugin-react';
445
+
446
+ export default defineConfig({
447
+ plugins: [react()],
448
+ server: { port: 5174 },
449
+ build: { outDir: '../dist/player' },
450
+ });
451
+ `,
452
+ "index.html": `<!DOCTYPE html>
453
+ <html lang="en">
454
+ <head>
455
+ <meta charset="UTF-8" />
456
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
457
+ <title>${gameId} - Player</title>
458
+ <style>
459
+ * { margin: 0; padding: 0; box-sizing: border-box; }
460
+ html, body, #root {
461
+ width: 100%; height: 100vh; height: 100dvh;
462
+ background: #0f0f0f; color: #fff; font-family: sans-serif;
463
+ overflow: hidden; overscroll-behavior: none;
464
+ }
465
+ </style>
466
+ </head>
467
+ <body>
468
+ <div id="root"></div>
469
+ <script type="module" src="/src/main.tsx"></script>
470
+ </body>
471
+ </html>
472
+ `,
473
+ "src/main.tsx": `import { createRoot } from 'react-dom/client';
474
+ import { IframeRoomProvider } from '@smoregg/sdk/iframe';
475
+ import { App } from './App';
476
+
477
+ createRoot(document.getElementById('root')!).render(
478
+ <IframeRoomProvider>
479
+ <App />
480
+ </IframeRoomProvider>
481
+ );
482
+ `,
483
+ "src/App.tsx": `import { useGamePlayer, TapButton } from '@smoregg/sdk/iframe';
484
+ import { useState } from 'react';
485
+
486
+ export function App() {
487
+ const [count, setCount] = useState(0);
488
+
489
+ const { emit, room } = useGamePlayer({
490
+ gameId: '${gameId}',
491
+ listeners: {
492
+ 'score-update': (data: { score: number }) => {
493
+ setCount(data.score);
494
+ },
495
+ },
496
+ });
497
+
498
+ const handleTap = () => {
499
+ emit('tap', { timestamp: Date.now() });
500
+ setCount((c) => c + 1);
501
+ };
502
+
503
+ return (
504
+ <div style={{
505
+ position: 'fixed', inset: 0, display: 'flex', flexDirection: 'column',
506
+ alignItems: 'center', justifyContent: 'center', gap: '24px',
507
+ padding: 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)',
508
+ touchAction: 'manipulation', userSelect: 'none',
509
+ }}>
510
+ <div style={{ fontSize: '48px', fontWeight: 'bold' }}>{count}</div>
511
+ <TapButton onTap={handleTap} style={{
512
+ width: '200px', height: '200px', borderRadius: '50%',
513
+ background: '#4f46e5', border: 'none', color: '#fff',
514
+ fontSize: '24px', fontWeight: 'bold', cursor: 'pointer',
515
+ }}>
516
+ TAP
517
+ </TapButton>
518
+ </div>
519
+ );
520
+ }
521
+ `,
522
+ };
523
+ }
524
+
525
+ // Player: Vanilla JS
526
+ export function playerVanilla(gameId) {
527
+ return {
528
+ "package.json": JSON.stringify(
529
+ {
530
+ name: "player",
531
+ private: true,
532
+ type: "module",
533
+ scripts: {
534
+ dev: "vite",
535
+ build: "tsc && vite build",
536
+ },
537
+ dependencies: {
538
+ "@smoregg/sdk": "^0.1.0",
539
+ },
540
+ devDependencies: {
541
+ typescript: "^5.5.0",
542
+ vite: "^5.4.0",
543
+ },
544
+ },
545
+ null,
546
+ 2,
547
+ ),
548
+ "tsconfig.json": playerTsconfigVanilla,
549
+ "vite.config.ts": `import { defineConfig } from 'vite';
550
+
551
+ export default defineConfig({
552
+ server: { port: 5174 },
553
+ build: { outDir: '../dist/player' },
554
+ });
555
+ `,
556
+ "index.html": `<!DOCTYPE html>
557
+ <html lang="en">
558
+ <head>
559
+ <meta charset="UTF-8" />
560
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
561
+ <title>${gameId} - Player</title>
562
+ <style>
563
+ * { margin: 0; padding: 0; box-sizing: border-box; }
564
+ html, body {
565
+ width: 100%; height: 100vh; height: 100dvh;
566
+ background: #0f0f0f; color: #fff; font-family: sans-serif;
567
+ overflow: hidden; overscroll-behavior: none;
568
+ touch-action: manipulation; user-select: none;
569
+ -webkit-user-select: none;
570
+ }
571
+ #app {
572
+ position: fixed; inset: 0;
573
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px;
574
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
575
+ }
576
+ #count { font-size: 48px; font-weight: bold; }
577
+ #tap-btn {
578
+ width: 200px; height: 200px; border-radius: 50%;
579
+ background: #4f46e5; border: none; color: #fff;
580
+ font-size: 24px; font-weight: bold; cursor: pointer;
581
+ -webkit-tap-highlight-color: transparent;
582
+ }
583
+ #tap-btn:active { background: #4338ca; }
584
+ </style>
585
+ </head>
586
+ <body>
587
+ <div id="app">
588
+ <div id="count">0</div>
589
+ <button id="tap-btn">TAP</button>
590
+ </div>
591
+ <script type="module" src="/src/main.ts"></script>
592
+ </body>
593
+ </html>
594
+ `,
595
+ "src/main.ts": `import { createIframePlayer } from '@smoregg/sdk/iframe';
596
+
597
+ let count = 0;
598
+ const countEl = document.getElementById('count')!;
599
+ const tapBtn = document.getElementById('tap-btn')!;
600
+
601
+ const player = createIframePlayer({
602
+ gameId: '${gameId}',
603
+ listeners: {
604
+ 'score-update': (data: { score: number }) => {
605
+ count = data.score;
606
+ countEl.textContent = String(count);
607
+ },
608
+ },
609
+ });
610
+
611
+ tapBtn.addEventListener('pointerdown', () => {
612
+ player.emit('tap', { timestamp: Date.now() });
613
+ count++;
614
+ countEl.textContent = String(count);
615
+ });
616
+ `,
617
+ };
618
+ }