loading-games 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/games/2048.js +2 -0
- package/dist/games/2048.js.map +1 -0
- package/dist/games/asteroids.js +2 -0
- package/dist/games/asteroids.js.map +1 -0
- package/dist/games/brick-breaker.js +2 -0
- package/dist/games/brick-breaker.js.map +1 -0
- package/dist/games/flappy.js +2 -0
- package/dist/games/flappy.js.map +1 -0
- package/dist/games/memory-cards.js +2 -0
- package/dist/games/memory-cards.js.map +1 -0
- package/dist/games/snake.js +2 -0
- package/dist/games/snake.js.map +1 -0
- package/dist/games/whack-a-mole.js +2 -0
- package/dist/games/whack-a-mole.js.map +1 -0
- package/dist/games/wordle-lite.js +2 -0
- package/dist/games/wordle-lite.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +403 -0
- package/dist/index.d.ts +403 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +33 -0
- package/dist/react/index.d.ts +33 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.d.ts +88 -0
- package/dist/svelte/index.js +2 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +2 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +157 -0
- package/dist/vue/index.d.ts +157 -0
- package/dist/vue/index.js +2 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +105 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loading-games — TypeScript Types
|
|
3
|
+
*
|
|
4
|
+
* This file is the single source of truth for all types.
|
|
5
|
+
* Re-export from here in all packages.
|
|
6
|
+
*/
|
|
7
|
+
/** All built-in game identifiers. 'random' selects a different game each time. */
|
|
8
|
+
type GameName = 'snake' | 'brick-breaker' | 'flappy' | '2048' | 'wordle-lite' | 'asteroids' | 'memory-cards' | 'whack-a-mole' | 'random';
|
|
9
|
+
/** Size presets. 'full' creates a full-viewport overlay. */
|
|
10
|
+
type GameSize = 'sm' | 'md' | 'lg' | 'full';
|
|
11
|
+
/** Animation played when loading completes and the game exits. */
|
|
12
|
+
type ExitAnimation = 'fade' | 'slide' | 'none';
|
|
13
|
+
/**
|
|
14
|
+
* Theme color tokens.
|
|
15
|
+
* All fields optional — unset values fall back to CSS variables or system defaults.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* { primary: '#6366F1', background: '#0F0F0F' }
|
|
19
|
+
*/
|
|
20
|
+
interface ThemeObject {
|
|
21
|
+
/** Main accent color — buttons, score, active game elements */
|
|
22
|
+
primary?: string;
|
|
23
|
+
/** Canvas / component background */
|
|
24
|
+
background?: string;
|
|
25
|
+
/** Card and panel surfaces */
|
|
26
|
+
surface?: string;
|
|
27
|
+
/** Primary text (labels, scores, UI) */
|
|
28
|
+
text?: string;
|
|
29
|
+
/** Secondary highlight / accent */
|
|
30
|
+
accent?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Fired on every score change via onScore callback. */
|
|
33
|
+
interface Score {
|
|
34
|
+
game: Exclude<GameName, 'random'>;
|
|
35
|
+
/** Current score in this session */
|
|
36
|
+
current: number;
|
|
37
|
+
/** All-time personal best (from localStorage) */
|
|
38
|
+
personalBest: number;
|
|
39
|
+
/** true if current just exceeded the previous personal best */
|
|
40
|
+
isNewRecord: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Fired when a game round ends (not when loading ends). */
|
|
43
|
+
interface GameResult {
|
|
44
|
+
game: Exclude<GameName, 'random'>;
|
|
45
|
+
finalScore: number;
|
|
46
|
+
/** How long the game was active in milliseconds */
|
|
47
|
+
duration: number;
|
|
48
|
+
isNewRecord: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Complete configuration for a LoadingGame instance.
|
|
52
|
+
* All fields optional — sensible defaults apply.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* {
|
|
56
|
+
* game: 'snake',
|
|
57
|
+
* active: isLoading,
|
|
58
|
+
* theme: { primary: '#6366F1', background: '#0F0F0F' },
|
|
59
|
+
* onScore: (s) => console.log(s),
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
interface LoadingGameOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Which game to render.
|
|
65
|
+
* 'random' selects a different game each activation.
|
|
66
|
+
* @default 'random'
|
|
67
|
+
*/
|
|
68
|
+
game?: GameName;
|
|
69
|
+
/**
|
|
70
|
+
* Controls whether the game is shown.
|
|
71
|
+
* Set to true when loading begins, false when it ends.
|
|
72
|
+
* @default false
|
|
73
|
+
*/
|
|
74
|
+
active?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Theme color overrides.
|
|
77
|
+
* Merged with CSS variables and system color scheme.
|
|
78
|
+
*/
|
|
79
|
+
theme?: ThemeObject;
|
|
80
|
+
/**
|
|
81
|
+
* Container size preset.
|
|
82
|
+
* 'full' renders a full-viewport overlay.
|
|
83
|
+
* @default 'md'
|
|
84
|
+
*/
|
|
85
|
+
size?: GameSize;
|
|
86
|
+
/**
|
|
87
|
+
* Milliseconds to wait before showing the game.
|
|
88
|
+
* Prevents a jarring flash for fast loads.
|
|
89
|
+
* If loading completes before delay, game never renders.
|
|
90
|
+
* @default 800
|
|
91
|
+
*/
|
|
92
|
+
delay?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Minimum milliseconds to show the game once it appears.
|
|
95
|
+
* Prevents a confusing half-second flash.
|
|
96
|
+
* @default 0
|
|
97
|
+
*/
|
|
98
|
+
minDisplay?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Animation when the game exits after loading completes.
|
|
101
|
+
* @default 'fade'
|
|
102
|
+
*/
|
|
103
|
+
exitAnimation?: ExitAnimation;
|
|
104
|
+
/**
|
|
105
|
+
* Persist personal bests in localStorage.
|
|
106
|
+
* @default true
|
|
107
|
+
*/
|
|
108
|
+
saveScores?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Namespace for score storage.
|
|
111
|
+
* Allows multiple instances to maintain separate leaderboards.
|
|
112
|
+
* @default undefined (global namespace)
|
|
113
|
+
*/
|
|
114
|
+
namespace?: string;
|
|
115
|
+
/** Fires every time the score changes. */
|
|
116
|
+
onScore?: (score: Score) => void;
|
|
117
|
+
/** Fires when a game round ends (game over, not loading end). */
|
|
118
|
+
onGameOver?: (result: GameResult) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Fires when loading completes and the game has fully exited.
|
|
121
|
+
* Use this to trigger post-load UI updates.
|
|
122
|
+
*/
|
|
123
|
+
onComplete?: () => void;
|
|
124
|
+
/**
|
|
125
|
+
* Fires when loading fails.
|
|
126
|
+
* The game exits immediately, without animation.
|
|
127
|
+
*/
|
|
128
|
+
onError?: (err: Error) => void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Interface for custom game plugins.
|
|
132
|
+
* Implement this to register your own game via registerGame().
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* class MyGame implements GamePlugin {
|
|
136
|
+
* name = 'my-game'
|
|
137
|
+
* init(canvas, theme) { ... }
|
|
138
|
+
* start() { ... }
|
|
139
|
+
* pause() { ... }
|
|
140
|
+
* resume() { ... }
|
|
141
|
+
* destroy() { ... }
|
|
142
|
+
* }
|
|
143
|
+
*/
|
|
144
|
+
interface GamePlugin {
|
|
145
|
+
/** Unique identifier used in the game prop */
|
|
146
|
+
readonly name: string;
|
|
147
|
+
/** Approximate gzipped bundle size in bytes (informational) */
|
|
148
|
+
readonly bundleSize?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Initialize the game. Set up canvas, load assets.
|
|
151
|
+
* Called once per activation. May be async.
|
|
152
|
+
*/
|
|
153
|
+
init(canvas: HTMLCanvasElement, theme: ThemeObject): void | Promise<void>;
|
|
154
|
+
/** Begin the game loop. Called after init completes. */
|
|
155
|
+
start(): void;
|
|
156
|
+
/** Pause the game loop. Called on tab blur and when loading completes. */
|
|
157
|
+
pause(): void;
|
|
158
|
+
/** Resume from pause. Called on tab focus if loading is still active. */
|
|
159
|
+
resume(): void;
|
|
160
|
+
/**
|
|
161
|
+
* Fully clean up. Cancel all requestAnimationFrame callbacks,
|
|
162
|
+
* remove all event listeners, release resources.
|
|
163
|
+
* Called when active becomes false or component unmounts.
|
|
164
|
+
*/
|
|
165
|
+
destroy(): void;
|
|
166
|
+
/** Return the current score. Called by the score persistence system. */
|
|
167
|
+
getScore?(): number;
|
|
168
|
+
}
|
|
169
|
+
/** Internal game lifecycle states */
|
|
170
|
+
type GameState = 'idle' | 'delaying' | 'loading-game' | 'playing' | 'min-display' | 'exiting' | 'complete';
|
|
171
|
+
/** Resolved theme — all fields required after theme resolution */
|
|
172
|
+
interface ResolvedTheme {
|
|
173
|
+
primary: string;
|
|
174
|
+
background: string;
|
|
175
|
+
surface: string;
|
|
176
|
+
text: string;
|
|
177
|
+
accent: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* <loading-game> Web Component
|
|
182
|
+
*
|
|
183
|
+
* Registers a custom element that wraps the GameController.
|
|
184
|
+
* Works in any framework or plain HTML.
|
|
185
|
+
*
|
|
186
|
+
* @example HTML
|
|
187
|
+
* <loading-game game="snake" active="true" theme-primary="#6366F1"></loading-game>
|
|
188
|
+
*
|
|
189
|
+
* @example Imperative JS
|
|
190
|
+
* const el = document.querySelector('loading-game')
|
|
191
|
+
* el.start()
|
|
192
|
+
* el.stop()
|
|
193
|
+
* el.setTheme({ primary: '#E94560' })
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
declare class LoadingGameElement extends HTMLElement {
|
|
197
|
+
static observedAttributes: string[];
|
|
198
|
+
private controller;
|
|
199
|
+
private _theme;
|
|
200
|
+
connectedCallback(): void;
|
|
201
|
+
disconnectedCallback(): void;
|
|
202
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
203
|
+
set theme(value: ThemeObject);
|
|
204
|
+
get theme(): ThemeObject;
|
|
205
|
+
/** Start the game manually. Equivalent to setting active="true". */
|
|
206
|
+
start(): void;
|
|
207
|
+
/** Stop the game manually. Equivalent to setting active="false". */
|
|
208
|
+
stop(): void;
|
|
209
|
+
/** Update the theme dynamically. */
|
|
210
|
+
setTheme(theme: ThemeObject): void;
|
|
211
|
+
private buildOptions;
|
|
212
|
+
private getExitAnimation;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* GameController — Orchestrates the entire game lifecycle.
|
|
217
|
+
*
|
|
218
|
+
* Manages:
|
|
219
|
+
* - Delay timer (don't flash for fast loads)
|
|
220
|
+
* - Dynamic game chunk loading
|
|
221
|
+
* - Canvas setup and teardown
|
|
222
|
+
* - Entry/exit animations
|
|
223
|
+
* - Score callbacks
|
|
224
|
+
* - Visibility API pause/resume
|
|
225
|
+
* - prefers-reduced-motion fallback
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
interface ControllerCallbacks {
|
|
229
|
+
onScore: (score: Score) => void;
|
|
230
|
+
onGameOver: (result: GameResult) => void;
|
|
231
|
+
onComplete: () => void;
|
|
232
|
+
onError: (err: Error) => void;
|
|
233
|
+
}
|
|
234
|
+
declare class GameController {
|
|
235
|
+
hasEverActivated: boolean;
|
|
236
|
+
private host;
|
|
237
|
+
private callbacks;
|
|
238
|
+
private options;
|
|
239
|
+
private state;
|
|
240
|
+
private canvas;
|
|
241
|
+
private overlay;
|
|
242
|
+
private skipLink;
|
|
243
|
+
private currentGame;
|
|
244
|
+
private delayController;
|
|
245
|
+
private _gameStartTime;
|
|
246
|
+
private _currentScore;
|
|
247
|
+
private boundVisibilityChange;
|
|
248
|
+
constructor(host: HTMLElement, callbacks: ControllerCallbacks);
|
|
249
|
+
activate(options: LoadingGameOptions): void;
|
|
250
|
+
deactivate(opts: {
|
|
251
|
+
animation: ExitAnimation;
|
|
252
|
+
}): void;
|
|
253
|
+
/** Immediately tear down on error — no animation, no delay. */
|
|
254
|
+
errorDeactivate(): void;
|
|
255
|
+
updateOptions(options: LoadingGameOptions): void;
|
|
256
|
+
destroy(): void;
|
|
257
|
+
private showGame;
|
|
258
|
+
private exitGame;
|
|
259
|
+
private setupDOM;
|
|
260
|
+
private teardownDOM;
|
|
261
|
+
private animateExit;
|
|
262
|
+
private showToast;
|
|
263
|
+
private showCompletionOverlay;
|
|
264
|
+
private showReducedMotionFallback;
|
|
265
|
+
private handleGameOverEvent;
|
|
266
|
+
private handleScoreUpdate;
|
|
267
|
+
private showNewRecordBadge;
|
|
268
|
+
private loadGameChunk;
|
|
269
|
+
private resolveGameName;
|
|
270
|
+
private setState;
|
|
271
|
+
private handleVisibilityChange;
|
|
272
|
+
private sleep;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Theme resolution — 3-level priority:
|
|
277
|
+
* 1. Explicit theme prop (highest)
|
|
278
|
+
* 2. CSS variables on :root (--lg-primary, etc.)
|
|
279
|
+
* 3. prefers-color-scheme detection (lowest)
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Resolve the final theme from all sources.
|
|
284
|
+
* Priority: explicit prop > CSS variables > system defaults.
|
|
285
|
+
*/
|
|
286
|
+
declare function resolveTheme(theme?: ThemeObject): ResolvedTheme;
|
|
287
|
+
/**
|
|
288
|
+
* Apply resolved theme as CSS variables to a container element.
|
|
289
|
+
* This allows games to reference CSS variables for dynamic theming.
|
|
290
|
+
*/
|
|
291
|
+
declare function applyThemeToElement(element: HTMLElement, theme: ResolvedTheme): void;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Score persistence layer.
|
|
295
|
+
* Stores personal bests per game in localStorage under the 'loading-games' namespace.
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
interface ScoreEntry {
|
|
299
|
+
personalBest: number;
|
|
300
|
+
lastPlayed: string;
|
|
301
|
+
totalGames: number;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get the personal best for a game.
|
|
305
|
+
* Returns 0 if no score has been recorded.
|
|
306
|
+
*/
|
|
307
|
+
declare function getPersonalBest(game: Exclude<GameName, 'random'>, namespace?: string): number;
|
|
308
|
+
/**
|
|
309
|
+
* Save a new score. Returns true if this is a new personal best.
|
|
310
|
+
*/
|
|
311
|
+
declare function saveScore(game: Exclude<GameName, 'random'>, score: number, namespace?: string): boolean;
|
|
312
|
+
/**
|
|
313
|
+
* Get all scores, optionally filtered by namespace.
|
|
314
|
+
*/
|
|
315
|
+
declare function getAllScores(namespace?: string): Record<string, ScoreEntry>;
|
|
316
|
+
/**
|
|
317
|
+
* Clear all scores, optionally scoped to a namespace.
|
|
318
|
+
*/
|
|
319
|
+
declare function clearScores(namespace?: string): void;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Intelligent delay system.
|
|
323
|
+
*
|
|
324
|
+
* Rules:
|
|
325
|
+
* - Don't show the game if loading completes before `delay` ms (prevents flash)
|
|
326
|
+
* - Once shown, keep the game visible for at least `minDisplay` ms
|
|
327
|
+
* - When loading completes, transition to the exit animation
|
|
328
|
+
*/
|
|
329
|
+
interface DelayOptions {
|
|
330
|
+
/** Milliseconds to wait before showing the game. @default 800 */
|
|
331
|
+
delay: number;
|
|
332
|
+
/** Minimum milliseconds to show the game once visible. @default 0 */
|
|
333
|
+
minDisplay: number;
|
|
334
|
+
/** Called when the game should become visible */
|
|
335
|
+
onShow: () => void;
|
|
336
|
+
/** Called when the game should start exiting */
|
|
337
|
+
onExit: () => void;
|
|
338
|
+
}
|
|
339
|
+
declare class DelayController {
|
|
340
|
+
private options;
|
|
341
|
+
private delayTimer;
|
|
342
|
+
private minDisplayTimer;
|
|
343
|
+
private showTime;
|
|
344
|
+
private loadingCompletedWhileDelaying;
|
|
345
|
+
private isVisible;
|
|
346
|
+
private destroyed;
|
|
347
|
+
constructor(options: DelayOptions);
|
|
348
|
+
/**
|
|
349
|
+
* Called when loading starts (active = true).
|
|
350
|
+
* Starts the delay timer.
|
|
351
|
+
*/
|
|
352
|
+
start(): void;
|
|
353
|
+
/**
|
|
354
|
+
* Called when loading ends (active = false).
|
|
355
|
+
* Either cancels the delay (if still waiting) or triggers exit with minDisplay logic.
|
|
356
|
+
*/
|
|
357
|
+
end(): void;
|
|
358
|
+
private triggerExit;
|
|
359
|
+
/** Clean up all timers. */
|
|
360
|
+
destroy(): void;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Virtual D-pad — Reusable touch controller overlay.
|
|
365
|
+
*
|
|
366
|
+
* Renders 4 directional arrows (+ optional center/fire button) as an
|
|
367
|
+
* overlay positioned at the bottom-center of a parent element.
|
|
368
|
+
* Only renders when touch is available ('ontouchstart' in window).
|
|
369
|
+
*
|
|
370
|
+
* Each button meets the 44×44px minimum touch target requirement.
|
|
371
|
+
* Semi-transparent, themed with the provided primary color.
|
|
372
|
+
*/
|
|
373
|
+
type DpadDirection = 'up' | 'down' | 'left' | 'right';
|
|
374
|
+
interface DpadOptions {
|
|
375
|
+
/** Parent element to attach the overlay to. Must have position: relative/absolute/fixed. */
|
|
376
|
+
parent: HTMLElement;
|
|
377
|
+
/** Primary theme color for button backgrounds. */
|
|
378
|
+
primaryColor: string;
|
|
379
|
+
/** Text/icon color. */
|
|
380
|
+
textColor: string;
|
|
381
|
+
/** Called when a direction button is pressed. */
|
|
382
|
+
onDirection: (dir: DpadDirection) => void;
|
|
383
|
+
/** If true, render a center "fire" button. */
|
|
384
|
+
showFire?: boolean;
|
|
385
|
+
/** Called when the fire button is pressed. */
|
|
386
|
+
onFire?: () => void;
|
|
387
|
+
}
|
|
388
|
+
declare class Dpad {
|
|
389
|
+
private options;
|
|
390
|
+
private overlay;
|
|
391
|
+
private boundClick;
|
|
392
|
+
private boundTouch;
|
|
393
|
+
constructor(options: DpadOptions);
|
|
394
|
+
/** Create and mount the D-pad. No-op if touch is unavailable. */
|
|
395
|
+
mount(): void;
|
|
396
|
+
/** Remove the D-pad from the DOM and clean up listeners. */
|
|
397
|
+
destroy(): void;
|
|
398
|
+
private buttonStyle;
|
|
399
|
+
private handleClick;
|
|
400
|
+
private handleTouch;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export { DelayController, Dpad, type DpadDirection, type DpadOptions, type ExitAnimation, GameController, type GameName, type GamePlugin, type GameResult, type GameSize, type GameState, LoadingGameElement, type LoadingGameOptions, type ResolvedTheme, type Score, type ThemeObject, applyThemeToElement, clearScores, getAllScores, getPersonalBest, resolveTheme, saveScore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Te=Object.defineProperty;var p=(a,e)=>()=>(a&&(e=a(a=0)),e);var g=(a,e)=>{for(var t in e)Te(a,t,{get:e[t],enumerable:true});};function Se(a){return typeof window>"u"?null:getComputedStyle(document.documentElement).getPropertyValue(a).trim()||null}function Ee(){if(typeof window>"u")return I;try{return window.matchMedia("(prefers-color-scheme: dark)").matches?I:we}catch{return I}}function d(a){let t={...Ee()};for(let[s,i]of Object.entries(V)){let n=Se(i);n&&(t[s]=n);}if(a)for(let[s,i]of Object.entries(a))i&&(t[s]=i);return t}function G(a,e){for(let[t,s]of Object.entries(V))a.style.setProperty(s,e[t]);}var I,we,V,m=p(()=>{I={primary:"#6366F1",background:"#0F0F0F",surface:"#1A1A2E",text:"#F8F8F8",accent:"#E94560"},we={primary:"#4F46E5",background:"#FFFFFF",surface:"#F3F4F6",text:"#111827",accent:"#E94560"},V={primary:"--lg-primary",background:"--lg-background",surface:"--lg-surface",text:"--lg-text",accent:"--lg-accent"};});var Ae,T,F=p(()=>{Ae={up:"\u25B2",down:"\u25BC",left:"\u25C0",right:"\u25B6"},T=class{constructor(e){this.options=e;this.boundClick=this.handleClick.bind(this),this.boundTouch=this.handleTouch.bind(this);}overlay=null;boundClick;boundTouch;mount(){if(!("ontouchstart"in window)||this.overlay)return;let{parent:e,primaryColor:t,textColor:s,showFire:i}=this.options;getComputedStyle(e).position==="static"&&(e.style.position="relative");let r=document.createElement("div");r.setAttribute("data-lg-dpad",""),r.style.cssText=["position:absolute","bottom:12px","left:50%","transform:translateX(-50%)","display:grid","grid-template-columns:44px 44px 44px","grid-template-rows:44px 44px 44px","gap:2px","pointer-events:none","z-index:10"].join(";");let l=[null,"up",null,"left",i?"fire":null,"right",null,"down",null];for(let o of l){let h=document.createElement("button");o&&o!=="fire"?(h.textContent=Ae[o],h.setAttribute("aria-label",o),h.dataset.dir=o,h.style.cssText=this.buttonStyle(t,s)):o==="fire"?(h.textContent="\u25CF",h.setAttribute("aria-label","fire"),h.dataset.action="fire",h.style.cssText=this.buttonStyle(t,s)):h.style.cssText="visibility:hidden;",r.appendChild(h);}r.addEventListener("click",this.boundClick),r.addEventListener("touchstart",this.boundTouch,{passive:true}),e.appendChild(r),this.overlay=r;}destroy(){this.overlay&&(this.overlay.removeEventListener("click",this.boundClick),this.overlay.removeEventListener("touchstart",this.boundTouch),this.overlay.remove(),this.overlay=null);}buttonStyle(e,t){return ["pointer-events:auto","width:44px","height:44px","border:none","border-radius:8px",`background:${e}55`,`color:${t}`,"font-size:16px","display:flex","align-items:center","justify-content:center","touch-action:manipulation","cursor:pointer","-webkit-tap-highlight-color:transparent"].join(";")}handleClick(e){let t=e.target.closest("[data-dir],[data-action]");t&&(t.dataset.dir?this.options.onDirection(t.dataset.dir):t.dataset.action==="fire"&&this.options.onFire?.());}handleTouch(e){let t=e.target.closest("[data-dir],[data-action]");t&&(t.dataset.dir?this.options.onDirection(t.dataset.dir):t.dataset.action==="fire"&&this.options.onFire?.());}};});var q={};g(q,{SnakeGame:()=>j});var y,Z,De,Pe,j,J=p(()=>{m();F();y=20,Z=150,De=5,Pe=30,j=class{name="snake";bundleSize=4e3;canvas;ctx;theme;snake=[];food={x:0,y:0};direction="right";nextDirection="right";score=0;tickMs=Z;lastTick=0;animFrameId=null;running=false;cellSize=0;boundKeyDown;boundTouchStart;boundTouchEnd;touchStartX=0;touchStartY=0;isTouchDevice=false;dpad=null;onScoreCallback;onGameOverCallback;constructor(e,t){this.onScoreCallback=e,this.onGameOverCallback=t,this.boundKeyDown=this.handleKeyDown.bind(this),this.boundTouchStart=this.handleTouchStart.bind(this),this.boundTouchEnd=this.handleTouchEnd.bind(this);}init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t),e.setAttribute("aria-label","Snake game \u2014 loading in background"),e.setAttribute("role","img");let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,this.cellSize=Math.floor(i.width/y),this.isTouchDevice="ontouchstart"in window,this.reset();}reset(){let e=Math.floor(y/2);this.snake=[{x:e,y:e},{x:e-1,y:e},{x:e-2,y:e}],this.direction="right",this.nextDirection="right",this.score=0,this.tickMs=Z,this.placeFood();}placeFood(){let e=new Set(this.snake.map(s=>`${s.x},${s.y}`)),t;do t={x:Math.floor(Math.random()*y),y:Math.floor(Math.random()*y)};while(e.has(`${t.x},${t.y}`));this.food=t;}start(){this.running=true,this.lastTick=performance.now(),this.loop(performance.now()),this.attachEventListeners(),this.isTouchDevice&&this.mountDpad();}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.lastTick=performance.now(),this.loop(performance.now()));}destroy(){this.pause(),this.removeEventListeners(),this.dpad?.destroy(),this.dpad=null;}getScore(){return this.score}loop(e){this.running&&(this.animFrameId=requestAnimationFrame(t=>this.loop(t)),e-this.lastTick>=this.tickMs&&(this.lastTick=e,this.tick()),this.render());}tick(){this.direction=this.nextDirection;let e=this.snake[0],t={x:(e.x+(this.direction==="right"?1:this.direction==="left"?-1:0)+y)%y,y:(e.y+(this.direction==="down"?1:this.direction==="up"?-1:0)+y)%y};if(this.snake.some(s=>s.x===t.x&&s.y===t.y)){this.onGameOverCallback?.(),this.reset();return}this.snake.unshift(t),t.x===this.food.x&&t.y===this.food.y?(this.score+=10,this.onScoreCallback?.(this.score),this.score%50===0&&(this.tickMs=Math.max(60,this.tickMs-De)),this.placeFood()):this.snake.pop();}render(){let{ctx:e,cellSize:t,theme:s}=this,i=this.canvas.getBoundingClientRect(),n=i.width,r=i.height;e.fillStyle=s.background,e.fillRect(0,0,n,r),e.strokeStyle=s.surface,e.lineWidth=.5,e.globalAlpha=.3;for(let h=0;h<=n;h+=t)e.beginPath(),e.moveTo(h,0),e.lineTo(h,r),e.stroke();for(let h=0;h<=r;h+=t)e.beginPath(),e.moveTo(0,h),e.lineTo(n,h),e.stroke();e.globalAlpha=1;let l=this.food.x*t+t/2,o=this.food.y*t+t/2;e.fillStyle=s.accent,e.beginPath(),e.arc(l,o,t*.4,0,Math.PI*2),e.fill(),this.snake.forEach((h,v)=>{let c=h.x*t,f=h.y*t,k=1;if(v===0)e.fillStyle=s.primary;else {let xe=1-v/this.snake.length*.4;e.globalAlpha=xe,e.fillStyle=s.primary;}let ve=3,fe=c+k,ge=f+k,ye=t-k*2,be=t-k*2;e.beginPath(),e.roundRect(fe,ge,ye,be,ve),e.fill(),e.globalAlpha=1;}),e.fillStyle=s.text,e.font=`bold ${Math.round(t*.7)}px monospace`,e.textAlign="right",e.fillText(`${this.score}`,n-8,t);}attachEventListeners(){document.addEventListener("keydown",this.boundKeyDown),this.canvas.addEventListener("touchstart",this.boundTouchStart,{passive:true}),this.canvas.addEventListener("touchend",this.boundTouchEnd,{passive:true});}removeEventListeners(){document.removeEventListener("keydown",this.boundKeyDown),this.canvas.removeEventListener("touchstart",this.boundTouchStart),this.canvas.removeEventListener("touchend",this.boundTouchEnd);}handleKeyDown(e){let s={ArrowUp:"up",ArrowDown:"down",ArrowLeft:"left",ArrowRight:"right"}[e.key];if(!s)return;({up:"down",down:"up",left:"right",right:"left"})[s]!==this.direction&&(this.nextDirection=s,e.preventDefault());}handleTouchStart(e){let t=e.touches[0];t&&(this.touchStartX=t.clientX,this.touchStartY=t.clientY);}handleTouchEnd(e){let t=e.changedTouches[0];if(!t)return;let s=t.clientX-this.touchStartX,i=t.clientY-this.touchStartY,n=Math.abs(s),r=Math.abs(i);if(Math.max(n,r)<Pe)return;let l;n>r?l=s>0?"right":"left":l=i>0?"down":"up",{up:"down",down:"up",left:"right",right:"left"}[l]!==this.direction&&(this.nextDirection=l);}mountDpad(){let e=this.canvas.parentElement;e&&(this.dpad=new T({parent:e,primaryColor:this.theme.primary,textColor:this.theme.text,onDirection:t=>{({up:"down",down:"up",left:"right",right:"left"})[t]!==this.direction&&(this.nextDirection=t);}}),this.dpad.mount());}};});var Q={};g(Q,{BrickBreakerGame:()=>B});var B,ee=p(()=>{m();B=class{constructor(e,t){this.onScore=e;this._onGameOver=t;}name="brick-breaker";bundleSize=6e3;canvas;ctx;theme;animFrameId=null;running=false;score=0;init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t);let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`;}start(){this.running=true,this.render();}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.render());}destroy(){this.pause();}getScore(){return this.score}render(){let e=this.canvas.getBoundingClientRect(),t=e.width,s=e.height,{ctx:i,theme:n}=this;i.fillStyle=n.background,i.fillRect(0,0,t,s),i.fillStyle=n.text,i.font="bold 18px system-ui",i.textAlign="center",i.textBaseline="middle",i.fillText("Brick Breaker",t/2,s/2-16),i.font="14px system-ui",i.globalAlpha=.6,i.fillText("Coming soon!",t/2,s/2+16),i.globalAlpha=1;}};});var ie={};g(ie,{FlappyGame:()=>_});var Me,te,b,H,Re,D,u,_,se=p(()=>{m();Me=.4,te=-7,b=52,H=150,Re=2.2,D=80,u=14,_=class{name="flappy";bundleSize=5e3;canvas;ctx;theme;W=0;H=0;birdY=0;birdVY=0;pipes=[];score=0;personalBest=0;animFrameId=null;running=false;dead=false;frameCount=0;lastTime=0;boundJump;boundKey;boundTouch;onScore;onGameOver;constructor(e,t){this.onScore=e,this.onGameOver=t,this.boundJump=this.jump.bind(this),this.boundKey=s=>{s.code==="Space"&&(s.preventDefault(),this.jump());},this.boundTouch=this.jump.bind(this);}init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t);let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,this.W=i.width,this.H=i.height,e.setAttribute("aria-label","Flappy Bird game \u2014 loading in background"),e.setAttribute("role","img"),this.reset();}reset(){this.birdY=this.H/2,this.birdVY=0,this.pipes=[],this.score=0,this.dead=false,this.frameCount=0,this.spawnPipe(this.W+100);}spawnPipe(e){let s=this.H-H-60,i=60+Math.random()*(s-60);this.pipes.push({x:e??this.W+50,topHeight:i,passed:false});}start(){this.running=true,this.lastTime=performance.now(),this.loop(performance.now()),this.canvas.addEventListener("click",this.boundJump),document.addEventListener("keydown",this.boundKey),this.canvas.addEventListener("touchstart",this.boundTouch,{passive:true});}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.lastTime=performance.now(),this.loop(performance.now()));}destroy(){this.pause(),this.canvas.removeEventListener("click",this.boundJump),document.removeEventListener("keydown",this.boundKey),this.canvas.removeEventListener("touchstart",this.boundTouch);}getScore(){return this.score}jump(){if(this.dead){this.reset();return}this.birdVY=te;}loop(e){if(!this.running)return;this.animFrameId=requestAnimationFrame(s=>this.loop(s));let t=Math.min((e-this.lastTime)/16.67,3);this.lastTime=e,this.dead||this.update(t),this.render();}update(e){if(this.frameCount++,this.birdVY+=Me*e,this.birdY+=this.birdVY*e,this.birdY+u>=this.H||this.birdY-u<=0){this.die();return}this.frameCount%90===0&&this.spawnPipe();for(let t of this.pipes){t.x-=Re*e,!t.passed&&t.x+b<D&&(t.passed=true,this.score++,this.score>this.personalBest&&(this.personalBest=this.score),this.onScore?.(this.score));let s=D+u>t.x&&D-u<t.x+b,i=this.birdY-u<t.topHeight||this.birdY+u>t.topHeight+H;if(s&&i){this.die();return}}this.pipes=this.pipes.filter(t=>t.x+b>-10);}die(){this.dead=true,this.birdVY=te*.5,this.onGameOver?.();}render(){let{ctx:e,W:t,H:s,theme:i}=this;e.fillStyle=i.background,e.fillRect(0,0,t,s);for(let n of this.pipes){e.fillStyle=i.primary,e.fillRect(n.x-4,n.topHeight-24,b+8,24),e.beginPath(),e.roundRect(n.x,0,b,n.topHeight-6,[0,0,6,6]),e.fill();let r=n.topHeight+H;e.fillRect(n.x-4,r,b+8,24),e.beginPath(),e.roundRect(n.x,r+6,b,s-r,[6,6,0,0]),e.fill();}e.save(),e.translate(D,this.birdY),e.rotate(Math.min(Math.max(this.birdVY*.05,-0.5),1)),this.dead&&(e.globalAlpha=.6),e.fillStyle="#FFD93D",e.beginPath(),e.arc(0,0,u,0,Math.PI*2),e.fill(),e.fillStyle="#fff",e.beginPath(),e.arc(5,-4,5,0,Math.PI*2),e.fill(),e.fillStyle="#333",e.beginPath(),e.arc(6,-4,2.5,0,Math.PI*2),e.fill(),e.fillStyle="#FF6B35",e.beginPath(),e.moveTo(u-2,0),e.lineTo(u+8,-3),e.lineTo(u+8,3),e.closePath(),e.fill(),e.restore(),e.fillStyle=i.text,e.font="bold 28px monospace",e.textAlign="center",e.fillText(String(this.score),t/2,44),this.dead&&(e.fillStyle="rgba(0,0,0,0.45)",e.fillRect(0,0,t,s),e.fillStyle=i.text,e.font="bold 20px system-ui",e.textAlign="center",e.fillText("Tap to retry",t/2,s/2),e.font="14px system-ui",e.globalAlpha=.7,e.fillText(`Score: ${this.score} Best: ${this.personalBest}`,t/2,s/2+28),e.globalAlpha=1);}};});var ne={};g(ne,{Game2048:()=>$});var $,oe=p(()=>{m();$=class{constructor(e,t){this._onScore=e;this._onGameOver=t;}name="2048";bundleSize=5e3;canvas;ctx;theme;animFrameId=null;running=false;score=0;init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t);let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`;}start(){this.running=true,this.render();}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.render());}destroy(){this.pause();}getScore(){return this.score}render(){let e=this.canvas.getBoundingClientRect(),t=e.width,s=e.height,{ctx:i,theme:n}=this;i.fillStyle=n.background,i.fillRect(0,0,t,s),i.fillStyle=n.text,i.font="bold 18px system-ui",i.textAlign="center",i.textBaseline="middle",i.fillText("2048",t/2,s/2-16),i.font="14px system-ui",i.globalAlpha=.6,i.fillText("Coming soon!",t/2,s/2+16),i.globalAlpha=1;}};});var re={};g(re,{WordleLiteGame:()=>N});var N,ae=p(()=>{m();N=class{constructor(e,t){this._onScore=e;this._onGameOver=t;}name="wordle-lite";bundleSize=7e3;canvas;ctx;theme;animFrameId=null;running=false;score=0;init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t);let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`;}start(){this.running=true,this.render();}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.render());}destroy(){this.pause();}getScore(){return this.score}render(){let e=this.canvas.getBoundingClientRect(),t=e.width,s=e.height,{ctx:i,theme:n}=this;i.fillStyle=n.background,i.fillRect(0,0,t,s),i.fillStyle=n.text,i.font="bold 18px system-ui",i.textAlign="center",i.textBaseline="middle",i.fillText("Wordle-lite",t/2,s/2-16),i.font="14px system-ui",i.globalAlpha=.6,i.fillText("Coming soon!",t/2,s/2+16),i.globalAlpha=1;}};});var le={};g(le,{AsteroidsGame:()=>W});var W,he=p(()=>{m();W=class{constructor(e,t){this._onScore=e;this._onGameOver=t;}name="asteroids";bundleSize=8e3;canvas;ctx;theme;animFrameId=null;running=false;score=0;init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t);let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`;}start(){this.running=true,this.render();}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.render());}destroy(){this.pause();}getScore(){return this.score}render(){let e=this.canvas.getBoundingClientRect(),t=e.width,s=e.height,{ctx:i,theme:n}=this;i.fillStyle=n.background,i.fillRect(0,0,t,s),i.fillStyle=n.text,i.font="bold 18px system-ui",i.textAlign="center",i.textBaseline="middle",i.fillText("Asteroids",t/2,s/2-16),i.font="14px system-ui",i.globalAlpha=.6,i.fillText("Coming soon!",t/2,s/2+16),i.globalAlpha=1;}};});var de={};g(de,{MemoryCardsGame:()=>Y});var P,M,K,Y,ce=p(()=>{m();P=["\u{1F3AE}","\u{1F680}","\u{1F3AF}","\u{1F3B2}","\u{1F31F}","\u{1F3B8}","\u{1F984}","\u{1F355}"],M=4,K=300,Y=class{name="memory-cards";canvas;ctx;theme;W=0;H=0;cards=[];flippedIdxs=[];locked=false;score=0;matchCount=0;startTime=0;selectedIdx=0;animFrameId=null;running=false;lastTime=0;boundClick;boundTouch;boundKeyDown;onScore;onGameOver;constructor(e,t){this.onScore=e,this.onGameOver=t,this.boundClick=this.onClick.bind(this),this.boundTouch=this.onTouch.bind(this),this.boundKeyDown=this.onKeyDown.bind(this);}init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t),e.setAttribute("aria-label","Memory Cards game \u2014 loading in background"),e.setAttribute("role","img"),e.setAttribute("tabindex","0");let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,this.W=i.width,this.H=i.height,this.reset();}reset(){let e=[...P,...P];for(let t=e.length-1;t>0;t--){let s=Math.floor(Math.random()*(t+1));[e[t],e[s]]=[e[s],e[t]];}this.cards=e.map((t,s)=>({id:s,emoji:t,flipped:false,matched:false,flipProgress:0,pulsePhase:0})),this.flippedIdxs=[],this.locked=false,this.score=0,this.matchCount=0,this.startTime=Date.now(),this.selectedIdx=0;}start(){this.running=true,this.lastTime=performance.now(),this.loop(performance.now()),this.canvas.addEventListener("click",this.boundClick),this.canvas.addEventListener("touchend",this.boundTouch,{passive:true}),document.addEventListener("keydown",this.boundKeyDown);}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.lastTime=performance.now(),this.loop(performance.now()));}destroy(){this.pause(),this.canvas.removeEventListener("click",this.boundClick),this.canvas.removeEventListener("touchend",this.boundTouch),document.removeEventListener("keydown",this.boundKeyDown);}getScore(){return this.score}bounds(e){let s=M,i=M,n=(this.W-8*(s+1))/s,r=(this.H-8*(i+1))/i;return {x:8+e%s*(n+8),y:8+Math.floor(e/s)*(r+8),w:n,h:r}}cardAt(e,t){for(let s=0;s<this.cards.length;s++){let i=this.bounds(s);if(e>=i.x&&e<=i.x+i.w&&t>=i.y&&t<=i.y+i.h)return s}return -1}onClick(e){let t=this.canvas.getBoundingClientRect();this.tryFlip(e.clientX-t.left,e.clientY-t.top);}onTouch(e){let t=e.changedTouches[0];if(!t)return;let s=this.canvas.getBoundingClientRect();this.tryFlip(t.clientX-s.left,t.clientY-s.top);}onKeyDown(e){let t=this.cards.length;switch(e.key){case "Tab":e.preventDefault(),this.selectedIdx=(this.selectedIdx+(e.shiftKey?t-1:1))%t;break;case "ArrowRight":e.preventDefault(),this.selectedIdx=(this.selectedIdx+1)%t;break;case "ArrowLeft":e.preventDefault(),this.selectedIdx=(this.selectedIdx+t-1)%t;break;case "ArrowDown":e.preventDefault(),this.selectedIdx=(this.selectedIdx+M)%t;break;case "ArrowUp":e.preventDefault(),this.selectedIdx=(this.selectedIdx+t-M)%t;break;case "Enter":case " ":e.preventDefault(),this.tryFlipByIdx(this.selectedIdx);break}}tryFlipByIdx(e){if(this.locked)return;let t=this.cards[e];t.flipped||t.matched||(t.flipped=true,this.flippedIdxs.push(e),this.checkMatch());}tryFlip(e,t){if(this.locked)return;let s=this.cardAt(e,t);if(s===-1)return;let i=this.cards[s];i.flipped||i.matched||(i.flipped=true,this.flippedIdxs.push(s),this.selectedIdx=s,this.checkMatch());}checkMatch(){if(this.flippedIdxs.length!==2)return;this.locked=true;let[e,t]=this.flippedIdxs;this.cards[e].emoji===this.cards[t].emoji?setTimeout(()=>{if(this.cards[e].matched=true,this.cards[t].matched=true,this.matchCount++,this.score+=10,this.onScore?.(this.score),this.flippedIdxs=[],this.locked=false,this.matchCount===P.length){let s=(Date.now()-this.startTime)/1e3,i=Math.max(0,Math.round((60-s)*5));this.score+=i,this.onScore?.(this.score),this.onGameOver?.(),setTimeout(()=>this.reset(),2e3);}},K+100):setTimeout(()=>{this.cards[e].flipped=false,this.cards[t].flipped=false,this.flippedIdxs=[],this.locked=false;},K+600);}loop(e){if(!this.running)return;this.animFrameId=requestAnimationFrame(s=>this.loop(s));let t=(e-this.lastTime)/K;this.lastTime=e;for(let s of this.cards)s.flipped&&s.flipProgress<1?s.flipProgress=Math.min(1,s.flipProgress+t):!s.flipped&&s.flipProgress>0&&(s.flipProgress=Math.max(0,s.flipProgress-t)),s.matched&&(s.pulsePhase=(s.pulsePhase+t*.15)%(Math.PI*2));this.render();}render(){let{ctx:e,W:t,H:s,theme:i}=this;e.fillStyle=i.background,e.fillRect(0,0,t,s);for(let n=0;n<this.cards.length;n++){let r=this.cards[n],l=this.bounds(n),o=Math.abs(Math.cos(r.flipProgress*Math.PI)),h=l.x+l.w/2,v=l.y+l.h/2;if(e.save(),e.translate(h,v),e.scale(o,1),e.translate(-l.w/2,-l.h/2),e.beginPath(),e.roundRect(0,0,l.w,l.h,6),r.flipProgress>.5){if(r.matched){let c=.2+Math.sin(r.pulsePhase)*.1;e.fillStyle=i.primary+"33",e.fill(),e.strokeStyle=i.primary,e.lineWidth=3,e.globalAlpha=.6+c,e.stroke(),e.globalAlpha=1,e.shadowColor=i.primary,e.shadowBlur=8,e.stroke(),e.shadowBlur=0;}else e.fillStyle=i.surface,e.fill();e.font=`${Math.min(l.w,l.h)*.45}px serif`,e.textAlign="center",e.textBaseline="middle",e.fillText(r.emoji,l.w/2,l.h/2);}else {e.fillStyle=i.primary+"99",e.fill(),e.strokeStyle=i.primary,e.lineWidth=1,e.globalAlpha=.25;for(let c=0;c<3;c++){let f=4+c*5;e.strokeRect(f,f,l.w-f*2,l.h-f*2);}e.globalAlpha=1;}n===this.selectedIdx&&(e.strokeStyle=i.text,e.lineWidth=2,e.setLineDash([4,3]),e.strokeRect(0,0,l.w,l.h),e.setLineDash([])),e.restore();}e.fillStyle=i.text,e.globalAlpha=.5,e.font="11px system-ui",e.textAlign="right",e.fillText(`${this.matchCount}/${P.length} \xB7 ${this.score}pts`,t-6,s-4),e.globalAlpha=1;}};});var pe={};g(pe,{WhackAMoleGame:()=>z});var R,w,Ie,me,Ge,Oe,Le,z,ue=p(()=>{m();R=3,w=2e4,Ie=R*R,me=900,Ge=500,Oe=800,Le=700,z=class{name="whack-a-mole";bundleSize=4e3;canvas;ctx;theme;W=0;H=0;holes=[];score=0;timeLeft=w;roundStart=0;animFrameId=null;running=false;lastTime=0;roundOver=false;nextSpawnTime=0;boundClick;boundTouch;boundKeyDown;onScore;onGameOver;constructor(e,t){this.onScore=e,this.onGameOver=t,this.boundClick=this.onClick.bind(this),this.boundTouch=this.onTouch.bind(this),this.boundKeyDown=this.onKeyDown.bind(this);}init(e,t){this.canvas=e,this.ctx=e.getContext("2d"),this.theme=d(t),e.setAttribute("aria-label","Whack-a-Mole game \u2014 loading in background"),e.setAttribute("role","img");let s=window.devicePixelRatio||1,i=e.getBoundingClientRect();e.width=i.width*s,e.height=i.height*s,this.ctx.scale(s,s),e.style.width=`${i.width}px`,e.style.height=`${i.height}px`,this.W=i.width,this.H=i.height,this.reset();}reset(){this.holes=Array.from({length:Ie},()=>({active:false,showProgress:0,hideAt:0,hit:false,hitProgress:0})),this.score=0,this.timeLeft=w,this.roundOver=false,this.nextSpawnTime=0;}start(){this.running=true,this.roundStart=performance.now(),this.lastTime=performance.now(),this.nextSpawnTime=performance.now(),this.loop(performance.now()),this.canvas.addEventListener("click",this.boundClick),this.canvas.addEventListener("touchend",this.boundTouch,{passive:true}),document.addEventListener("keydown",this.boundKeyDown);}pause(){this.running=false,this.animFrameId!==null&&(cancelAnimationFrame(this.animFrameId),this.animFrameId=null);}resume(){this.running||(this.running=true,this.lastTime=performance.now(),this.nextSpawnTime=performance.now(),this.loop(performance.now()));}destroy(){this.pause(),this.canvas.removeEventListener("click",this.boundClick),this.canvas.removeEventListener("touchend",this.boundTouch),document.removeEventListener("keydown",this.boundKeyDown);}getScore(){return this.score}trySpawnMole(e){if(this.roundOver||e<this.nextSpawnTime)return;let t=this.holes.map((r,l)=>!r.active&&!r.hit?l:-1).filter(r=>r>=0);if(t.length>0){let r=t[Math.floor(Math.random()*t.length)];this.activateMole(r,e);}let s=e-this.roundStart,i=Math.min(1,s/w),n=me-i*(me-Ge);this.nextSpawnTime=e+n;}activateMole(e,t){let s=this.holes[e];s.active=true,s.hit=false,s.hitProgress=0,s.hideAt=t+Oe+Math.random()*Le;}holeBounds(e){let t=R,s=R,i=24,n=24,r=(this.W-i*2)/t,l=(this.H-n*2-30)/s,o=e%t,h=Math.floor(e/t),v=i+o*r+r/2,c=n+h*l+l/2+30,f=Math.min(r,l)*.38;return {cx:v,cy:c,r:f}}onClick(e){let t=this.canvas.getBoundingClientRect();this.tryHit(e.clientX-t.left,e.clientY-t.top);}onTouch(e){let t=e.changedTouches[0];if(!t)return;let s=this.canvas.getBoundingClientRect();this.tryHit(t.clientX-s.left,t.clientY-s.top);}onKeyDown(e){let t=parseInt(e.key,10);if(t>=1&&t<=9){if(e.preventDefault(),this.roundOver){this.reset(),this.nextSpawnTime=performance.now();return}let s=t-1,i=this.holes[s];i&&i.active&&!i.hit&&(i.active=false,i.hit=true,i.hitProgress=0,i.hideAt=0,this.score++,this.onScore?.(this.score));}}tryHit(e,t){if(this.roundOver){this.reset(),this.nextSpawnTime=performance.now();return}for(let s=0;s<this.holes.length;s++){let i=this.holes[s];if(!i.active||i.hit)continue;let n=this.holeBounds(s);if(Math.hypot(e-n.cx,t-n.cy)<n.r*1.2){i.active=false,i.hit=true,i.hitProgress=0,i.hideAt=0,this.score++,this.onScore?.(this.score);return}}}loop(e){if(!this.running)return;this.animFrameId=requestAnimationFrame(s=>this.loop(s));let t=e-this.lastTime;this.lastTime=e,this.update(e,t),this.render();}update(e,t){if(this.roundOver)return;if(this.timeLeft=Math.max(0,w-(e-this.roundStart)),this.timeLeft===0){this.roundOver=true,this.onGameOver?.();return}this.trySpawnMole(e);for(let i of this.holes)i.active&&i.hideAt>0&&e>=i.hideAt&&(i.active=false,i.hideAt=0);let s=t/200;for(let i of this.holes)i.active?i.showProgress=Math.min(1,i.showProgress+s):i.hit?(i.hitProgress=Math.min(1,i.hitProgress+s),i.hitProgress>=1&&(i.hit=false)):i.showProgress=Math.max(0,i.showProgress-s);}render(){let{ctx:e,W:t,H:s,theme:i}=this;e.fillStyle=i.background,e.fillRect(0,0,t,s);let n=this.timeLeft/w;e.fillStyle=i.surface,e.fillRect(16,10,t-32,12),e.fillStyle=n>.3?i.primary:i.accent,e.beginPath(),e.roundRect(16,10,(t-32)*n,12,4),e.fill(),e.fillStyle=i.text,e.font="bold 18px monospace",e.textAlign="left",e.fillText(`${this.score}`,16,s-8),e.textAlign="right",e.globalAlpha=.5,e.font="12px system-ui",e.fillText(`${Math.ceil(this.timeLeft/1e3)}s`,t-16,s-8),e.globalAlpha=1;for(let r=0;r<this.holes.length;r++){let l=this.holes[r],o=this.holeBounds(r);e.fillStyle=i.surface,e.globalAlpha=.6,e.beginPath(),e.ellipse(o.cx,o.cy+o.r*.2,o.r,o.r*.4,0,0,Math.PI*2),e.fill(),e.globalAlpha=1,e.fillStyle=i.text,e.globalAlpha=.2,e.font=`bold ${o.r*.3}px monospace`,e.textAlign="center",e.fillText(`${r+1}`,o.cx,o.cy+o.r*.55),e.globalAlpha=1;let h=(l.active||l.hit)&&l.hit?1-l.hitProgress:l.showProgress;if(h<=0)continue;let v=(1-h)*o.r*1.5,c=o.cy-v;e.save(),e.beginPath(),e.ellipse(o.cx,o.cy+o.r*.25,o.r,o.r*.45,0,0,Math.PI*2),e.clip(),e.fillStyle=l.hit?i.accent:"#8B4513",e.beginPath(),e.arc(o.cx,c,o.r*.8,0,Math.PI*2),e.fill(),e.fillStyle="#fff",e.beginPath(),e.arc(o.cx-o.r*.28,c-o.r*.15,o.r*.15,0,Math.PI*2),e.fill(),e.beginPath(),e.arc(o.cx+o.r*.28,c-o.r*.15,o.r*.15,0,Math.PI*2),e.fill(),e.fillStyle="#333",e.beginPath(),e.arc(o.cx-o.r*.25,c-o.r*.15,o.r*.08,0,Math.PI*2),e.fill(),e.beginPath(),e.arc(o.cx+o.r*.25,c-o.r*.15,o.r*.08,0,Math.PI*2),e.fill(),e.fillStyle="#FFB6C1",e.beginPath(),e.arc(o.cx,c+o.r*.05,o.r*.1,0,Math.PI*2),e.fill(),l.hit&&(e.fillStyle="#FFD700",e.font=`bold ${o.r*.6}px system-ui`,e.textAlign="center",e.fillText("\u2713",o.cx,c-o.r*.6)),e.restore();}this.roundOver&&(e.fillStyle="rgba(0,0,0,0.55)",e.fillRect(0,0,t,s),e.fillStyle=i.text,e.font="bold 24px system-ui",e.textAlign="center",e.fillText(`Score: ${this.score}`,t/2,s/2),e.font="14px system-ui",e.globalAlpha=.7,e.fillText("Tap to play again",t/2,s/2+32),e.globalAlpha=1);}};});m();var x=class{constructor(e){this.options=e;}delayTimer=null;minDisplayTimer=null;showTime=null;loadingCompletedWhileDelaying=false;isVisible=false;destroyed=false;start(){this.destroyed||(this.loadingCompletedWhileDelaying=false,this.delayTimer=setTimeout(()=>{this.delayTimer=null,!this.destroyed&&(this.loadingCompletedWhileDelaying||(this.isVisible=true,this.showTime=Date.now(),this.options.onShow()));},this.options.delay));}end(){if(this.destroyed)return;if(this.delayTimer!==null){clearTimeout(this.delayTimer),this.delayTimer=null,this.loadingCompletedWhileDelaying=true;return}if(!this.isVisible)return;let e=Date.now()-(this.showTime??0),t=this.options.minDisplay-e;t<=0?this.triggerExit():this.minDisplayTimer=setTimeout(()=>{this.destroyed||this.triggerExit();},t);}triggerExit(){this.isVisible=false,this.options.onExit();}destroy(){this.destroyed=true,this.delayTimer!==null&&(clearTimeout(this.delayTimer),this.delayTimer=null),this.minDisplayTimer!==null&&(clearTimeout(this.minDisplayTimer),this.minDisplayTimer=null);}};var O="loading-games:scores";function C(){if(typeof window>"u"||!window.localStorage)return {};try{let a=localStorage.getItem(O);return a?JSON.parse(a):{}}catch{return {}}}function X(a){if(!(typeof window>"u"||!window.localStorage))try{localStorage.setItem(O,JSON.stringify(a));}catch{}}function U(a,e){return e?`${e}:${a}`:a}function A(a,e){let t=C(),s=U(a,e);return t[s]?.personalBest??0}function L(a,e,t){let s=C(),i=U(a,t),n=s[i],r=e>(n?.personalBest??0);return s[i]={personalBest:r?e:n?.personalBest??0,lastPlayed:new Date().toISOString(),totalGames:(n?.totalGames??0)+1},X(s),r}function ke(a){let e=C();return a?Object.fromEntries(Object.entries(e).filter(([t])=>t.startsWith(`${a}:`))):e}function Ce(a){if(!a){typeof window<"u"&&window.localStorage&&localStorage.removeItem(O);return}let e=C(),t=`${a}:`,s=Object.fromEntries(Object.entries(e).filter(([i])=>!i.startsWith(t)));X(s);}var Fe={sm:{width:280,height:220},md:{width:400,height:320},lg:{width:560,height:440},full:{width:0,height:0}},S=class{hasEverActivated=false;host;callbacks;options={};state="idle";canvas=null;overlay=null;skipLink=null;currentGame=null;delayController=null;_gameStartTime=0;_currentScore=0;boundVisibilityChange;constructor(e,t){this.host=e,this.callbacks=t,this.boundVisibilityChange=this.handleVisibilityChange.bind(this),document.addEventListener("visibilitychange",this.boundVisibilityChange);}activate(e){this.hasEverActivated=true,this.options=e,this.state==="idle"&&(this.setState("delaying"),this.delayController=new x({delay:e.delay??800,minDisplay:e.minDisplay??0,onShow:()=>this.showGame(),onExit:()=>this.exitGame()}),this.delayController.start());}deactivate(e){if(this.options.exitAnimation=e.animation,this.state==="delaying"){this.delayController?.end(),this.setState("idle");return}this.state==="playing"&&(this.currentGame?.pause(),this.delayController?.end());}errorDeactivate(){this.delayController?.destroy(),this.delayController=null,this.currentGame?.destroy(),this.currentGame=null,this.teardownDOM(),this.setState("idle");}updateOptions(e){this.options={...this.options,...e};}destroy(){this.delayController?.destroy(),this.currentGame?.destroy(),this.currentGame=null,this.teardownDOM(),document.removeEventListener("visibilitychange",this.boundVisibilityChange);}async showGame(){this.setState("loading-game");let e=this.resolveGameName(this.options.game??"random");try{let t=await this.loadGameChunk(e);this.setupDOM();let s=d(this.options.theme);if(this.overlay&&G(this.overlay,s),this.canvas&&this.canvas.setAttribute("aria-label",`${e} game \u2014 loading in background`),window.matchMedia("(prefers-reduced-motion: reduce)").matches){this.showReducedMotionFallback(s);return}this.currentGame=new t(i=>this.handleScoreUpdate(i,e),()=>this.handleGameOverEvent(e)),await this.currentGame.init(this.canvas,this.options.theme??{}),this.setState("playing"),this._gameStartTime=Date.now(),this._currentScore=0,this.currentGame.start(),this.showToast("Loading in the background \u2014 play while you wait!");}catch(t){this.callbacks.onError(t instanceof Error?t:new Error(String(t))),this.teardownDOM(),this.setState("idle");}}async exitGame(){(this.state==="playing"||this.state==="min-display")&&(this.currentGame?.pause(),this.showCompletionOverlay(),await this.sleep(1500)),this.setState("exiting"),await this.animateExit(this.options.exitAnimation??"fade"),this.currentGame?.destroy(),this.currentGame=null,this.teardownDOM(),this.setState("idle"),this.callbacks.onComplete();}setupDOM(){let e=this.options.size??"md",t=Fe[e];this.overlay=document.createElement("div"),this.overlay.setAttribute("data-lg-overlay",""),Object.assign(this.overlay.style,{position:e==="full"?"fixed":"relative",inset:e==="full"?"0":"auto",width:e==="full"?"100%":`${t.width}px`,height:e==="full"?"100%":`${t.height}px`,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",zIndex:e==="full"?"9999":"auto",opacity:"0",transition:"opacity 300ms ease, transform 300ms ease",transform:"scale(0.97)",overflow:"hidden",borderRadius:e==="full"?"0":"12px"}),this.skipLink=document.createElement("a"),this.skipLink.href="#",this.skipLink.textContent="Skip game, wait for loading",this.skipLink.setAttribute("data-lg-skip",""),Object.assign(this.skipLink.style,{position:"absolute",top:"-9999px",left:"-9999px"}),this.skipLink.addEventListener("focus",()=>{Object.assign(this.skipLink.style,{top:"8px",left:"8px"});}),this.skipLink.addEventListener("blur",()=>{Object.assign(this.skipLink.style,{top:"-9999px",left:"-9999px"});}),this.skipLink.addEventListener("click",s=>{s.preventDefault(),this.deactivate({animation:"none"});}),this.canvas=document.createElement("canvas"),this.canvas.setAttribute("role","application"),this.canvas.setAttribute("tabindex","0"),Object.assign(this.canvas.style,{width:"100%",height:"100%",display:"block"}),this.overlay.appendChild(this.skipLink),this.overlay.appendChild(this.canvas),this.host.appendChild(this.overlay),requestAnimationFrame(()=>{requestAnimationFrame(()=>{this.overlay&&(this.overlay.style.opacity="1",this.overlay.style.transform="scale(1)");});});}teardownDOM(){this.overlay?.remove(),this.overlay=null,this.canvas=null,this.skipLink=null;}async animateExit(e){!this.overlay||e==="none"||(e==="fade"?this.overlay.style.opacity="0":e==="slide"&&(this.overlay.style.transform="translateY(-20px)",this.overlay.style.opacity="0"),await this.sleep(350));}showToast(e){let t=document.createElement("div");t.textContent=e,Object.assign(t.style,{position:"absolute",bottom:"12px",left:"50%",transform:"translateX(-50%)",background:"rgba(0,0,0,0.7)",color:"#fff",padding:"6px 12px",borderRadius:"6px",fontSize:"12px",whiteSpace:"nowrap",opacity:"0",transition:"opacity 300ms ease",pointerEvents:"none"}),this.overlay?.appendChild(t),requestAnimationFrame(()=>{t.style.opacity="1";}),setTimeout(()=>{t.style.opacity="0",setTimeout(()=>t.remove(),300);},2e3);}showCompletionOverlay(){let e=document.createElement("div");e.textContent="\u2713 Done! Loading complete.",Object.assign(e.style,{position:"absolute",inset:"0",display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(0,0,0,0.6)",color:"#fff",fontSize:"18px",fontWeight:"bold",fontFamily:"system-ui, sans-serif",opacity:"0",transition:"opacity 300ms ease"}),this.overlay?.appendChild(e),requestAnimationFrame(()=>{e.style.opacity="1";});}showReducedMotionFallback(e){if(!this.canvas)return;let t=this.canvas.getContext("2d"),s=this.canvas.width,i=this.canvas.height;t.fillStyle=e.background,t.fillRect(0,0,s,i),t.fillStyle=e.primary,t.font="16px system-ui",t.textAlign="center",t.fillText("Loading\u2026",s/2,i/2),this.setState("playing");}handleGameOverEvent(e){let t=Date.now()-this._gameStartTime,s=A(e,this.options.namespace);this.callbacks.onGameOver({game:e,finalScore:this._currentScore,duration:t,isNewRecord:this._currentScore>s});}handleScoreUpdate(e,t){this._currentScore=e;let s=A(t,this.options.namespace),i=false;this.options.saveScores!==false&&e>s&&(i=L(t,e,this.options.namespace)),this.callbacks.onScore({game:t,current:e,personalBest:Math.max(e,s),isNewRecord:i}),i&&this.showNewRecordBadge();}showNewRecordBadge(){let e=document.createElement("div");e.textContent="\u{1F3C6} New Record!",Object.assign(e.style,{position:"absolute",top:"12px",left:"50%",transform:"translateX(-50%) scale(0.8)",background:"gold",color:"#000",padding:"4px 10px",borderRadius:"6px",fontSize:"12px",fontWeight:"bold",fontFamily:"system-ui, sans-serif",opacity:"0",transition:"opacity 300ms ease, transform 300ms ease",pointerEvents:"none"}),this.overlay?.appendChild(e),requestAnimationFrame(()=>{e.style.opacity="1",e.style.transform="translateX(-50%) scale(1)";}),setTimeout(()=>{e.style.opacity="0",setTimeout(()=>e.remove(),300);},2500);}async loadGameChunk(e){switch(e){case "snake":return (await Promise.resolve().then(()=>(J(),q))).SnakeGame;case "brick-breaker":return (await Promise.resolve().then(()=>(ee(),Q))).BrickBreakerGame;case "flappy":return (await Promise.resolve().then(()=>(se(),ie))).FlappyGame;case "2048":return (await Promise.resolve().then(()=>(oe(),ne))).Game2048;case "wordle-lite":return (await Promise.resolve().then(()=>(ae(),re))).WordleLiteGame;case "asteroids":return (await Promise.resolve().then(()=>(he(),le))).AsteroidsGame;case "memory-cards":return (await Promise.resolve().then(()=>(ce(),de))).MemoryCardsGame;case "whack-a-mole":return (await Promise.resolve().then(()=>(ue(),pe))).WhackAMoleGame;default:throw new Error(`Unknown game: ${e}`)}}resolveGameName(e){if(e!=="random")return e;let t=["snake","brick-breaker","flappy","2048","wordle-lite","asteroids","memory-cards","whack-a-mole"];return t[Math.floor(Math.random()*t.length)]}setState(e){this.state=e;}handleVisibilityChange(){this.currentGame&&(document.visibilityState==="hidden"?this.currentGame.pause():this.state==="playing"&&this.currentGame.resume());}sleep(e){return new Promise(t=>setTimeout(t,e))}};var E=class extends HTMLElement{static observedAttributes=["game","active","size","delay","min-display","exit-animation","save-scores","namespace"];controller=null;_theme={};connectedCallback(){this.setAttribute("role","region"),this.setAttribute("aria-label","Loading game"),this.controller=new S(this,{onScore:e=>this.dispatchEvent(new CustomEvent("lg:score",{detail:e,bubbles:true})),onGameOver:e=>this.dispatchEvent(new CustomEvent("lg:gameover",{detail:e,bubbles:true})),onComplete:()=>this.dispatchEvent(new CustomEvent("lg:complete",{bubbles:true})),onError:e=>{this.controller?.errorDeactivate(),this.dispatchEvent(new CustomEvent("lg:error",{detail:e,bubbles:true}));}}),this.getAttribute("active")==="true"&&this.controller.activate(this.buildOptions());}disconnectedCallback(){this.controller?.destroy(),this.controller=null;}attributeChangedCallback(e,t,s){t===s||!this.controller||(e==="active"?s==="true"?this.controller.activate(this.buildOptions()):this.controller.deactivate({animation:this.getExitAnimation()}):this.controller.updateOptions(this.buildOptions()));}set theme(e){this._theme=e,this.controller?.updateOptions(this.buildOptions());}get theme(){return this._theme}start(){this.setAttribute("active","true");}stop(){this.removeAttribute("active");}setTheme(e){this.theme=e;}buildOptions(){let e=this.getAttribute("game")??"random",t=this.getAttribute("size")??"md",s=parseInt(this.getAttribute("delay")??"800",10),i=parseInt(this.getAttribute("min-display")??"0",10),n=this.getAttribute("exit-animation")??"fade",r=this.getAttribute("save-scores")!=="false",l=this.getAttribute("namespace")??void 0;return {game:e,size:t,delay:s,minDisplay:i,exitAnimation:n,saveScores:r,namespace:l,theme:this._theme}}getExitAnimation(){return this.getAttribute("exit-animation")??"fade"}};m();F();typeof customElements<"u"&&!customElements.get("loading-game")&&customElements.define("loading-game",E);export{x as DelayController,T as Dpad,S as GameController,E as LoadingGameElement,G as applyThemeToElement,Ce as clearScores,ke as getAllScores,A as getPersonalBest,d as resolveTheme,L as saveScore};//# sourceMappingURL=index.js.map
|
|
2
|
+
//# sourceMappingURL=index.js.map
|