waitless-api 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.
@@ -0,0 +1,1956 @@
1
+ /**
2
+ * Base game class with common functionality
3
+ */
4
+ class BaseGame {
5
+ constructor(config) {
6
+ this.isRunning = false;
7
+ this.score = 0;
8
+ this.container = config.container;
9
+ this.theme = config.theme || 'default';
10
+ this.onScoreCallback = config.onScore;
11
+ }
12
+ /**
13
+ * Update the score and trigger callback
14
+ * @param points Points to add to the score
15
+ */
16
+ updateScore(points) {
17
+ this.score += points;
18
+ // Call score callback if provided
19
+ if (this.onScoreCallback && typeof this.onScoreCallback === 'function') {
20
+ this.onScoreCallback(this.score);
21
+ }
22
+ }
23
+ /**
24
+ * Get the current score
25
+ * @returns Current score
26
+ */
27
+ getScore() {
28
+ return this.score;
29
+ }
30
+ /**
31
+ * Check if the game is currently running
32
+ * @returns Whether the game is running
33
+ */
34
+ isActive() {
35
+ return this.isRunning;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Tetris game implementation
41
+ */
42
+ class TetrisGame extends BaseGame {
43
+ constructor(config) {
44
+ var _a, _b, _c, _d;
45
+ super(config);
46
+ // Game state
47
+ this.grid = [];
48
+ this.currentPiece = null;
49
+ this.nextPiece = null;
50
+ this.animationFrame = null;
51
+ this.lastTime = 0;
52
+ this.dropInterval = 1000;
53
+ this.dropCounter = 0;
54
+ // Tetromino shapes and colors
55
+ this.shapes = [
56
+ // I-piece
57
+ [
58
+ [0, 0, 0, 0],
59
+ [1, 1, 1, 1],
60
+ [0, 0, 0, 0],
61
+ [0, 0, 0, 0]
62
+ ],
63
+ // J-piece
64
+ [
65
+ [2, 0, 0],
66
+ [2, 2, 2],
67
+ [0, 0, 0]
68
+ ],
69
+ // L-piece
70
+ [
71
+ [0, 0, 3],
72
+ [3, 3, 3],
73
+ [0, 0, 0]
74
+ ],
75
+ // O-piece
76
+ [
77
+ [4, 4],
78
+ [4, 4]
79
+ ],
80
+ // S-piece
81
+ [
82
+ [0, 5, 5],
83
+ [5, 5, 0],
84
+ [0, 0, 0]
85
+ ],
86
+ // T-piece
87
+ [
88
+ [0, 6, 0],
89
+ [6, 6, 6],
90
+ [0, 0, 0]
91
+ ],
92
+ // Z-piece
93
+ [
94
+ [7, 7, 0],
95
+ [0, 7, 7],
96
+ [0, 0, 0]
97
+ ]
98
+ ];
99
+ this.colors = [
100
+ 'transparent',
101
+ '#00FFFF',
102
+ '#0000FF',
103
+ '#FF8000',
104
+ '#FFFF00',
105
+ '#00FF00',
106
+ '#8000FF',
107
+ '#FF0000' // Z-piece (red)
108
+ ];
109
+ /**
110
+ * Handle keyboard input
111
+ */
112
+ this.handleKeyDown = (e) => {
113
+ if (!this.isRunning) {
114
+ return;
115
+ }
116
+ switch (e.key) {
117
+ case 'ArrowLeft':
118
+ this.movePiece(-1);
119
+ break;
120
+ case 'ArrowRight':
121
+ this.movePiece(1);
122
+ break;
123
+ case 'ArrowDown':
124
+ this.dropPiece();
125
+ break;
126
+ case 'ArrowUp':
127
+ this.rotatePiece();
128
+ break;
129
+ case ' ': // Space - hard drop
130
+ while (!this.checkCollision()) {
131
+ this.currentPiece.pos.y++;
132
+ }
133
+ this.currentPiece.pos.y--;
134
+ this.mergePiece();
135
+ this.checkLines();
136
+ this.createPiece();
137
+ break;
138
+ }
139
+ };
140
+ this.blockSize = typeof config.blockSize === 'number' ? config.blockSize : 30;
141
+ const width = typeof config.width === 'number' ? config.width : 300;
142
+ const height = typeof config.height === 'number' ? config.height : 600;
143
+ this.cols = Math.floor(width / this.blockSize) || 10;
144
+ this.rows = Math.floor(height / this.blockSize) || 20;
145
+ const speed = typeof config.speed === 'number' ? config.speed : 1;
146
+ this.scoring = config.scoring && typeof config.scoring === 'object'
147
+ ? {
148
+ singleLine: (_a = config.scoring.singleLine) !== null && _a !== void 0 ? _a : 100,
149
+ doubleLine: (_b = config.scoring.doubleLine) !== null && _b !== void 0 ? _b : 300,
150
+ tripleLine: (_c = config.scoring.tripleLine) !== null && _c !== void 0 ? _c : 500,
151
+ tetris: (_d = config.scoring.tetris) !== null && _d !== void 0 ? _d : 800
152
+ }
153
+ : { singleLine: 100, doubleLine: 300, tripleLine: 500, tetris: 800 };
154
+ this.dropInterval = Math.max(100, 1000 / speed);
155
+ this.canvas = document.createElement('canvas');
156
+ this.canvas.width = this.cols * this.blockSize;
157
+ this.canvas.height = this.rows * this.blockSize;
158
+ this.canvas.style.display = 'block';
159
+ this.canvas.style.margin = '0 auto';
160
+ this.canvas.style.border = '1px solid #333';
161
+ this.container.appendChild(this.canvas);
162
+ const ctx = this.canvas.getContext('2d');
163
+ if (!ctx) {
164
+ throw new Error('Could not get canvas context');
165
+ }
166
+ this.ctx = ctx;
167
+ // Initialize game grid
168
+ this.resetGrid();
169
+ // Set up event listeners
170
+ this.setupControls();
171
+ }
172
+ /**
173
+ * Start the game
174
+ */
175
+ start() {
176
+ if (this.isRunning) {
177
+ return;
178
+ }
179
+ this.isRunning = true;
180
+ this.resetGrid();
181
+ this.createPiece();
182
+ this.lastTime = 0;
183
+ this.score = 0;
184
+ // Start game loop
185
+ this.update(0);
186
+ }
187
+ /**
188
+ * Pause the game
189
+ */
190
+ pause() {
191
+ if (!this.isRunning || !this.animationFrame) {
192
+ return;
193
+ }
194
+ this.isRunning = false;
195
+ cancelAnimationFrame(this.animationFrame);
196
+ this.animationFrame = null;
197
+ }
198
+ /**
199
+ * Resume the game
200
+ */
201
+ resume() {
202
+ if (this.isRunning) {
203
+ return;
204
+ }
205
+ this.isRunning = true;
206
+ this.lastTime = 0;
207
+ this.update(0);
208
+ }
209
+ /**
210
+ * Destroy the game and clean up
211
+ */
212
+ destroy() {
213
+ // Stop game loop
214
+ if (this.animationFrame) {
215
+ cancelAnimationFrame(this.animationFrame);
216
+ this.animationFrame = null;
217
+ }
218
+ // Remove event listeners
219
+ window.removeEventListener('keydown', this.handleKeyDown);
220
+ // Remove canvas
221
+ if (this.canvas.parentNode) {
222
+ this.canvas.parentNode.removeChild(this.canvas);
223
+ }
224
+ this.isRunning = false;
225
+ }
226
+ /**
227
+ * Main game update loop
228
+ */
229
+ update(time) {
230
+ if (!this.isRunning) {
231
+ return;
232
+ }
233
+ const deltaTime = time - this.lastTime;
234
+ this.lastTime = time;
235
+ // Update drop counter
236
+ this.dropCounter += deltaTime;
237
+ if (this.dropCounter > this.dropInterval) {
238
+ this.dropPiece();
239
+ }
240
+ // Draw game
241
+ this.draw();
242
+ // Continue game loop
243
+ this.animationFrame = requestAnimationFrame(this.update.bind(this));
244
+ }
245
+ /**
246
+ * Draw the game state
247
+ */
248
+ draw() {
249
+ // Clear canvas
250
+ this.ctx.fillStyle = '#000';
251
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
252
+ // Draw grid
253
+ this.drawGrid();
254
+ // Draw current piece
255
+ if (this.currentPiece) {
256
+ this.drawPiece(this.currentPiece);
257
+ }
258
+ }
259
+ /**
260
+ * Draw the game grid
261
+ */
262
+ drawGrid() {
263
+ for (let y = 0; y < this.rows; y++) {
264
+ for (let x = 0; x < this.cols; x++) {
265
+ const value = this.grid[y][x];
266
+ if (value !== 0) {
267
+ this.ctx.fillStyle = this.colors[value];
268
+ this.ctx.fillRect(x * this.blockSize, y * this.blockSize, this.blockSize, this.blockSize);
269
+ // Draw outline
270
+ this.ctx.strokeStyle = '#222';
271
+ this.ctx.strokeRect(x * this.blockSize, y * this.blockSize, this.blockSize, this.blockSize);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ /**
277
+ * Draw a tetromino piece
278
+ */
279
+ drawPiece(piece) {
280
+ const { shape, pos } = piece;
281
+ for (let y = 0; y < shape.length; y++) {
282
+ for (let x = 0; x < shape[y].length; x++) {
283
+ const value = shape[y][x];
284
+ if (value !== 0) {
285
+ this.ctx.fillStyle = this.colors[value];
286
+ this.ctx.fillRect((pos.x + x) * this.blockSize, (pos.y + y) * this.blockSize, this.blockSize, this.blockSize);
287
+ // Draw outline
288
+ this.ctx.strokeStyle = '#222';
289
+ this.ctx.strokeRect((pos.x + x) * this.blockSize, (pos.y + y) * this.blockSize, this.blockSize, this.blockSize);
290
+ }
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Reset the game grid
296
+ */
297
+ resetGrid() {
298
+ this.grid = Array(this.rows).fill(0).map(() => Array(this.cols).fill(0));
299
+ }
300
+ /**
301
+ * Create a new tetromino piece
302
+ */
303
+ createPiece() {
304
+ // If we have a next piece, use it
305
+ if (this.nextPiece) {
306
+ this.currentPiece = this.nextPiece;
307
+ this.nextPiece = null;
308
+ }
309
+ else {
310
+ // Create a new random piece
311
+ const shapeIndex = Math.floor(Math.random() * this.shapes.length);
312
+ this.currentPiece = {
313
+ shape: this.shapes[shapeIndex],
314
+ pos: { x: Math.floor(this.cols / 2) - 1, y: 0 }
315
+ };
316
+ }
317
+ // Create the next piece
318
+ const nextShapeIndex = Math.floor(Math.random() * this.shapes.length);
319
+ this.nextPiece = {
320
+ shape: this.shapes[nextShapeIndex],
321
+ pos: { x: Math.floor(this.cols / 2) - 1, y: 0 }
322
+ };
323
+ // Check for game over
324
+ if (this.checkCollision()) {
325
+ // Game over
326
+ this.isRunning = false;
327
+ this.resetGrid();
328
+ }
329
+ }
330
+ /**
331
+ * Move the current piece down
332
+ */
333
+ dropPiece() {
334
+ this.currentPiece.pos.y++;
335
+ this.dropCounter = 0;
336
+ if (this.checkCollision()) {
337
+ // Revert position
338
+ this.currentPiece.pos.y--;
339
+ // Merge piece with grid
340
+ this.mergePiece();
341
+ // Check for completed lines
342
+ this.checkLines();
343
+ // Create new piece
344
+ this.createPiece();
345
+ }
346
+ }
347
+ /**
348
+ * Check if the current piece collides with the grid or boundaries
349
+ */
350
+ checkCollision() {
351
+ const { shape, pos } = this.currentPiece;
352
+ for (let y = 0; y < shape.length; y++) {
353
+ for (let x = 0; x < shape[y].length; x++) {
354
+ if (shape[y][x] !== 0) {
355
+ const gridX = pos.x + x;
356
+ const gridY = pos.y + y;
357
+ // Check boundaries
358
+ if (gridX < 0 || gridX >= this.cols || gridY >= this.rows) {
359
+ return true;
360
+ }
361
+ // Check collision with existing blocks
362
+ if (gridY >= 0 && this.grid[gridY][gridX] !== 0) {
363
+ return true;
364
+ }
365
+ }
366
+ }
367
+ }
368
+ return false;
369
+ }
370
+ /**
371
+ * Merge the current piece with the grid
372
+ */
373
+ mergePiece() {
374
+ const { shape, pos } = this.currentPiece;
375
+ for (let y = 0; y < shape.length; y++) {
376
+ for (let x = 0; x < shape[y].length; x++) {
377
+ const value = shape[y][x];
378
+ if (value !== 0) {
379
+ const gridY = pos.y + y;
380
+ const gridX = pos.x + x;
381
+ // Only merge if within bounds
382
+ if (gridY >= 0 && gridY < this.rows && gridX >= 0 && gridX < this.cols) {
383
+ this.grid[gridY][gridX] = value;
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Check for completed lines and remove them
391
+ */
392
+ checkLines() {
393
+ var _a;
394
+ let linesCleared = 0;
395
+ for (let y = this.rows - 1; y >= 0; y--) {
396
+ // Check if row is full
397
+ if (this.grid[y].every(value => value !== 0)) {
398
+ // Remove the line
399
+ this.grid.splice(y, 1);
400
+ // Add a new empty line at the top
401
+ this.grid.unshift(Array(this.cols).fill(0));
402
+ // Increment counter and check the same row again (since we moved rows down)
403
+ linesCleared++;
404
+ y++;
405
+ }
406
+ }
407
+ if (linesCleared > 0) {
408
+ const pts = (_a = [0, this.scoring.singleLine, this.scoring.doubleLine, this.scoring.tripleLine, this.scoring.tetris][linesCleared]) !== null && _a !== void 0 ? _a : this.scoring.tetris;
409
+ this.updateScore(pts);
410
+ }
411
+ }
412
+ /**
413
+ * Move the current piece left or right
414
+ */
415
+ movePiece(dir) {
416
+ this.currentPiece.pos.x += dir;
417
+ if (this.checkCollision()) {
418
+ this.currentPiece.pos.x -= dir;
419
+ }
420
+ }
421
+ /**
422
+ * Rotate the current piece
423
+ */
424
+ rotatePiece() {
425
+ // Save current position
426
+ const pos = this.currentPiece.pos.x;
427
+ // Rotate matrix
428
+ const rotated = [];
429
+ const shape = this.currentPiece.shape;
430
+ for (let y = 0; y < shape[0].length; y++) {
431
+ const row = [];
432
+ for (let x = 0; x < shape.length; x++) {
433
+ row.push(shape[shape.length - 1 - x][y]);
434
+ }
435
+ rotated.push(row);
436
+ }
437
+ // Apply rotation
438
+ this.currentPiece.shape = rotated;
439
+ // Check for collision and adjust if needed
440
+ let offset = 1;
441
+ while (this.checkCollision()) {
442
+ this.currentPiece.pos.x += offset;
443
+ offset = -(offset + (offset > 0 ? 1 : -1));
444
+ // If we've tried moving too far, revert rotation
445
+ if (offset > shape[0].length) {
446
+ this.currentPiece.shape = shape;
447
+ this.currentPiece.pos.x = pos;
448
+ return;
449
+ }
450
+ }
451
+ }
452
+ /**
453
+ * Set up control event listeners
454
+ */
455
+ setupControls() {
456
+ window.addEventListener('keydown', this.handleKeyDown);
457
+ // Mobile touch controls could be added here
458
+ }
459
+ }
460
+
461
+ // Simple theme colors for snake/food (SDK has theme string only)
462
+ const THEME_COLORS$1 = {
463
+ default: { primary: '#333333', secondary: '#666666', accent: '#4caf50', text: '#222222' },
464
+ cyberpunk: { primary: '#ff00ff', secondary: '#00ffff', accent: '#ffff00', text: '#ffffff' },
465
+ minimal: { primary: '#000000', secondary: '#333333', accent: '#555555', text: '#111111' },
466
+ corporate: { primary: '#3f51b5', secondary: '#7986cb', accent: '#ff4081', text: '#263238' }
467
+ };
468
+ /**
469
+ * Snake game implementation
470
+ */
471
+ class SnakeGame extends BaseGame {
472
+ constructor(config) {
473
+ super(config);
474
+ this.snake = [];
475
+ this.food = null;
476
+ this.direction = 'right';
477
+ this.nextDirection = 'right';
478
+ this.animationFrame = null;
479
+ this.lastTime = 0;
480
+ this.moveInterval = 200;
481
+ this.moveCounter = 0;
482
+ this.highScore = 0;
483
+ this.touchStartX = 0;
484
+ this.touchStartY = 0;
485
+ this.handleKeyDown = (e) => {
486
+ if (!this.isRunning)
487
+ return;
488
+ const current = this.nextDirection;
489
+ switch (e.key) {
490
+ case 'ArrowUp':
491
+ if (current !== 'down')
492
+ this.nextDirection = 'up';
493
+ break;
494
+ case 'ArrowDown':
495
+ if (current !== 'up')
496
+ this.nextDirection = 'down';
497
+ break;
498
+ case 'ArrowLeft':
499
+ if (current !== 'right')
500
+ this.nextDirection = 'left';
501
+ break;
502
+ case 'ArrowRight':
503
+ if (current !== 'left')
504
+ this.nextDirection = 'right';
505
+ break;
506
+ }
507
+ e.preventDefault();
508
+ };
509
+ this.handleTouchStart = (e) => {
510
+ if (e.touches.length > 0) {
511
+ this.touchStartX = e.touches[0].clientX;
512
+ this.touchStartY = e.touches[0].clientY;
513
+ }
514
+ };
515
+ this.handleTouchEnd = (e) => {
516
+ if (!this.isRunning || !e.changedTouches || e.changedTouches.length === 0)
517
+ return;
518
+ const dx = e.changedTouches[0].clientX - this.touchStartX;
519
+ const dy = e.changedTouches[0].clientY - this.touchStartY;
520
+ const minSwipe = 30;
521
+ if (Math.abs(dx) > Math.abs(dy)) {
522
+ if (dx > minSwipe && this.nextDirection !== 'left')
523
+ this.nextDirection = 'right';
524
+ else if (dx < -minSwipe && this.nextDirection !== 'right')
525
+ this.nextDirection = 'left';
526
+ }
527
+ else {
528
+ if (dy > minSwipe && this.nextDirection !== 'up')
529
+ this.nextDirection = 'down';
530
+ else if (dy < -minSwipe && this.nextDirection !== 'down')
531
+ this.nextDirection = 'up';
532
+ }
533
+ };
534
+ this.gridSize = typeof config.gridSize === 'number' ? config.gridSize : 20;
535
+ this.foodValue = typeof config.foodValue === 'number' ? config.foodValue : 10;
536
+ const width = typeof config.width === 'number' ? config.width : 400;
537
+ const height = typeof config.height === 'number' ? config.height : 400;
538
+ this.gridWidth = this.gridSize;
539
+ this.gridHeight = this.gridSize;
540
+ this.cellSize = Math.max(10, Math.floor(Math.min(width, height) / this.gridSize)) || 20;
541
+ this.moveInterval = typeof config.speed === 'number' && config.speed > 0
542
+ ? Math.max(80, 200 / config.speed)
543
+ : 200;
544
+ this.canvas = document.createElement('canvas');
545
+ this.canvas.width = this.gridWidth * this.cellSize;
546
+ this.canvas.height = this.gridHeight * this.cellSize;
547
+ this.canvas.style.display = 'block';
548
+ this.canvas.style.margin = '0 auto';
549
+ this.canvas.style.border = '1px solid #333';
550
+ this.container.appendChild(this.canvas);
551
+ const ctx = this.canvas.getContext('2d');
552
+ if (!ctx) {
553
+ throw new Error('Could not get canvas context');
554
+ }
555
+ this.ctx = ctx;
556
+ this.setupControls();
557
+ }
558
+ getColors() {
559
+ return THEME_COLORS$1[this.theme] || THEME_COLORS$1.default;
560
+ }
561
+ start() {
562
+ if (this.isRunning) {
563
+ return;
564
+ }
565
+ this.isRunning = true;
566
+ this.resetGame();
567
+ this.lastTime = 0;
568
+ this.score = 0;
569
+ this.moveInterval = 200;
570
+ this.update(0);
571
+ }
572
+ pause() {
573
+ if (!this.isRunning || !this.animationFrame) {
574
+ return;
575
+ }
576
+ this.isRunning = false;
577
+ cancelAnimationFrame(this.animationFrame);
578
+ this.animationFrame = null;
579
+ }
580
+ resume() {
581
+ if (this.isRunning) {
582
+ return;
583
+ }
584
+ this.isRunning = true;
585
+ this.lastTime = 0;
586
+ this.update(0);
587
+ }
588
+ destroy() {
589
+ if (this.animationFrame) {
590
+ cancelAnimationFrame(this.animationFrame);
591
+ this.animationFrame = null;
592
+ }
593
+ window.removeEventListener('keydown', this.handleKeyDown);
594
+ window.removeEventListener('touchstart', this.handleTouchStart);
595
+ window.removeEventListener('touchend', this.handleTouchEnd);
596
+ if (this.canvas.parentNode) {
597
+ this.canvas.parentNode.removeChild(this.canvas);
598
+ }
599
+ this.isRunning = false;
600
+ }
601
+ update(time) {
602
+ if (!this.isRunning)
603
+ return;
604
+ const deltaTime = time - this.lastTime;
605
+ this.lastTime = time;
606
+ this.moveCounter += deltaTime;
607
+ if (this.moveCounter >= this.moveInterval) {
608
+ this.direction = this.nextDirection;
609
+ this.moveSnake();
610
+ this.moveCounter = 0;
611
+ }
612
+ this.draw();
613
+ this.animationFrame = requestAnimationFrame(this.update.bind(this));
614
+ }
615
+ draw() {
616
+ const colors = this.getColors();
617
+ this.ctx.fillStyle = '#0a0a0a';
618
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
619
+ // Subtle grid
620
+ this.ctx.strokeStyle = colors.text;
621
+ this.ctx.globalAlpha = 0.15;
622
+ for (let i = 0; i <= this.gridWidth; i++) {
623
+ this.ctx.beginPath();
624
+ this.ctx.moveTo(i * this.cellSize, 0);
625
+ this.ctx.lineTo(i * this.cellSize, this.canvas.height);
626
+ this.ctx.stroke();
627
+ }
628
+ for (let j = 0; j <= this.gridHeight; j++) {
629
+ this.ctx.beginPath();
630
+ this.ctx.moveTo(0, j * this.cellSize);
631
+ this.ctx.lineTo(this.canvas.width, j * this.cellSize);
632
+ this.ctx.stroke();
633
+ }
634
+ this.ctx.globalAlpha = 1;
635
+ // Food
636
+ if (this.food) {
637
+ this.ctx.fillStyle = colors.secondary;
638
+ this.ctx.fillRect(this.food.x * this.cellSize + 1, this.food.y * this.cellSize + 1, this.cellSize - 2, this.cellSize - 2);
639
+ }
640
+ // Snake body
641
+ for (let i = 0; i < this.snake.length; i++) {
642
+ const seg = this.snake[i];
643
+ const isHead = i === this.snake.length - 1;
644
+ this.ctx.fillStyle = isHead ? colors.accent : colors.primary;
645
+ this.ctx.fillRect(seg.x * this.cellSize + 1, seg.y * this.cellSize + 1, this.cellSize - 2, this.cellSize - 2);
646
+ }
647
+ // Score
648
+ this.ctx.fillStyle = colors.text;
649
+ this.ctx.font = '14px Arial';
650
+ this.ctx.fillText(`Score: ${this.score} High: ${this.highScore}`, 8, 18);
651
+ }
652
+ resetGame() {
653
+ const cx = Math.floor(this.gridWidth / 2);
654
+ const cy = Math.floor(this.gridHeight / 2);
655
+ this.snake = [
656
+ { x: cx - 2, y: cy },
657
+ { x: cx - 1, y: cy },
658
+ { x: cx, y: cy }
659
+ ];
660
+ this.direction = 'right';
661
+ this.nextDirection = 'right';
662
+ this.spawnFood();
663
+ }
664
+ spawnFood() {
665
+ const empty = [];
666
+ for (let y = 0; y < this.gridHeight; y++) {
667
+ for (let x = 0; x < this.gridWidth; x++) {
668
+ if (!this.snake.some(s => s.x === x && s.y === y)) {
669
+ empty.push({ x, y });
670
+ }
671
+ }
672
+ }
673
+ if (empty.length === 0) {
674
+ this.food = null;
675
+ return;
676
+ }
677
+ this.food = empty[Math.floor(Math.random() * empty.length)];
678
+ }
679
+ moveSnake() {
680
+ const head = this.snake[this.snake.length - 1];
681
+ let nx = head.x;
682
+ let ny = head.y;
683
+ switch (this.direction) {
684
+ case 'up':
685
+ ny--;
686
+ break;
687
+ case 'down':
688
+ ny++;
689
+ break;
690
+ case 'left':
691
+ nx--;
692
+ break;
693
+ case 'right':
694
+ nx++;
695
+ break;
696
+ }
697
+ // Wall collision
698
+ if (nx < 0 || nx >= this.gridWidth || ny < 0 || ny >= this.gridHeight) {
699
+ this.gameOver();
700
+ return;
701
+ }
702
+ // Self collision
703
+ if (this.snake.some(s => s.x === nx && s.y === ny)) {
704
+ this.gameOver();
705
+ return;
706
+ }
707
+ this.snake.push({ x: nx, y: ny });
708
+ // Food collision
709
+ if (this.food && this.food.x === nx && this.food.y === ny) {
710
+ this.updateScore(this.foodValue);
711
+ if (this.score > this.highScore)
712
+ this.highScore = this.score;
713
+ this.spawnFood();
714
+ if (this.moveInterval > 80) {
715
+ this.moveInterval -= 5;
716
+ }
717
+ }
718
+ else {
719
+ this.snake.shift();
720
+ }
721
+ }
722
+ gameOver() {
723
+ this.isRunning = false;
724
+ if (this.animationFrame) {
725
+ cancelAnimationFrame(this.animationFrame);
726
+ this.animationFrame = null;
727
+ }
728
+ }
729
+ setupControls() {
730
+ window.addEventListener('keydown', this.handleKeyDown);
731
+ window.addEventListener('touchstart', this.handleTouchStart, { passive: true });
732
+ window.addEventListener('touchend', this.handleTouchEnd, { passive: true });
733
+ }
734
+ }
735
+
736
+ const THEME_COLORS = {
737
+ default: { primary: '#333333', secondary: '#666666', accent: '#4caf50', text: '#222222' },
738
+ cyberpunk: { primary: '#ff00ff', secondary: '#00ffff', accent: '#ffff00', text: '#ffffff' },
739
+ minimal: { primary: '#000000', secondary: '#333333', accent: '#555555', text: '#111111' },
740
+ corporate: { primary: '#3f51b5', secondary: '#7986cb', accent: '#ff4081', text: '#263238' }
741
+ };
742
+ /**
743
+ * Breakout game implementation
744
+ */
745
+ class BreakoutGame extends BaseGame {
746
+ constructor(config) {
747
+ super(config);
748
+ this.bricks = [];
749
+ this.animationFrame = null;
750
+ this.lastTime = 0;
751
+ this.ballLaunched = false;
752
+ this.lives = 3;
753
+ this.level = 1;
754
+ this.highScore = 0;
755
+ this.paused = false;
756
+ this.paddleHeight = 10;
757
+ this.ballRadius = 8;
758
+ this.brickPadding = 8;
759
+ this.brickOffsetTop = 50;
760
+ this.handleMouseMove = (e) => {
761
+ const rect = this.canvas.getBoundingClientRect();
762
+ const scaleX = this.canvas.width / rect.width;
763
+ const relativeX = (e.clientX - rect.left) * scaleX;
764
+ this.paddle.x = Math.max(0, Math.min(this.width - this.paddle.width, relativeX - this.paddle.width / 2));
765
+ };
766
+ this.handleTouchMove = (e) => {
767
+ e.preventDefault();
768
+ if (e.touches.length > 0) {
769
+ const rect = this.canvas.getBoundingClientRect();
770
+ const scaleX = this.canvas.width / rect.width;
771
+ const relativeX = (e.touches[0].clientX - rect.left) * scaleX;
772
+ this.paddle.x = Math.max(0, Math.min(this.width - this.paddle.width, relativeX - this.paddle.width / 2));
773
+ }
774
+ };
775
+ this.handleClick = () => {
776
+ this.launchBall();
777
+ };
778
+ this.handleKeyDown = (e) => {
779
+ if (e.key === ' ') {
780
+ e.preventDefault();
781
+ if (this.isRunning) {
782
+ if (this.paused)
783
+ this.resume();
784
+ else
785
+ this.pause();
786
+ }
787
+ }
788
+ };
789
+ this.width = typeof config.width === 'number' ? config.width : 480;
790
+ this.height = typeof config.height === 'number' ? config.height : 320;
791
+ this.paddleWidth = typeof config.paddleWidth === 'number' ? config.paddleWidth : 100;
792
+ this.brickRows = typeof config.brickRows === 'number' ? config.brickRows : 5;
793
+ this.brickColumns = typeof config.brickColumns === 'number' ? config.brickColumns : 8;
794
+ this.baseBallSpeed = typeof config.ballSpeed === 'number' && config.ballSpeed > 0 ? config.ballSpeed : 5;
795
+ this.canvas = document.createElement('canvas');
796
+ this.canvas.width = this.width;
797
+ this.canvas.height = this.height;
798
+ this.canvas.style.display = 'block';
799
+ this.canvas.style.margin = '0 auto';
800
+ this.canvas.style.border = '1px solid #333';
801
+ this.container.appendChild(this.canvas);
802
+ const ctx = this.canvas.getContext('2d');
803
+ if (!ctx)
804
+ throw new Error('Could not get canvas context');
805
+ this.ctx = ctx;
806
+ this.paddle = {
807
+ x: this.width / 2 - this.paddleWidth / 2,
808
+ y: this.height - 25,
809
+ width: this.paddleWidth,
810
+ height: this.paddleHeight
811
+ };
812
+ this.ball = {
813
+ x: this.width / 2,
814
+ y: this.height - 40,
815
+ dx: 0,
816
+ dy: 0,
817
+ radius: this.ballRadius
818
+ };
819
+ this.setupControls();
820
+ }
821
+ getColors() {
822
+ return THEME_COLORS[this.theme] || THEME_COLORS.default;
823
+ }
824
+ start() {
825
+ if (this.isRunning)
826
+ return;
827
+ this.isRunning = true;
828
+ this.lives = 3;
829
+ this.level = 1;
830
+ this.score = 0;
831
+ this.ballLaunched = false;
832
+ this.baseBallSpeed = 5;
833
+ this.resetGame();
834
+ this.lastTime = 0;
835
+ this.update(0);
836
+ }
837
+ pause() {
838
+ if (!this.isRunning || !this.animationFrame)
839
+ return;
840
+ this.paused = true;
841
+ this.isRunning = false;
842
+ cancelAnimationFrame(this.animationFrame);
843
+ this.animationFrame = null;
844
+ }
845
+ resume() {
846
+ if (this.isRunning)
847
+ return;
848
+ this.paused = false;
849
+ this.isRunning = true;
850
+ this.lastTime = 0;
851
+ this.update(0);
852
+ }
853
+ destroy() {
854
+ if (this.animationFrame) {
855
+ cancelAnimationFrame(this.animationFrame);
856
+ this.animationFrame = null;
857
+ }
858
+ this.canvas.removeEventListener('mousemove', this.handleMouseMove);
859
+ this.canvas.removeEventListener('touchmove', this.handleTouchMove);
860
+ this.canvas.removeEventListener('click', this.handleClick);
861
+ window.removeEventListener('keydown', this.handleKeyDown);
862
+ if (this.canvas.parentNode) {
863
+ this.canvas.parentNode.removeChild(this.canvas);
864
+ }
865
+ this.isRunning = false;
866
+ }
867
+ update(time) {
868
+ if (!this.isRunning) {
869
+ this.draw();
870
+ this.animationFrame = requestAnimationFrame(this.update.bind(this));
871
+ return;
872
+ }
873
+ const deltaTime = Math.min(time - this.lastTime, 50);
874
+ this.lastTime = time;
875
+ if (!this.paused && this.ballLaunched) {
876
+ this.updateGameState(deltaTime);
877
+ }
878
+ this.draw();
879
+ this.animationFrame = requestAnimationFrame(this.update.bind(this));
880
+ }
881
+ initBricks() {
882
+ const colors = this.getColors();
883
+ const rowColors = [colors.primary, colors.secondary, colors.accent];
884
+ const brickW = (this.width - this.brickPadding * (this.brickColumns + 1)) / this.brickColumns;
885
+ const brickH = 18;
886
+ this.bricks = [];
887
+ for (let r = 0; r < this.brickRows; r++) {
888
+ for (let c = 0; c < this.brickColumns; c++) {
889
+ const points = this.brickRows - r;
890
+ this.bricks.push({
891
+ x: this.brickPadding + c * (brickW + this.brickPadding),
892
+ y: this.brickOffsetTop + r * (brickH + this.brickPadding),
893
+ width: brickW,
894
+ height: brickH,
895
+ visible: true,
896
+ color: rowColors[r % rowColors.length],
897
+ points
898
+ });
899
+ }
900
+ }
901
+ }
902
+ resetGame() {
903
+ this.initBricks();
904
+ this.paddle.x = this.width / 2 - this.paddle.width / 2;
905
+ this.paddle.y = this.height - 25;
906
+ this.baseBallSpeed;
907
+ this.ball.x = this.width / 2;
908
+ this.ball.y = this.height - 40;
909
+ this.ball.dx = 0;
910
+ this.ball.dy = 0;
911
+ this.ballLaunched = false;
912
+ }
913
+ launchBall() {
914
+ if (this.ballLaunched)
915
+ return;
916
+ this.ballLaunched = true;
917
+ const speed = this.baseBallSpeed;
918
+ this.ball.dx = speed * 0.6;
919
+ this.ball.dy = -speed;
920
+ }
921
+ updateGameState(deltaTime) {
922
+ this.ball.x += this.ball.dx;
923
+ this.ball.y += this.ball.dy;
924
+ if (this.ball.x - this.ball.radius < 0) {
925
+ this.ball.x = this.ball.radius;
926
+ this.ball.dx = -this.ball.dx;
927
+ }
928
+ if (this.ball.x + this.ball.radius > this.width) {
929
+ this.ball.x = this.width - this.ball.radius;
930
+ this.ball.dx = -this.ball.dx;
931
+ }
932
+ if (this.ball.y - this.ball.radius < 0) {
933
+ this.ball.y = this.ball.radius;
934
+ this.ball.dy = -this.ball.dy;
935
+ }
936
+ if (this.ball.y + this.ball.radius > this.paddle.y &&
937
+ this.ball.y - this.ball.radius < this.paddle.y + this.paddle.height &&
938
+ this.ball.x >= this.paddle.x &&
939
+ this.ball.x <= this.paddle.x + this.paddle.width) {
940
+ const hitPos = (this.ball.x - (this.paddle.x + this.paddle.width / 2)) / (this.paddle.width / 2);
941
+ this.ball.dy = -Math.abs(this.ball.dy);
942
+ this.ball.dx = this.baseBallSpeed * 0.8 * hitPos;
943
+ this.ball.y = this.paddle.y - this.ball.radius;
944
+ }
945
+ if (this.ball.y + this.ball.radius > this.height) {
946
+ this.lives--;
947
+ if (this.lives <= 0) {
948
+ this.isRunning = false;
949
+ if (this.score > this.highScore)
950
+ this.highScore = this.score;
951
+ return;
952
+ }
953
+ this.ball.x = this.width / 2;
954
+ this.ball.y = this.height - 40;
955
+ this.ball.dx = 0;
956
+ this.ball.dy = 0;
957
+ this.ballLaunched = false;
958
+ return;
959
+ }
960
+ for (const brick of this.bricks) {
961
+ if (!brick.visible)
962
+ continue;
963
+ if (this.ball.x + this.ball.radius > brick.x &&
964
+ this.ball.x - this.ball.radius < brick.x + brick.width &&
965
+ this.ball.y + this.ball.radius > brick.y &&
966
+ this.ball.y - this.ball.radius < brick.y + brick.height) {
967
+ brick.visible = false;
968
+ this.updateScore(brick.points);
969
+ if (this.score > this.highScore)
970
+ this.highScore = this.score;
971
+ this.ball.dy = -this.ball.dy;
972
+ break;
973
+ }
974
+ }
975
+ const visibleCount = this.bricks.filter(b => b.visible).length;
976
+ if (visibleCount === 0) {
977
+ this.level++;
978
+ this.baseBallSpeed = Math.min(this.baseBallSpeed * 1.1, this.baseBallSpeed * 2);
979
+ this.resetGame();
980
+ this.ballLaunched = false;
981
+ }
982
+ }
983
+ draw() {
984
+ const colors = this.getColors();
985
+ this.ctx.fillStyle = '#0a0a12';
986
+ this.ctx.fillRect(0, 0, this.width, this.height);
987
+ this.bricks.forEach(b => {
988
+ if (!b.visible)
989
+ return;
990
+ this.ctx.fillStyle = b.color;
991
+ this.ctx.fillRect(b.x, b.y, b.width, b.height);
992
+ });
993
+ this.ctx.fillStyle = colors.primary;
994
+ this.ctx.fillRect(this.paddle.x, this.paddle.y, this.paddle.width, this.paddle.height);
995
+ this.ctx.fillStyle = colors.accent;
996
+ this.ctx.beginPath();
997
+ this.ctx.arc(this.ball.x, this.ball.y, this.ball.radius, 0, Math.PI * 2);
998
+ this.ctx.fill();
999
+ this.ctx.fillStyle = colors.text;
1000
+ this.ctx.font = '14px Arial';
1001
+ this.ctx.fillText(`Lives: ${this.lives} Score: ${this.score} High: ${this.highScore}`, 8, 22);
1002
+ this.ctx.fillText(`Level ${this.level}`, this.width - 60, 22);
1003
+ for (let i = 0; i < this.lives; i++) {
1004
+ this.ctx.beginPath();
1005
+ this.ctx.arc(8 + i * 14, 38, 5, 0, Math.PI * 2);
1006
+ this.ctx.fillStyle = colors.accent;
1007
+ this.ctx.fill();
1008
+ }
1009
+ if (this.paused) {
1010
+ this.ctx.fillStyle = 'rgba(0,0,0,0.6)';
1011
+ this.ctx.fillRect(0, 0, this.width, this.height);
1012
+ this.ctx.fillStyle = '#fff';
1013
+ this.ctx.font = '24px Arial';
1014
+ this.ctx.textAlign = 'center';
1015
+ this.ctx.fillText('Paused - Press Space', this.width / 2, this.height / 2);
1016
+ this.ctx.textAlign = 'left';
1017
+ }
1018
+ if (!this.isRunning && this.lives <= 0) {
1019
+ this.ctx.fillStyle = 'rgba(0,0,0,0.7)';
1020
+ this.ctx.fillRect(0, 0, this.width, this.height);
1021
+ this.ctx.fillStyle = '#fff';
1022
+ this.ctx.font = '28px Arial';
1023
+ this.ctx.textAlign = 'center';
1024
+ this.ctx.fillText('Game Over', this.width / 2, this.height / 2 - 20);
1025
+ this.ctx.font = '16px Arial';
1026
+ this.ctx.fillText(`Score: ${this.score}`, this.width / 2, this.height / 2 + 10);
1027
+ this.ctx.textAlign = 'left';
1028
+ }
1029
+ if (this.isRunning && !this.ballLaunched) {
1030
+ this.ctx.fillStyle = 'rgba(0,0,0,0.4)';
1031
+ this.ctx.font = '14px Arial';
1032
+ this.ctx.textAlign = 'center';
1033
+ this.ctx.fillText('Click or tap to launch', this.width / 2, this.height / 2);
1034
+ this.ctx.textAlign = 'left';
1035
+ }
1036
+ }
1037
+ setupControls() {
1038
+ this.canvas.addEventListener('mousemove', this.handleMouseMove);
1039
+ this.canvas.addEventListener('touchmove', this.handleTouchMove, { passive: false });
1040
+ this.canvas.addEventListener('click', this.handleClick);
1041
+ window.addEventListener('keydown', this.handleKeyDown);
1042
+ }
1043
+ }
1044
+
1045
+ /**
1046
+ * Load a game instance
1047
+ * @param gameType Type of game to load
1048
+ * @param config Game configuration
1049
+ * @returns Game instance
1050
+ */
1051
+ function loadGame(gameType, config) {
1052
+ switch (gameType) {
1053
+ case 'tetris':
1054
+ return new TetrisGame(config);
1055
+ case 'snake':
1056
+ return new SnakeGame(config);
1057
+ case 'breakout':
1058
+ return new BreakoutGame(config);
1059
+ default:
1060
+ // TypeScript should prevent this, but just in case
1061
+ return new TetrisGame(config);
1062
+ }
1063
+ }
1064
+
1065
+ /**
1066
+ * Validate and normalize game options
1067
+ * @param options User-provided game options
1068
+ * @returns Validated game options
1069
+ */
1070
+ function validateOptions(options) {
1071
+ const validatedOptions = { ...options };
1072
+ // Validate game type
1073
+ if (options.game && !isValidGameType(options.game)) {
1074
+ console.warn(`Invalid game type: ${options.game}. Defaulting to tetris.`);
1075
+ validatedOptions.game = 'tetris';
1076
+ }
1077
+ // Validate theme
1078
+ if (options.theme && !isValidThemeType(options.theme)) {
1079
+ console.warn(`Invalid theme: ${options.theme}. Defaulting to default theme.`);
1080
+ validatedOptions.theme = 'default';
1081
+ }
1082
+ // Ensure message is a string
1083
+ if (options.message && typeof options.message !== 'string') {
1084
+ validatedOptions.message = String(options.message);
1085
+ }
1086
+ // Validate dimensions
1087
+ if (options.width && (typeof options.width !== 'number' || options.width <= 0)) {
1088
+ delete validatedOptions.width;
1089
+ }
1090
+ if (options.height && (typeof options.height !== 'number' || options.height <= 0)) {
1091
+ delete validatedOptions.height;
1092
+ }
1093
+ return validatedOptions;
1094
+ }
1095
+ /**
1096
+ * Check if a game type is valid
1097
+ * @param game Game type to validate
1098
+ * @returns Whether the game type is valid
1099
+ */
1100
+ function isValidGameType(game) {
1101
+ return ['tetris', 'snake', 'breakout'].includes(game);
1102
+ }
1103
+ /**
1104
+ * Check if a theme type is valid
1105
+ * @param theme Theme type to validate
1106
+ * @returns Whether the theme type is valid
1107
+ */
1108
+ function isValidThemeType(theme) {
1109
+ return ['default', 'cyberpunk', 'minimal', 'corporate'].includes(theme);
1110
+ }
1111
+ /**
1112
+ * Preload common assets used by games
1113
+ */
1114
+ function preloadAssets() {
1115
+ // This would preload images, sounds, etc. needed by the games
1116
+ // For now, we'll just create a placeholder implementation
1117
+ // Example of image preloading
1118
+ const imageUrls = [
1119
+ // These would be actual URLs in production
1120
+ // 'https://assets.waitlessapi.com/tetris/blocks.png',
1121
+ // 'https://assets.waitlessapi.com/common/bg.png'
1122
+ ];
1123
+ imageUrls.forEach(url => {
1124
+ const img = new Image();
1125
+ img.src = url;
1126
+ });
1127
+ }
1128
+
1129
+ class AdPlayer {
1130
+ constructor(container) {
1131
+ this.loadingOverlay = null;
1132
+ this.adBadge = null;
1133
+ this.container = container;
1134
+ this.adDisplayContainer = null;
1135
+ this.adsLoader = null;
1136
+ this.adsManager = null;
1137
+ this.videoElement = null;
1138
+ this.adContainer = null;
1139
+ this.setupElements();
1140
+ }
1141
+ setupElements() {
1142
+ this.videoElement = document.createElement('video');
1143
+ this.videoElement.style.cssText = `
1144
+ width: 100%;
1145
+ height: 100%;
1146
+ object-fit: contain;
1147
+ background: #000;
1148
+ `;
1149
+ this.adContainer = document.createElement('div');
1150
+ const w = Math.max(this.container.offsetWidth || 1, 1);
1151
+ const h = Math.max(this.container.offsetHeight || 1, 1);
1152
+ this.adContainer.style.cssText = `
1153
+ position: absolute;
1154
+ top: 0;
1155
+ left: 0;
1156
+ width: 100%;
1157
+ height: 100%;
1158
+ min-width: ${w}px;
1159
+ min-height: ${h}px;
1160
+ `;
1161
+ this.adContainer.appendChild(this.videoElement);
1162
+ this.adBadge = document.createElement('div');
1163
+ this.adBadge.textContent = 'Ad';
1164
+ this.adBadge.style.cssText = `
1165
+ position: absolute;
1166
+ top: 8px;
1167
+ right: 8px;
1168
+ font-size: 10px;
1169
+ font-family: sans-serif;
1170
+ color: rgba(255,255,255,0.9);
1171
+ background: rgba(0,0,0,0.5);
1172
+ padding: 2px 6px;
1173
+ border-radius: 2px;
1174
+ z-index: 10;
1175
+ display: none;
1176
+ `;
1177
+ this.adContainer.appendChild(this.adBadge);
1178
+ this.container.appendChild(this.adContainer);
1179
+ }
1180
+ showLoadingOverlay() {
1181
+ if (this.loadingOverlay && this.loadingOverlay.parentNode)
1182
+ return;
1183
+ this.loadingOverlay = document.createElement('div');
1184
+ this.loadingOverlay.setAttribute('aria-live', 'polite');
1185
+ this.loadingOverlay.setAttribute('aria-label', 'Loading ad');
1186
+ this.loadingOverlay.style.cssText = `
1187
+ position: absolute;
1188
+ top: 0; left: 0;
1189
+ width: 100%; height: 100%;
1190
+ background: rgba(0,0,0,0.85);
1191
+ display: flex;
1192
+ flex-direction: column;
1193
+ align-items: center;
1194
+ justify-content: center;
1195
+ gap: 12px;
1196
+ z-index: 20;
1197
+ color: rgba(255,255,255,0.9);
1198
+ font-family: sans-serif;
1199
+ font-size: 14px;
1200
+ `;
1201
+ const spinner = document.createElement('div');
1202
+ spinner.style.cssText = `
1203
+ width: 32px; height: 32px;
1204
+ border: 3px solid rgba(255,255,255,0.2);
1205
+ border-top-color: #fff;
1206
+ border-radius: 50%;
1207
+ animation: ad-spin 0.8s linear infinite;
1208
+ `;
1209
+ const style = document.createElement('style');
1210
+ style.textContent = '@keyframes ad-spin { to { transform: rotate(360deg); } }';
1211
+ this.loadingOverlay.appendChild(style);
1212
+ this.loadingOverlay.appendChild(spinner);
1213
+ const text = document.createElement('span');
1214
+ text.textContent = 'Loading ad…';
1215
+ this.loadingOverlay.appendChild(text);
1216
+ this.container.appendChild(this.loadingOverlay);
1217
+ }
1218
+ removeLoadingOverlay() {
1219
+ if (this.loadingOverlay && this.loadingOverlay.parentNode) {
1220
+ this.loadingOverlay.parentNode.removeChild(this.loadingOverlay);
1221
+ }
1222
+ this.loadingOverlay = null;
1223
+ }
1224
+ showErrorOverlay(thenRun) {
1225
+ this.removeLoadingOverlay();
1226
+ const overlay = document.createElement('div');
1227
+ overlay.setAttribute('aria-live', 'polite');
1228
+ overlay.setAttribute('aria-label', 'Ad could not load');
1229
+ overlay.style.cssText = `
1230
+ position: absolute;
1231
+ top: 0; left: 0;
1232
+ width: 100%; height: 100%;
1233
+ background: rgba(0,0,0,0.9);
1234
+ display: flex;
1235
+ flex-direction: column;
1236
+ align-items: center;
1237
+ justify-content: center;
1238
+ gap: 16px;
1239
+ z-index: 30;
1240
+ color: rgba(255,255,255,0.95);
1241
+ font-family: sans-serif;
1242
+ font-size: 14px;
1243
+ `;
1244
+ const message = document.createElement('p');
1245
+ message.textContent = "Ad couldn't load";
1246
+ message.style.margin = '0';
1247
+ overlay.appendChild(message);
1248
+ const btn = document.createElement('button');
1249
+ btn.textContent = 'Continue';
1250
+ btn.style.cssText = `
1251
+ padding: 8px 20px;
1252
+ font-size: 14px;
1253
+ cursor: pointer;
1254
+ background: rgba(255,255,255,0.2);
1255
+ color: #fff;
1256
+ border: 1px solid rgba(255,255,255,0.4);
1257
+ border-radius: 4px;
1258
+ `;
1259
+ const run = () => {
1260
+ if (overlay.parentNode)
1261
+ overlay.parentNode.removeChild(overlay);
1262
+ thenRun();
1263
+ };
1264
+ btn.addEventListener('click', run);
1265
+ overlay.appendChild(btn);
1266
+ this.container.appendChild(overlay);
1267
+ setTimeout(run, 3000);
1268
+ }
1269
+ async playAd(config) {
1270
+ return new Promise((resolve) => {
1271
+ var _a;
1272
+ this.showLoadingOverlay();
1273
+ const onError = () => {
1274
+ this.showErrorOverlay(() => {
1275
+ this.cleanup();
1276
+ resolve();
1277
+ });
1278
+ };
1279
+ if (!((_a = window.google) === null || _a === void 0 ? void 0 : _a.ima)) {
1280
+ this.loadIMAScript()
1281
+ .then(() => this.initializeAd(config, resolve, onError))
1282
+ .catch(() => {
1283
+ var _a;
1284
+ (_a = config.onAdError) === null || _a === void 0 ? void 0 : _a.call(config, new Error('Failed to load IMA SDK'));
1285
+ onError();
1286
+ });
1287
+ }
1288
+ else {
1289
+ this.initializeAd(config, resolve, onError);
1290
+ }
1291
+ });
1292
+ }
1293
+ loadIMAScript() {
1294
+ return new Promise((resolve, reject) => {
1295
+ var _a;
1296
+ if ((_a = window.google) === null || _a === void 0 ? void 0 : _a.ima) {
1297
+ resolve();
1298
+ return;
1299
+ }
1300
+ const script = document.createElement('script');
1301
+ script.src = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
1302
+ script.async = true;
1303
+ script.onload = () => resolve();
1304
+ script.onerror = () => reject(new Error('Failed to load IMA SDK'));
1305
+ document.head.appendChild(script);
1306
+ });
1307
+ }
1308
+ initializeAd(config, resolve, onError) {
1309
+ var _a;
1310
+ const google = window.google;
1311
+ if (!(google === null || google === void 0 ? void 0 : google.ima)) {
1312
+ (_a = config.onAdError) === null || _a === void 0 ? void 0 : _a.call(config, new Error('IMA SDK not available'));
1313
+ onError();
1314
+ return;
1315
+ }
1316
+ this.adDisplayContainer = new google.ima.AdDisplayContainer(this.adContainer, this.videoElement);
1317
+ this.adsLoader = new google.ima.AdsLoader(this.adDisplayContainer);
1318
+ this.adsLoader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, (event) => this.onAdsManagerLoaded(event, config, resolve, onError), false);
1319
+ this.adsLoader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (event) => {
1320
+ var _a;
1321
+ console.warn('Ad error:', event.getError());
1322
+ (_a = config.onAdError) === null || _a === void 0 ? void 0 : _a.call(config, event.getError());
1323
+ this.showErrorOverlay(() => {
1324
+ this.cleanup();
1325
+ resolve();
1326
+ });
1327
+ }, false);
1328
+ const adsRequest = new google.ima.AdsRequest();
1329
+ adsRequest.adTagUrl = config.adTagUrl;
1330
+ adsRequest.linearAdSlotWidth = this.container.offsetWidth;
1331
+ adsRequest.linearAdSlotHeight = this.container.offsetHeight;
1332
+ adsRequest.nonLinearAdSlotWidth = this.container.offsetWidth;
1333
+ adsRequest.nonLinearAdSlotHeight = this.container.offsetHeight;
1334
+ this.adDisplayContainer.initialize();
1335
+ this.adsLoader.requestAds(adsRequest);
1336
+ }
1337
+ onAdsManagerLoaded(event, config, resolve, onError) {
1338
+ var _a;
1339
+ const google = window.google;
1340
+ if (!(google === null || google === void 0 ? void 0 : google.ima)) {
1341
+ resolve();
1342
+ return;
1343
+ }
1344
+ const adsRenderingSettings = new google.ima.AdsRenderingSettings();
1345
+ adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true;
1346
+ this.adsManager = event.getAdsManager(this.videoElement, adsRenderingSettings);
1347
+ this.adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (e) => {
1348
+ var _a;
1349
+ console.warn('Ad manager error:', e.getError());
1350
+ (_a = config.onAdError) === null || _a === void 0 ? void 0 : _a.call(config, e.getError());
1351
+ this.showErrorOverlay(() => {
1352
+ this.cleanup();
1353
+ resolve();
1354
+ });
1355
+ });
1356
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, () => {
1357
+ var _a;
1358
+ this.removeLoadingOverlay();
1359
+ if (this.adBadge)
1360
+ this.adBadge.style.display = 'block';
1361
+ (_a = config.onAdStarted) === null || _a === void 0 ? void 0 : _a.call(config);
1362
+ });
1363
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, () => {
1364
+ var _a;
1365
+ (_a = config.onAdComplete) === null || _a === void 0 ? void 0 : _a.call(config);
1366
+ this.cleanup();
1367
+ resolve();
1368
+ });
1369
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.ALL_ADS_COMPLETED, () => {
1370
+ var _a;
1371
+ (_a = config.onAdComplete) === null || _a === void 0 ? void 0 : _a.call(config);
1372
+ this.cleanup();
1373
+ resolve();
1374
+ });
1375
+ this.adsManager.addEventListener(google.ima.AdEvent.Type.SKIPPED, () => {
1376
+ var _a;
1377
+ (_a = config.onAdSkipped) === null || _a === void 0 ? void 0 : _a.call(config);
1378
+ this.cleanup();
1379
+ resolve();
1380
+ });
1381
+ try {
1382
+ this.adsManager.init(this.container.offsetWidth, this.container.offsetHeight, google.ima.ViewMode.NORMAL);
1383
+ this.adsManager.start();
1384
+ }
1385
+ catch (error) {
1386
+ console.warn('Ad start error:', error);
1387
+ (_a = config.onAdError) === null || _a === void 0 ? void 0 : _a.call(config, error);
1388
+ this.showErrorOverlay(() => {
1389
+ this.cleanup();
1390
+ resolve();
1391
+ });
1392
+ }
1393
+ }
1394
+ cleanup() {
1395
+ if (this.adsManager) {
1396
+ try {
1397
+ this.adsManager.destroy();
1398
+ }
1399
+ catch (_) { }
1400
+ this.adsManager = null;
1401
+ }
1402
+ if (this.adsLoader) {
1403
+ try {
1404
+ this.adsLoader.destroy();
1405
+ }
1406
+ catch (_) { }
1407
+ this.adsLoader = null;
1408
+ }
1409
+ this.removeLoadingOverlay();
1410
+ if (this.adContainer && this.adContainer.parentNode) {
1411
+ this.adContainer.parentNode.removeChild(this.adContainer);
1412
+ }
1413
+ this.adBadge = null;
1414
+ }
1415
+ destroy() {
1416
+ this.cleanup();
1417
+ }
1418
+ }
1419
+
1420
+ const CONFIG_CACHE_TTL_MS = 5 * 60 * 1000;
1421
+ const CONFIG_CACHE_KEY_PREFIX = 'waitless_config_';
1422
+ const DEFAULT_AD_TAG_URL = 'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/single_ad_samples&sz=640x480&cust_params=sample_ct%3Dlinear&ciu_szs=300x250%2C728x90&gdfp_req=1&output=vast&unviewed_position_start=1&env=vp&impl=s&correlator=';
1423
+ class WaitlessAPI {
1424
+ /**
1425
+ * Initialize the WaitlessAPI SDK
1426
+ * @param apiKey Your WaitlessAPI key
1427
+ * @param baseUrlOrOptions Base URL string or options object with apiBaseUrl (defaults to production)
1428
+ */
1429
+ constructor(apiKey, baseUrlOrOptions = 'https://api.waitlessapi.com') {
1430
+ var _a;
1431
+ this.sessionId = null;
1432
+ this.container = null;
1433
+ this.gameInstance = null;
1434
+ this.isPlaying = false;
1435
+ this.lastBackendConfig = null;
1436
+ this.config = null;
1437
+ this.configLoaded = false;
1438
+ this.configPromise = null;
1439
+ this.batchIdsSeen = new Set();
1440
+ this.lastAdTime = 0;
1441
+ this.adsThisSession = 0;
1442
+ this.adsThisHour = 0;
1443
+ this.lastHourStart = 0;
1444
+ if (!apiKey) {
1445
+ throw new Error('API key is required');
1446
+ }
1447
+ const baseUrl = typeof baseUrlOrOptions === 'string'
1448
+ ? baseUrlOrOptions
1449
+ : (_a = baseUrlOrOptions === null || baseUrlOrOptions === void 0 ? void 0 : baseUrlOrOptions.apiBaseUrl) !== null && _a !== void 0 ? _a : 'https://api.waitlessapi.com';
1450
+ this.apiKey = apiKey;
1451
+ this.baseUrl = baseUrl;
1452
+ this.apiBaseUrl = baseUrl;
1453
+ this.defaultAdTagUrl = DEFAULT_AD_TAG_URL;
1454
+ this.configPromise = this.loadConfig();
1455
+ preloadAssets();
1456
+ }
1457
+ getDefaultConfig() {
1458
+ return {
1459
+ strategy: { preset: 'balanced', behavior: 'auto' },
1460
+ frequency: { minInterval: 300, maxAdsPerSession: 3, maxAdsPerHour: 6 },
1461
+ duration: { shortThreshold: 10, longThreshold: 60, adMaxDuration: 15, enableGames: true },
1462
+ batch: { enabled: true, showAdOnce: true },
1463
+ fallback: { type: 'spinner', showMessage: true },
1464
+ analytics: { trackEvents: true, enableABTesting: false }
1465
+ };
1466
+ }
1467
+ getCachedConfig() {
1468
+ try {
1469
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem(CONFIG_CACHE_KEY_PREFIX + this.apiKey) : null;
1470
+ if (!raw)
1471
+ return null;
1472
+ const { config, timestamp } = JSON.parse(raw);
1473
+ if (Date.now() - timestamp > CONFIG_CACHE_TTL_MS)
1474
+ return null;
1475
+ return config;
1476
+ }
1477
+ catch (_a) {
1478
+ return null;
1479
+ }
1480
+ }
1481
+ cacheConfig(config) {
1482
+ try {
1483
+ if (typeof localStorage !== 'undefined') {
1484
+ localStorage.setItem(CONFIG_CACHE_KEY_PREFIX + this.apiKey, JSON.stringify({ config, timestamp: Date.now() }));
1485
+ }
1486
+ }
1487
+ catch (_a) {
1488
+ // ignore
1489
+ }
1490
+ }
1491
+ async loadConfig() {
1492
+ const cached = this.getCachedConfig();
1493
+ if (cached) {
1494
+ this.config = cached;
1495
+ this.configLoaded = true;
1496
+ return;
1497
+ }
1498
+ try {
1499
+ const url = `${this.apiBaseUrl}/api/keys/${encodeURIComponent(this.apiKey)}/config`;
1500
+ const res = await fetch(url);
1501
+ if (res.ok) {
1502
+ const data = (await res.json());
1503
+ this.config = data;
1504
+ this.cacheConfig(data);
1505
+ }
1506
+ else {
1507
+ this.config = this.getDefaultConfig();
1508
+ }
1509
+ }
1510
+ catch (_a) {
1511
+ this.config = this.getDefaultConfig();
1512
+ }
1513
+ this.configLoaded = true;
1514
+ }
1515
+ /** Wait for config to be loaded (e.g. before using showAd). */
1516
+ async ensureConfigLoaded() {
1517
+ if (!this.configLoaded && this.configPromise)
1518
+ await this.configPromise;
1519
+ }
1520
+ /** Clear cache and reload config from API. */
1521
+ async refreshConfig() {
1522
+ try {
1523
+ if (typeof localStorage !== 'undefined')
1524
+ localStorage.removeItem(CONFIG_CACHE_KEY_PREFIX + this.apiKey);
1525
+ }
1526
+ catch (_a) {
1527
+ // ignore
1528
+ }
1529
+ this.configLoaded = false;
1530
+ this.configPromise = this.loadConfig();
1531
+ await this.configPromise;
1532
+ }
1533
+ /** Return current config (for debugging). */
1534
+ getConfig() {
1535
+ return this.config;
1536
+ }
1537
+ /**
1538
+ * Run processing with a minimal fallback UI (spinner/message). No ad or game.
1539
+ */
1540
+ async showProcessing(processingFn, type, message) {
1541
+ const overlay = document.createElement('div');
1542
+ overlay.style.cssText =
1543
+ 'position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.7);display:flex;flex-direction:column;align-items:center;justify-content:center;';
1544
+ const text = document.createElement('div');
1545
+ text.textContent = message || 'Processing...';
1546
+ text.style.cssText = 'color:#fff;font-size:18px;margin-top:12px;';
1547
+ const spinner = document.createElement('div');
1548
+ spinner.style.cssText =
1549
+ 'width:40px;height:40px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:waitless-spin 0.8s linear infinite;';
1550
+ if (!document.getElementById('waitless-spinner-style')) {
1551
+ const style = document.createElement('style');
1552
+ style.id = 'waitless-spinner-style';
1553
+ style.textContent = '@keyframes waitless-spin { to { transform: rotate(360deg); } }';
1554
+ document.head.appendChild(style);
1555
+ }
1556
+ overlay.appendChild(spinner);
1557
+ overlay.appendChild(text);
1558
+ document.body.appendChild(overlay);
1559
+ try {
1560
+ return await processingFn();
1561
+ }
1562
+ finally {
1563
+ overlay.remove();
1564
+ }
1565
+ }
1566
+ shouldShowAd(frequency) {
1567
+ var _a, _b, _c;
1568
+ if (!frequency)
1569
+ return true;
1570
+ const now = Date.now();
1571
+ const minInterval = ((_a = frequency.minInterval) !== null && _a !== void 0 ? _a : 300) * 1000;
1572
+ if (now - this.lastAdTime < minInterval)
1573
+ return false;
1574
+ const maxSession = (_b = frequency.maxAdsPerSession) !== null && _b !== void 0 ? _b : 3;
1575
+ if (this.adsThisSession >= maxSession)
1576
+ return false;
1577
+ const hourMs = 60 * 60 * 1000;
1578
+ const currentHourStart = Math.floor(now / hourMs) * hourMs;
1579
+ if (currentHourStart !== this.lastHourStart) {
1580
+ this.lastHourStart = currentHourStart;
1581
+ this.adsThisHour = 0;
1582
+ }
1583
+ const maxHour = (_c = frequency.maxAdsPerHour) !== null && _c !== void 0 ? _c : 6;
1584
+ if (this.adsThisHour >= maxHour)
1585
+ return false;
1586
+ return true;
1587
+ }
1588
+ recordAdShown() {
1589
+ this.lastAdTime = Date.now();
1590
+ this.adsThisSession += 1;
1591
+ this.adsThisHour += 1;
1592
+ }
1593
+ /**
1594
+ * Config-driven entry point: show ad and/or game based on dashboard config, then run processing.
1595
+ */
1596
+ async showAd(processingFn, options = {}) {
1597
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
1598
+ if (!processingFn || typeof processingFn !== 'function') {
1599
+ throw new Error('Processing function is required and must be a function');
1600
+ }
1601
+ await this.ensureConfigLoaded();
1602
+ const base = (_a = this.config) !== null && _a !== void 0 ? _a : this.getDefaultConfig();
1603
+ const merged = {
1604
+ ...base,
1605
+ strategy: { ...base.strategy, ...(_b = options.overrideConfig) === null || _b === void 0 ? void 0 : _b.strategy },
1606
+ frequency: { ...base.frequency, ...(_c = options.overrideConfig) === null || _c === void 0 ? void 0 : _c.frequency },
1607
+ duration: { ...base.duration, ...(_d = options.overrideConfig) === null || _d === void 0 ? void 0 : _d.duration },
1608
+ batch: { ...base.batch, ...(_e = options.overrideConfig) === null || _e === void 0 ? void 0 : _e.batch },
1609
+ fallback: { ...base.fallback, ...(_f = options.overrideConfig) === null || _f === void 0 ? void 0 : _f.fallback },
1610
+ analytics: { ...base.analytics, ...(_g = options.overrideConfig) === null || _g === void 0 ? void 0 : _g.analytics }
1611
+ };
1612
+ const batch = (_h = merged.batch) !== null && _h !== void 0 ? _h : {};
1613
+ const frequency = (_j = merged.frequency) !== null && _j !== void 0 ? _j : {};
1614
+ const strategy = (_k = merged.strategy) !== null && _k !== void 0 ? _k : {};
1615
+ const behavior = (_l = strategy.behavior) !== null && _l !== void 0 ? _l : 'auto';
1616
+ const duration = (_m = merged.duration) !== null && _m !== void 0 ? _m : {};
1617
+ const message = (_o = options.message) !== null && _o !== void 0 ? _o : 'Loading...';
1618
+ if (options.batchId && batch.enabled && batch.showAdOnce && this.batchIdsSeen.has(options.batchId)) {
1619
+ return this.showProcessing(processingFn, 'spinner', message);
1620
+ }
1621
+ if (!this.shouldShowAd(frequency)) {
1622
+ return this.showProcessing(processingFn, 'spinner', message);
1623
+ }
1624
+ const shortThreshold = (_p = duration.shortThreshold) !== null && _p !== void 0 ? _p : 10;
1625
+ const longThreshold = (_q = duration.longThreshold) !== null && _q !== void 0 ? _q : 60;
1626
+ const estimatedDuration = (_r = options.estimatedDuration) !== null && _r !== void 0 ? _r : 0;
1627
+ const enableGames = duration.enableGames !== false;
1628
+ let mode = 'ad-only';
1629
+ if (behavior === 'game-only' || (enableGames && estimatedDuration > longThreshold)) {
1630
+ mode = 'game-only';
1631
+ }
1632
+ else if (enableGames && estimatedDuration > shortThreshold && estimatedDuration <= longThreshold) {
1633
+ mode = 'ad-plus-game';
1634
+ }
1635
+ if (options.batchId && batch.enabled && batch.showAdOnce) {
1636
+ this.batchIdsSeen.add(options.batchId);
1637
+ }
1638
+ if (mode === 'game-only') {
1639
+ return this.showGame(processingFn, { message });
1640
+ }
1641
+ const validatedOptions = validateOptions({ container: undefined, message });
1642
+ const session = await this.createSession(validatedOptions);
1643
+ const { container, overlay } = this.initializeGameUI(validatedOptions);
1644
+ const adMaxDuration = (_s = duration.adMaxDuration) !== null && _s !== void 0 ? _s : 15;
1645
+ try {
1646
+ if (session.sessionId) {
1647
+ await this.playAdInContainer(container, {
1648
+ adTagUrl: this.defaultAdTagUrl,
1649
+ maxDuration: adMaxDuration,
1650
+ sessionId: session.sessionId
1651
+ });
1652
+ this.recordAdShown();
1653
+ }
1654
+ if (mode === 'ad-plus-game') {
1655
+ const gameInstance = this.startGame(validatedOptions);
1656
+ const result = await processingFn();
1657
+ this.endGame(session.sessionId);
1658
+ this.cleanup(overlay, gameInstance);
1659
+ return result;
1660
+ }
1661
+ container.innerHTML = '';
1662
+ const spinner = document.createElement('div');
1663
+ spinner.style.cssText =
1664
+ 'width:40px;height:40px;border:3px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:waitless-spin 0.8s linear infinite;margin:auto;';
1665
+ const text = document.createElement('div');
1666
+ text.textContent = message;
1667
+ text.style.cssText = 'color:#fff;font-size:18px;margin-top:12px;text-align:center;';
1668
+ container.style.display = 'flex';
1669
+ container.style.flexDirection = 'column';
1670
+ container.style.alignItems = 'center';
1671
+ container.style.justifyContent = 'center';
1672
+ container.appendChild(spinner);
1673
+ container.appendChild(text);
1674
+ if (!document.getElementById('waitless-spinner-style')) {
1675
+ const style = document.createElement('style');
1676
+ style.id = 'waitless-spinner-style';
1677
+ style.textContent = '@keyframes waitless-spin { to { transform: rotate(360deg); } }';
1678
+ document.head.appendChild(style);
1679
+ }
1680
+ const result = await processingFn();
1681
+ this.endGame(session.sessionId);
1682
+ this.cleanup(overlay, null);
1683
+ return result;
1684
+ }
1685
+ catch (err) {
1686
+ this.endGame(session.sessionId);
1687
+ this.cleanup(overlay, null);
1688
+ throw err;
1689
+ }
1690
+ }
1691
+ /**
1692
+ * Show a game while executing a long-running function
1693
+ * @param processingFn The function to execute while showing the game
1694
+ * @param options Game configuration options
1695
+ * @returns Promise with the result of the processing function
1696
+ */
1697
+ async showGame(processingFn, options = {}) {
1698
+ if (!processingFn || typeof processingFn !== 'function') {
1699
+ throw new Error('Processing function is required and must be a function');
1700
+ }
1701
+ const showAds = options.showAds !== false && this.apiKey !== 'demo';
1702
+ const adPlacement = options.adPlacement || 'pre-game';
1703
+ const validatedOptions = validateOptions(options);
1704
+ const session = await this.createSession(validatedOptions);
1705
+ const { container, overlay } = this.initializeGameUI(validatedOptions);
1706
+ let gameInstance = null;
1707
+ try {
1708
+ if (showAds && session.sessionId && (adPlacement === 'pre-game' || adPlacement === 'both')) {
1709
+ await this.playAdInContainer(container, {
1710
+ adTagUrl: options.adTagUrl || this.defaultAdTagUrl,
1711
+ maxDuration: options.adDuration || 15,
1712
+ sessionId: session.sessionId
1713
+ });
1714
+ }
1715
+ gameInstance = this.startGame(validatedOptions);
1716
+ const result = await processingFn();
1717
+ if (showAds && session.sessionId && (adPlacement === 'post-game' || adPlacement === 'both')) {
1718
+ await this.playAdInContainer(container, {
1719
+ adTagUrl: options.adTagUrl || this.defaultAdTagUrl,
1720
+ maxDuration: options.adDuration || 15,
1721
+ sessionId: session.sessionId
1722
+ });
1723
+ }
1724
+ this.endGame(session.sessionId);
1725
+ this.cleanup(overlay, gameInstance);
1726
+ return result;
1727
+ }
1728
+ catch (error) {
1729
+ this.endGame(session.sessionId);
1730
+ this.cleanup(overlay, gameInstance || this.gameInstance);
1731
+ throw error;
1732
+ }
1733
+ }
1734
+ /**
1735
+ * Create a new game session with the API
1736
+ * @param options Game options
1737
+ * @returns Session id and config (null for demo/offline)
1738
+ */
1739
+ async createSession(options) {
1740
+ if (this.apiKey === 'demo') {
1741
+ this.sessionId = null;
1742
+ this.lastBackendConfig = null;
1743
+ return { sessionId: null, config: null };
1744
+ }
1745
+ try {
1746
+ const response = await fetch(`${this.baseUrl}/sessions`, {
1747
+ method: 'POST',
1748
+ headers: {
1749
+ 'Content-Type': 'application/json',
1750
+ 'x-api-key': this.apiKey
1751
+ },
1752
+ body: JSON.stringify({
1753
+ game: options.game,
1754
+ theme: options.theme,
1755
+ message: options.message
1756
+ })
1757
+ });
1758
+ if (!response.ok) {
1759
+ throw new Error(`Failed to create game session: ${response.statusText}`);
1760
+ }
1761
+ const data = await response.json();
1762
+ this.sessionId = data.sessionId;
1763
+ this.lastBackendConfig = data.config && typeof data.config === 'object' ? data.config : null;
1764
+ return { sessionId: this.sessionId, config: this.lastBackendConfig };
1765
+ }
1766
+ catch (error) {
1767
+ console.error('Error creating WaitlessAPI session:', error);
1768
+ this.sessionId = null;
1769
+ this.lastBackendConfig = null;
1770
+ return { sessionId: null, config: null };
1771
+ }
1772
+ }
1773
+ /**
1774
+ * Initialize the game UI container
1775
+ * @param options Game options
1776
+ * @returns container and overlay (overlay is non-null when we created the full-screen div)
1777
+ */
1778
+ initializeGameUI(options) {
1779
+ let overlay = null;
1780
+ if (options.container) {
1781
+ if (typeof options.container === 'string') {
1782
+ this.container = document.querySelector(options.container);
1783
+ if (!this.container) {
1784
+ throw new Error(`Container element not found: ${options.container}`);
1785
+ }
1786
+ }
1787
+ else {
1788
+ this.container = options.container;
1789
+ }
1790
+ }
1791
+ else {
1792
+ this.container = document.createElement('div');
1793
+ this.container.id = 'waitless-game-container';
1794
+ this.container.style.position = 'fixed';
1795
+ this.container.style.top = '0';
1796
+ this.container.style.left = '0';
1797
+ this.container.style.width = '100%';
1798
+ this.container.style.height = '100%';
1799
+ this.container.style.zIndex = '9999';
1800
+ document.body.appendChild(this.container);
1801
+ overlay = this.container;
1802
+ }
1803
+ if (options.width) {
1804
+ this.container.style.width = `${options.width}px`;
1805
+ }
1806
+ if (options.height) {
1807
+ this.container.style.height = `${options.height}px`;
1808
+ }
1809
+ const messageElement = document.createElement('div');
1810
+ messageElement.id = 'waitless-message';
1811
+ messageElement.textContent = options.message || 'Loading...';
1812
+ messageElement.style.position = 'absolute';
1813
+ messageElement.style.bottom = '20px';
1814
+ messageElement.style.left = '0';
1815
+ messageElement.style.width = '100%';
1816
+ messageElement.style.textAlign = 'center';
1817
+ messageElement.style.color = '#ffffff';
1818
+ messageElement.style.fontSize = '18px';
1819
+ messageElement.style.fontFamily = 'Arial, sans-serif';
1820
+ messageElement.style.padding = '10px';
1821
+ this.container.appendChild(messageElement);
1822
+ return { container: this.container, overlay };
1823
+ }
1824
+ /**
1825
+ * Start the game
1826
+ * @param options Game options
1827
+ * @returns The game instance
1828
+ */
1829
+ startGame(options) {
1830
+ if (!this.container) {
1831
+ return null;
1832
+ }
1833
+ const mergedConfig = {
1834
+ container: this.container,
1835
+ theme: options.theme || 'default',
1836
+ onScore: options.onScore
1837
+ };
1838
+ if (options.width != null)
1839
+ mergedConfig.width = options.width;
1840
+ if (options.height != null)
1841
+ mergedConfig.height = options.height;
1842
+ if (this.lastBackendConfig) {
1843
+ Object.assign(mergedConfig, this.lastBackendConfig);
1844
+ }
1845
+ this.gameInstance = loadGame(options.game || 'tetris', mergedConfig);
1846
+ if (this.gameInstance && typeof this.gameInstance.start === 'function') {
1847
+ this.gameInstance.start();
1848
+ }
1849
+ this.isPlaying = true;
1850
+ if (options.onStart && typeof options.onStart === 'function') {
1851
+ options.onStart();
1852
+ }
1853
+ return this.gameInstance;
1854
+ }
1855
+ async playAdInContainer(container, config) {
1856
+ if (config.sessionId === null) {
1857
+ return;
1858
+ }
1859
+ const adPlayer = new AdPlayer(container);
1860
+ const adStartTime = Date.now();
1861
+ try {
1862
+ await adPlayer.playAd({
1863
+ adTagUrl: config.adTagUrl,
1864
+ maxDuration: config.maxDuration,
1865
+ onAdStarted: () => {
1866
+ this.trackAdEvent(config.sessionId, 'ad_started');
1867
+ },
1868
+ onAdComplete: () => {
1869
+ const duration = (Date.now() - adStartTime) / 1000;
1870
+ this.trackAdEvent(config.sessionId, 'ad_completed', { duration });
1871
+ },
1872
+ onAdSkipped: () => {
1873
+ const duration = (Date.now() - adStartTime) / 1000;
1874
+ this.trackAdEvent(config.sessionId, 'ad_skipped', { duration });
1875
+ },
1876
+ onAdError: (error) => {
1877
+ this.trackAdEvent(config.sessionId, 'ad_error', { error: String(error) });
1878
+ }
1879
+ });
1880
+ }
1881
+ catch (error) {
1882
+ console.warn('Ad loading failed, continuing:', error);
1883
+ }
1884
+ finally {
1885
+ adPlayer.destroy();
1886
+ }
1887
+ }
1888
+ async trackAdEvent(sessionId, event, data) {
1889
+ try {
1890
+ await fetch(`${this.baseUrl}/sessions/${sessionId}/ad-event`, {
1891
+ method: 'POST',
1892
+ headers: {
1893
+ 'Content-Type': 'application/json',
1894
+ 'x-api-key': this.apiKey
1895
+ },
1896
+ body: JSON.stringify({
1897
+ event,
1898
+ timestamp: Date.now(),
1899
+ ...data
1900
+ })
1901
+ });
1902
+ }
1903
+ catch (error) {
1904
+ console.warn('Failed to track ad event:', error);
1905
+ }
1906
+ }
1907
+ cleanup(overlay, gameInstance) {
1908
+ if (overlay && overlay.parentNode) {
1909
+ overlay.parentNode.removeChild(overlay);
1910
+ }
1911
+ this.container = null;
1912
+ this.gameInstance = null;
1913
+ this.isPlaying = false;
1914
+ }
1915
+ /**
1916
+ * End the game and send session complete (overlay removal is done in cleanup)
1917
+ */
1918
+ endGame(sessionId) {
1919
+ var _a, _b;
1920
+ if (!this.isPlaying && !this.gameInstance) {
1921
+ return;
1922
+ }
1923
+ const score = this.gameInstance && typeof this.gameInstance.getScore === 'function'
1924
+ ? this.gameInstance.getScore()
1925
+ : ((_b = (_a = this.gameInstance) === null || _a === void 0 ? void 0 : _a.score) !== null && _b !== void 0 ? _b : 0);
1926
+ if (this.gameInstance && typeof this.gameInstance.destroy === 'function') {
1927
+ this.gameInstance.destroy();
1928
+ }
1929
+ this.isPlaying = false;
1930
+ this.gameInstance = null;
1931
+ this.container = null;
1932
+ const id = sessionId !== undefined ? sessionId : this.sessionId;
1933
+ if (id) {
1934
+ this.completeSession(score, id);
1935
+ }
1936
+ this.sessionId = null;
1937
+ }
1938
+ async completeSession(score, sessionId) {
1939
+ try {
1940
+ await fetch(`${this.baseUrl}/sessions/${sessionId}/complete`, {
1941
+ method: 'POST',
1942
+ headers: {
1943
+ 'Content-Type': 'application/json',
1944
+ 'x-api-key': this.apiKey
1945
+ },
1946
+ body: JSON.stringify({ duration: 0, score })
1947
+ });
1948
+ }
1949
+ catch (error) {
1950
+ console.error('Error completing game session:', error);
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ export { AdPlayer, WaitlessAPI, WaitlessAPI as default };
1956
+ //# sourceMappingURL=index.esm.js.map