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.
- package/dist/ads/AdPlayer.d.ts +62 -0
- package/dist/core.d.ts +132 -0
- package/dist/games/base.d.ts +54 -0
- package/dist/games/breakout.d.ts +45 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/snake.d.ts +40 -0
- package/dist/games/tetris.d.ts +94 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +1956 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/index.d.ts +30 -0
- package/package.json +42 -0
|
@@ -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
|