easter-egg-quest 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.
@@ -0,0 +1,4997 @@
1
+ const DEFAULT_SCRIPT = {
2
+ // ── Stage 0: Hidden entry hints (shown in escalating order) ───────────
3
+ hiddenEntryHints: [
4
+ "hey, it's not a bug — there's a hidden game on this page",
5
+ "look carefully — two words are hiding among the text",
6
+ "it blends in with everything else, but it’s clickable"
7
+ ],
8
+ // ── Entry confirmation ────────────────────────────────────────────────
9
+ entryConfirmation: [
10
+ "...",
11
+ "you noticed",
12
+ "then we can begin"
13
+ ],
14
+ // ── Stage 1: Stillness ────────────────────────────────────────────────
15
+ stage1Intro: [
16
+ "three eggs are hidden here",
17
+ "each one asks something different",
18
+ "the first riddle:",
19
+ "I am a cat that comes only to those who ignore it. Open your hand — and I leave. Close it — and I was never there."
20
+ ],
21
+ stage1Reactions: [
22
+ "the cat is watching",
23
+ "it doesn’t trust you yet",
24
+ "you’re trying too hard",
25
+ "what does a cat want?",
26
+ "the hand is still clenched",
27
+ "have you tried not trying?",
28
+ "forget the egg exists",
29
+ "it won’t come if you’re waiting",
30
+ "some things can’t be taken",
31
+ "only given"
32
+ ],
33
+ stage1Success: [
34
+ "you understood",
35
+ "stillness was the answer",
36
+ "the first egg reveals itself",
37
+ "to those who can stop",
38
+ "hold it to collect"
39
+ ],
40
+ // ── Stage 2: Motion ───────────────────────────────────────────────────
41
+ stage2Intro: [
42
+ "you solved the first one",
43
+ "the second riddle:",
44
+ "I am a river that needs you to flow. Without you, I dry up. But you can’t carry me — only become me.",
45
+ "what am I?"
46
+ ],
47
+ stage2Reactions: [
48
+ "the river is dry",
49
+ "nothing flows",
50
+ "what does a river need?",
51
+ "you are the current",
52
+ "without you, there’s no river",
53
+ "the answer isn’t in your head",
54
+ "it’s in your hands",
55
+ "be the water"
56
+ ],
57
+ stage2Success: [
58
+ "movement was the answer",
59
+ "the second egg appears",
60
+ "for those who never stop",
61
+ "not stillness this time",
62
+ "but life in motion"
63
+ ],
64
+ // ── Stage 3: Rhythm ───────────────────────────────────────────────────
65
+ stage3Intro: [
66
+ "two truths that shouldn’t exist together",
67
+ "the final riddle:",
68
+ "I am the conversation between opposites. Every living thing knows me. You’ve been doing me your whole life without thinking."
69
+ ],
70
+ stage3Reactions: [
71
+ "one truth alone isn’t enough",
72
+ "the other alone isn’t either",
73
+ "what do all living things share?",
74
+ "you already know the answer",
75
+ "you’ve been doing it since birth",
76
+ "listen to yourself",
77
+ "closer...",
78
+ "can you feel it?",
79
+ "the body remembers",
80
+ "in",
81
+ "out"
82
+ ],
83
+ stage3Success: [
84
+ "rhythm was the answer",
85
+ "not stopping, not rushing",
86
+ "but the dance between them",
87
+ "the third egg appears",
88
+ "for those who breathe"
89
+ ],
90
+ // ── Finale ────────────────────────────────────────────────────────────
91
+ finale: [
92
+ "you were looking for three eggs",
93
+ "but you found something else",
94
+ "stillness",
95
+ "motion",
96
+ "rhythm",
97
+ "this is where life appears"
98
+ ],
99
+ // ── Results / Share ───────────────────────────────────────────────────
100
+ results: [
101
+ "begin again",
102
+ "challenge someone"
103
+ ]
104
+ };
105
+ const THEME_DEFAULTS = {
106
+ overlayBg: "rgba(0,0,0,0.25)",
107
+ textColor: "rgba(255,255,255,0.92)",
108
+ textSecondary: "rgba(255,255,255,0.55)",
109
+ accent: "#D4A574",
110
+ accentGlow: "rgba(212,165,116,0.4)",
111
+ hudBg: "rgba(0,0,0,0.45)",
112
+ hudText: "rgba(255,255,255,0.85)"
113
+ };
114
+ const DANGEROUS_KEYWORDS = [
115
+ "buy",
116
+ "purchase",
117
+ "checkout",
118
+ "pay",
119
+ "delete",
120
+ "remove",
121
+ "submit",
122
+ "confirm",
123
+ "order",
124
+ "sign out",
125
+ "log out",
126
+ "logout",
127
+ "signout",
128
+ "unsubscribe",
129
+ "cancel",
130
+ "close account"
131
+ ];
132
+ const DEFAULT_EXCLUDE_SELECTORS = [
133
+ 'form [type="submit"]',
134
+ "[data-no-easter]"
135
+ ];
136
+ function resolveConfig(raw = {}) {
137
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
138
+ const prefersReduced = typeof window !== "undefined" && ((_a = window.matchMedia) == null ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)").matches);
139
+ const theme = resolveTheme(raw.theme);
140
+ return {
141
+ theme,
142
+ hiddenEntry: {
143
+ mode: ((_b = raw.hiddenEntry) == null ? void 0 : _b.mode) ?? "auto",
144
+ selector: (_c = raw.hiddenEntry) == null ? void 0 : _c.selector,
145
+ excludeSelectors: [
146
+ ...DEFAULT_EXCLUDE_SELECTORS,
147
+ ...((_d = raw.hiddenEntry) == null ? void 0 : _d.excludeSelectors) ?? []
148
+ ]
149
+ },
150
+ hud: typeof raw.hud === "boolean" ? { enabled: raw.hud, position: "top-left" } : { enabled: ((_e = raw.hud) == null ? void 0 : _e.enabled) ?? true, position: ((_f = raw.hud) == null ? void 0 : _f.position) ?? "top-left" },
151
+ shrine: typeof raw.shrine === "boolean" ? { enabled: raw.shrine, position: "bottom-right" } : { enabled: ((_g = raw.shrine) == null ? void 0 : _g.enabled) ?? true, position: ((_h = raw.shrine) == null ? void 0 : _h.position) ?? "bottom-right" },
152
+ sounds: raw.sounds ?? false,
153
+ stageDurations: {
154
+ stillnessMs: ((_i = raw.stageDurations) == null ? void 0 : _i.stillnessMs) ?? 2e4,
155
+ motionMs: ((_j = raw.stageDurations) == null ? void 0 : _j.motionMs) ?? 2e4,
156
+ rhythmCycles: ((_k = raw.stageDurations) == null ? void 0 : _k.rhythmCycles) ?? 6
157
+ },
158
+ renderer: raw.renderer ?? "auto",
159
+ scoring: {
160
+ enableLocal: ((_l = raw.scoring) == null ? void 0 : _l.enableLocal) ?? true,
161
+ leaderboardAdapter: (_m = raw.scoring) == null ? void 0 : _m.leaderboardAdapter
162
+ },
163
+ accessibility: {
164
+ reducedMotion: ((_n = raw.accessibility) == null ? void 0 : _n.reducedMotion) === "auto" ? prefersReduced : ((_o = raw.accessibility) == null ? void 0 : _o.reducedMotion) ?? prefersReduced,
165
+ highContrast: ((_p = raw.accessibility) == null ? void 0 : _p.highContrast) ?? false,
166
+ disableHiddenEntry: ((_q = raw.accessibility) == null ? void 0 : _q.disableHiddenEntry) ?? false,
167
+ subtitlesLog: ((_r = raw.accessibility) == null ? void 0 : _r.subtitlesLog) ?? false
168
+ },
169
+ callbacks: raw.callbacks ?? {}
170
+ };
171
+ }
172
+ function resolveTheme(input) {
173
+ if (!input || input === "auto" || input === "light" || input === "dark") {
174
+ return { ...THEME_DEFAULTS };
175
+ }
176
+ return { ...THEME_DEFAULTS, ...input };
177
+ }
178
+ function resolveNarrative(overrides) {
179
+ if (!overrides) return { ...DEFAULT_SCRIPT };
180
+ return { ...DEFAULT_SCRIPT, ...overrides };
181
+ }
182
+ class EventBus {
183
+ constructor() {
184
+ this.listeners = /* @__PURE__ */ new Map();
185
+ }
186
+ on(event, fn) {
187
+ if (!this.listeners.has(event)) {
188
+ this.listeners.set(event, /* @__PURE__ */ new Set());
189
+ }
190
+ this.listeners.get(event).add(fn);
191
+ }
192
+ off(event, fn) {
193
+ var _a;
194
+ (_a = this.listeners.get(event)) == null ? void 0 : _a.delete(fn);
195
+ }
196
+ emit(event, ...args) {
197
+ var _a;
198
+ (_a = this.listeners.get(event)) == null ? void 0 : _a.forEach((fn) => {
199
+ try {
200
+ fn(...args);
201
+ } catch {
202
+ }
203
+ });
204
+ }
205
+ clear() {
206
+ this.listeners.clear();
207
+ }
208
+ }
209
+ const VALID_TRANSITIONS = {
210
+ "idle": ["entry"],
211
+ "entry": ["stage1-intro"],
212
+ "stage1-intro": ["stage1-active"],
213
+ "stage1-active": ["stage1-success"],
214
+ "stage1-success": ["stage2-intro"],
215
+ "stage2-intro": ["stage2-active"],
216
+ "stage2-active": ["stage2-success"],
217
+ "stage2-success": ["stage3-intro"],
218
+ "stage3-intro": ["stage3-active"],
219
+ "stage3-active": ["stage3-success"],
220
+ "stage3-success": ["finale"],
221
+ "finale": ["results"],
222
+ "results": ["complete", "idle"],
223
+ "complete": ["idle"]
224
+ };
225
+ class StateMachine {
226
+ constructor() {
227
+ this._state = "idle";
228
+ this._onTransition = null;
229
+ }
230
+ get state() {
231
+ return this._state;
232
+ }
233
+ /** Register a callback invoked on every valid transition. */
234
+ onTransition(cb) {
235
+ this._onTransition = cb;
236
+ }
237
+ /** Attempt a transition. Returns true if valid, false otherwise. */
238
+ transitionTo(next) {
239
+ var _a;
240
+ const allowed = VALID_TRANSITIONS[this._state];
241
+ if (!allowed || !allowed.includes(next)) {
242
+ console.warn(`[EasterEggQuest] Invalid transition: ${this._state} → ${next}`);
243
+ return false;
244
+ }
245
+ const prev = this._state;
246
+ this._state = next;
247
+ (_a = this._onTransition) == null ? void 0 : _a.call(this, prev, next);
248
+ return true;
249
+ }
250
+ /** Force-reset to idle (used for restart / destroy). */
251
+ reset() {
252
+ this._state = "idle";
253
+ }
254
+ /** Force state to a specific value (used for resuming from checkpoint). */
255
+ forceState(state) {
256
+ this._state = state;
257
+ }
258
+ /** Human-readable label for the current stage. */
259
+ get label() {
260
+ return STAGE_LABELS[this._state] ?? "";
261
+ }
262
+ }
263
+ const STAGE_LABELS = {
264
+ "idle": "",
265
+ "entry": "Searching",
266
+ "stage1-intro": "Stillness",
267
+ "stage1-active": "Stillness",
268
+ "stage1-success": "Stillness",
269
+ "stage2-intro": "Motion",
270
+ "stage2-active": "Motion",
271
+ "stage2-success": "Motion",
272
+ "stage3-intro": "Rhythm",
273
+ "stage3-active": "Rhythm",
274
+ "stage3-success": "Rhythm",
275
+ "finale": "Finale",
276
+ "results": "Results",
277
+ "complete": "Complete"
278
+ };
279
+ class InputTracker {
280
+ constructor() {
281
+ this._snapshot = {
282
+ isMoving: false,
283
+ lastMoveTime: 0,
284
+ lastStillTime: Date.now(),
285
+ mouseX: 0,
286
+ mouseY: 0,
287
+ velocity: 0,
288
+ totalClicks: 0,
289
+ totalScrolls: 0,
290
+ totalKeyPresses: 0,
291
+ totalDistance: 0,
292
+ maxVelocity: 0
293
+ };
294
+ this._phases = [];
295
+ this._currentPhaseType = "still";
296
+ this._currentPhaseStart = Date.now();
297
+ this._prevX = 0;
298
+ this._prevY = 0;
299
+ this._prevMoveTs = 0;
300
+ this.STILL_THRESHOLD_MS = 400;
301
+ this.MIN_PHASE_MS = 300;
302
+ this._stillTimer = null;
303
+ this._handlers = [];
304
+ this._active = false;
305
+ this._onVisibilityChange = () => {
306
+ if (document.hidden) {
307
+ this._snapshot.isMoving = true;
308
+ } else {
309
+ const now = Date.now();
310
+ this._snapshot.isMoving = false;
311
+ this._snapshot.lastStillTime = now;
312
+ this._snapshot.velocity = 0;
313
+ this._currentPhaseType = "still";
314
+ this._currentPhaseStart = now;
315
+ }
316
+ };
317
+ this._onWindowBlur = () => {
318
+ this._snapshot.isMoving = true;
319
+ };
320
+ this._onPointerMove = (e) => {
321
+ const now = Date.now();
322
+ const dx = e.clientX - this._prevX;
323
+ const dy = e.clientY - this._prevY;
324
+ const dt = now - this._prevMoveTs || 1;
325
+ const dist = Math.sqrt(dx * dx + dy * dy);
326
+ if (dist < 2) return;
327
+ this._snapshot.mouseX = e.clientX;
328
+ this._snapshot.mouseY = e.clientY;
329
+ this._snapshot.velocity = dist / dt * 1e3;
330
+ this._snapshot.totalDistance += dist;
331
+ if (this._snapshot.velocity > this._snapshot.maxVelocity) {
332
+ this._snapshot.maxVelocity = this._snapshot.velocity;
333
+ }
334
+ this._prevX = e.clientX;
335
+ this._prevY = e.clientY;
336
+ this._prevMoveTs = now;
337
+ this._registerMove(now);
338
+ };
339
+ this._onTouchMove = (e) => {
340
+ const t = e.touches[0];
341
+ if (!t) return;
342
+ const now = Date.now();
343
+ const dx = t.clientX - this._prevX;
344
+ const dy = t.clientY - this._prevY;
345
+ const dist = Math.sqrt(dx * dx + dy * dy);
346
+ if (dist < 2) return;
347
+ this._snapshot.mouseX = t.clientX;
348
+ this._snapshot.mouseY = t.clientY;
349
+ this._prevX = t.clientX;
350
+ this._prevY = t.clientY;
351
+ this._prevMoveTs = now;
352
+ this._registerMove(now);
353
+ };
354
+ this._onClick = () => {
355
+ this._snapshot.totalClicks++;
356
+ this._registerMove(Date.now());
357
+ };
358
+ this._onActivity = () => {
359
+ this._registerMove(Date.now());
360
+ };
361
+ this._onScroll = () => {
362
+ this._snapshot.totalScrolls++;
363
+ this._registerMove(Date.now());
364
+ };
365
+ this._onKeyDown = () => {
366
+ this._snapshot.totalKeyPresses++;
367
+ this._registerMove(Date.now());
368
+ };
369
+ }
370
+ get snapshot() {
371
+ return { ...this._snapshot };
372
+ }
373
+ get phases() {
374
+ return this._phases.slice();
375
+ }
376
+ /** How long the user has been continuously still (ms). Returns 0 if moving. */
377
+ get stillDuration() {
378
+ if (this._snapshot.isMoving) return 0;
379
+ return Date.now() - this._snapshot.lastStillTime;
380
+ }
381
+ /** How long the user has been continuously moving (ms). Returns 0 if still. */
382
+ get moveDuration() {
383
+ if (!this._snapshot.isMoving) return 0;
384
+ return Date.now() - this._snapshot.lastMoveTime;
385
+ }
386
+ /** Time since any recorded activity. */
387
+ get timeSinceLastActivity() {
388
+ return Date.now() - Math.max(this._snapshot.lastMoveTime, this._snapshot.lastStillTime);
389
+ }
390
+ start() {
391
+ if (this._active) return;
392
+ this._active = true;
393
+ this._snapshot.lastStillTime = Date.now();
394
+ const listen = (event, fn, options) => {
395
+ window.addEventListener(event, fn, { passive: true, ...options });
396
+ this._handlers.push({ event, fn });
397
+ };
398
+ listen("mousemove", this._onPointerMove);
399
+ listen("touchmove", this._onTouchMove);
400
+ listen("click", this._onClick);
401
+ listen("mousedown", this._onActivity);
402
+ listen("touchstart", this._onActivity);
403
+ listen("scroll", this._onScroll, { capture: true });
404
+ listen("keydown", this._onKeyDown);
405
+ listen("wheel", this._onScroll);
406
+ const visHandler = this._onVisibilityChange;
407
+ document.addEventListener("visibilitychange", visHandler);
408
+ this._handlers.push({ event: "visibilitychange", fn: visHandler });
409
+ window.addEventListener("blur", this._onWindowBlur);
410
+ this._handlers.push({ event: "blur", fn: this._onWindowBlur });
411
+ }
412
+ stop() {
413
+ this._active = false;
414
+ for (const { event, fn } of this._handlers) {
415
+ window.removeEventListener(event, fn);
416
+ document.removeEventListener(event, fn);
417
+ }
418
+ this._handlers = [];
419
+ if (this._stillTimer) clearTimeout(this._stillTimer);
420
+ }
421
+ /** Reset phase history (e.g., between stages). */
422
+ resetPhases() {
423
+ this._phases = [];
424
+ this._currentPhaseType = "still";
425
+ this._currentPhaseStart = Date.now();
426
+ }
427
+ resetCounts() {
428
+ this._snapshot.totalClicks = 0;
429
+ this._snapshot.totalScrolls = 0;
430
+ this._snapshot.totalKeyPresses = 0;
431
+ this._snapshot.totalDistance = 0;
432
+ this._snapshot.maxVelocity = 0;
433
+ }
434
+ // ─── Movement / Stillness transition logic ─────────────────────────────
435
+ _registerMove(now) {
436
+ this._snapshot.lastMoveTime = now;
437
+ if (!this._snapshot.isMoving) {
438
+ this._snapshot.isMoving = true;
439
+ this._completePhase("still", now);
440
+ this._currentPhaseType = "move";
441
+ this._currentPhaseStart = now;
442
+ }
443
+ if (this._stillTimer) clearTimeout(this._stillTimer);
444
+ this._stillTimer = setTimeout(() => {
445
+ this._becomeStill();
446
+ }, this.STILL_THRESHOLD_MS);
447
+ }
448
+ _becomeStill() {
449
+ if (!this._snapshot.isMoving) return;
450
+ const now = Date.now();
451
+ this._snapshot.isMoving = false;
452
+ this._snapshot.lastStillTime = now;
453
+ this._snapshot.velocity = 0;
454
+ this._completePhase("move", now);
455
+ this._currentPhaseType = "still";
456
+ this._currentPhaseStart = now;
457
+ }
458
+ _completePhase(type, endTime) {
459
+ const duration = endTime - this._currentPhaseStart;
460
+ if (duration >= this.MIN_PHASE_MS) {
461
+ this._phases.push({
462
+ type,
463
+ startTime: this._currentPhaseStart,
464
+ endTime,
465
+ duration
466
+ });
467
+ }
468
+ if (this._phases.length > 200) {
469
+ this._phases = this._phases.slice(-100);
470
+ }
471
+ }
472
+ }
473
+ function isElementVisible(el) {
474
+ const rect = el.getBoundingClientRect();
475
+ if (rect.width === 0 || rect.height === 0) return false;
476
+ const style = getComputedStyle(el);
477
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
478
+ return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
479
+ }
480
+ function isDangerousElement(el) {
481
+ const text = (el.textContent ?? "").toLowerCase().trim();
482
+ const ariaLabel = (el.getAttribute("aria-label") ?? "").toLowerCase();
483
+ const combined = `${text} ${ariaLabel}`;
484
+ return DANGEROUS_KEYWORDS.some((kw) => combined.includes(kw));
485
+ }
486
+ function findEligibleEntryElements(containerSelector, excludeSelectors = []) {
487
+ const container = containerSelector ? document.querySelector(containerSelector) ?? document.body : document.body;
488
+ const candidates = container.querySelectorAll(
489
+ 'a, button, [role="button"], nav a, nav button, .cta, [data-easter-entry]'
490
+ );
491
+ const excludeSet = /* @__PURE__ */ new Set();
492
+ for (const sel of excludeSelectors) {
493
+ document.querySelectorAll(sel).forEach((el) => excludeSet.add(el));
494
+ }
495
+ const eligible = [];
496
+ candidates.forEach((el) => {
497
+ if (excludeSet.has(el)) return;
498
+ if (!isElementVisible(el)) return;
499
+ if (isDangerousElement(el)) return;
500
+ const rect = el.getBoundingClientRect();
501
+ if (rect.width < 30 || rect.height < 20) return;
502
+ eligible.push(el);
503
+ });
504
+ return eligible;
505
+ }
506
+ function pickRandom(arr) {
507
+ if (arr.length === 0) return void 0;
508
+ return arr[Math.floor(Math.random() * arr.length)];
509
+ }
510
+ function formatTime(ms) {
511
+ const totalSec = Math.floor(ms / 1e3);
512
+ const min = Math.floor(totalSec / 60);
513
+ const sec = totalSec % 60;
514
+ return `${String(min).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
515
+ }
516
+ class HiddenEntry {
517
+ constructor(config, script, onFound) {
518
+ this.targetElement = null;
519
+ this.hintTimers = [];
520
+ this.hintIndex = 0;
521
+ this.hintContainer = null;
522
+ this.shimmerStyle = null;
523
+ this.clickHandler = null;
524
+ this.fallbackButton = null;
525
+ this._injectedElement = null;
526
+ this._destroyed = false;
527
+ this.config = config;
528
+ this.script = script;
529
+ this.onFound = onFound;
530
+ }
531
+ start() {
532
+ if (this.config.accessibility.disableHiddenEntry) {
533
+ this._createFallbackEntry();
534
+ return;
535
+ }
536
+ const eligible = findEligibleEntryElements(
537
+ this.config.hiddenEntry.selector,
538
+ this.config.hiddenEntry.excludeSelectors
539
+ );
540
+ if (eligible.length === 0) {
541
+ this._createFallbackEntry();
542
+ return;
543
+ }
544
+ this._injectIntoExisting(eligible);
545
+ this._startHintEscalation();
546
+ }
547
+ cleanup() {
548
+ var _a, _b, _c, _d;
549
+ this._destroyed = true;
550
+ for (const t of this.hintTimers) clearTimeout(t);
551
+ this.hintTimers = [];
552
+ if (this.clickHandler && this.targetElement) {
553
+ this.targetElement.removeEventListener("click", this.clickHandler);
554
+ }
555
+ if (this.targetElement) {
556
+ this.targetElement.style.removeProperty("animation");
557
+ this.targetElement.classList.remove("eeq-entry-target");
558
+ }
559
+ (_a = this._injectedElement) == null ? void 0 : _a.remove();
560
+ this._injectedElement = null;
561
+ (_b = this.hintContainer) == null ? void 0 : _b.remove();
562
+ (_c = this.shimmerStyle) == null ? void 0 : _c.remove();
563
+ (_d = this.fallbackButton) == null ? void 0 : _d.remove();
564
+ }
565
+ // ─── Strategy 1: possess an existing element ──────────────────────────
566
+ _possessElement(eligible) {
567
+ this.targetElement = pickRandom(eligible);
568
+ this._attachToElement(this.targetElement);
569
+ }
570
+ // ─── Inject trigger text inside an existing element ─────────────────
571
+ _injectIntoExisting(_eligible) {
572
+ const candidates = Array.from(
573
+ document.querySelectorAll(
574
+ "p, li, figcaption, blockquote, h4, h3, h2, h1, header a, nav a, nav button"
575
+ )
576
+ ).filter((el) => {
577
+ var _a;
578
+ const text = ((_a = el.textContent) == null ? void 0 : _a.trim()) ?? "";
579
+ const rect = el.getBoundingClientRect();
580
+ return text.length >= 2 && rect.width > 0 && rect.height > 0 && !el.closest("[data-eeq]");
581
+ });
582
+ const sorted = candidates.sort((a, b) => {
583
+ const aNav = a.closest("nav") !== null || a.closest("header") !== null ? 1 : 0;
584
+ const bNav = b.closest("nav") !== null || b.closest("header") !== null ? 1 : 0;
585
+ if (aNav !== bNav) return aNav - bNav;
586
+ const aSize = parseFloat(getComputedStyle(a).fontSize);
587
+ const bSize = parseFloat(getComputedStyle(b).fontSize);
588
+ return aSize - bSize;
589
+ });
590
+ const prints = sorted.map(
591
+ (el) => {
592
+ var _a;
593
+ return `${el.tagName}:${(((_a = el.textContent) == null ? void 0 : _a.trim()) ?? "").slice(0, 30)}`;
594
+ }
595
+ );
596
+ const HISTORY_KEY = "eeq_trigger_history";
597
+ let usedList = [];
598
+ try {
599
+ const raw = localStorage.getItem(HISTORY_KEY);
600
+ if (raw) usedList = JSON.parse(raw);
601
+ } catch {
602
+ }
603
+ let usedSet = new Set(usedList);
604
+ const VISIT_KEY = "eeq_visit";
605
+ let visitCount = 0;
606
+ try {
607
+ visitCount = parseInt(localStorage.getItem(VISIT_KEY) || "0", 10) || 0;
608
+ localStorage.setItem(VISIT_KEY, String(visitCount + 1));
609
+ } catch {
610
+ }
611
+ const bodyEls = sorted.filter(
612
+ (el) => !el.closest("nav") && !el.closest("header")
613
+ );
614
+ const navEls = sorted.filter(
615
+ (el) => el.closest("nav") !== null || el.closest("header") !== null
616
+ );
617
+ const offset = bodyEls.length > 1 ? visitCount % bodyEls.length : 0;
618
+ const rotated = [
619
+ ...bodyEls.slice(offset),
620
+ ...bodyEls.slice(0, offset),
621
+ ...navEls
622
+ ];
623
+ const rotatedPrints = rotated.map(
624
+ (el) => {
625
+ var _a;
626
+ return `${el.tagName}:${(((_a = el.textContent) == null ? void 0 : _a.trim()) ?? "").slice(0, 30)}`;
627
+ }
628
+ );
629
+ const unused = rotated.filter((_, i) => !usedSet.has(rotatedPrints[i]));
630
+ if (unused.length === 0) {
631
+ usedSet.clear();
632
+ usedList = [];
633
+ }
634
+ const ordered = unused.length > 0 ? [...unused, ...rotated.filter((_, i) => usedSet.has(rotatedPrints[i]))] : rotated;
635
+ for (const candidate of ordered) {
636
+ const host = candidate;
637
+ const isInline = host.tagName === "A" || host.tagName === "BUTTON" || host.tagName === "LI" || host.closest("nav") !== null;
638
+ const span = document.createElement("span");
639
+ span.textContent = isInline ? " · start hunt" : " start hunt";
640
+ span.style.cssText = `
641
+ font: inherit;
642
+ color: inherit;
643
+ letter-spacing: inherit;
644
+ cursor: pointer;
645
+ `;
646
+ const inserted = !isInline && this._tryInsertBetweenWords(host, span);
647
+ if (!inserted) {
648
+ const heightBefore = host.getBoundingClientRect().height;
649
+ host.appendChild(span);
650
+ const heightAfter = host.getBoundingClientRect().height;
651
+ if (Math.abs(heightAfter - heightBefore) > 2) {
652
+ host.removeChild(span);
653
+ continue;
654
+ }
655
+ }
656
+ const idx = sorted.indexOf(candidate);
657
+ if (idx >= 0) {
658
+ usedSet.add(prints[idx]);
659
+ try {
660
+ localStorage.setItem(HISTORY_KEY, JSON.stringify([...usedSet].slice(-20)));
661
+ } catch {
662
+ }
663
+ }
664
+ this._injectedElement = span;
665
+ this.targetElement = span;
666
+ this._attachToElement(span);
667
+ return;
668
+ }
669
+ this._createFallbackEntry();
670
+ }
671
+ /**
672
+ * Try to insert the trigger span between words inside a text node.
673
+ * Picks a random word boundary in the element's first direct text node.
674
+ * Returns true if inserted without layout shift.
675
+ */
676
+ _tryInsertBetweenWords(host, span) {
677
+ var _a;
678
+ const textNodes = [];
679
+ for (const node of host.childNodes) {
680
+ if (node.nodeType === Node.TEXT_NODE && (((_a = node.textContent) == null ? void 0 : _a.trim().length) ?? 0) > 5) {
681
+ textNodes.push(node);
682
+ }
683
+ }
684
+ if (!textNodes.length) return false;
685
+ const slotSeed = Math.floor(Date.now() / (1e3 * 60 * 2));
686
+ const textNode = textNodes[slotSeed % textNodes.length];
687
+ const text = textNode.textContent ?? "";
688
+ const spacePositions = [];
689
+ for (let i = 1; i < text.length - 1; i++) {
690
+ if (text[i] === " ") spacePositions.push(i);
691
+ }
692
+ if (spacePositions.length < 2) return false;
693
+ const start = Math.floor(spacePositions.length * 0.25);
694
+ const end = Math.floor(spacePositions.length * 0.75);
695
+ const idx = start + slotSeed % Math.max(1, end - start);
696
+ const splitAt = spacePositions[Math.min(idx, spacePositions.length - 1)];
697
+ const before = text.slice(0, splitAt);
698
+ const after = text.slice(splitAt);
699
+ const heightBefore = host.getBoundingClientRect().height;
700
+ const afterNode = document.createTextNode(after);
701
+ textNode.textContent = before;
702
+ host.insertBefore(afterNode, textNode.nextSibling);
703
+ host.insertBefore(span, afterNode);
704
+ const heightAfter = host.getBoundingClientRect().height;
705
+ if (Math.abs(heightAfter - heightBefore) > 2) {
706
+ host.removeChild(span);
707
+ host.removeChild(afterNode);
708
+ textNode.textContent = text;
709
+ return false;
710
+ }
711
+ return true;
712
+ }
713
+ // ─── Shared ───────────────────────────────────────────────────────────
714
+ _attachToElement(el) {
715
+ this.clickHandler = (e) => {
716
+ e.preventDefault();
717
+ e.stopPropagation();
718
+ if (this._destroyed) return;
719
+ this.cleanup();
720
+ this.onFound();
721
+ };
722
+ el.addEventListener("click", this.clickHandler, {
723
+ capture: true,
724
+ once: true
725
+ });
726
+ }
727
+ _startHintEscalation() {
728
+ this._injectShimmerStyles();
729
+ this._createHintContainer();
730
+ const hints = this.script.hiddenEntryHints;
731
+ const FIRST_HINT_DELAY = 5e3;
732
+ const HINT_INTERVAL = 3e3;
733
+ for (let i = 0; i < hints.length; i++) {
734
+ const delay = FIRST_HINT_DELAY + i * HINT_INTERVAL;
735
+ const timer = setTimeout(() => {
736
+ if (this._destroyed) return;
737
+ this._showHint(hints[i]);
738
+ }, delay);
739
+ this.hintTimers.push(timer);
740
+ }
741
+ const shimmerDelay = 12e3;
742
+ const shimmerTimer = setTimeout(() => {
743
+ if (this._destroyed || !this.targetElement) return;
744
+ this.targetElement.classList.add("eeq-entry-target");
745
+ }, shimmerDelay);
746
+ this.hintTimers.push(shimmerTimer);
747
+ }
748
+ _showHint(text) {
749
+ if (!this.hintContainer) return;
750
+ const snack = document.createElement("div");
751
+ snack.className = "eeq-snackbar";
752
+ const msg = document.createElement("span");
753
+ msg.textContent = text;
754
+ snack.appendChild(msg);
755
+ const btn = document.createElement("button");
756
+ btn.className = "eeq-snackbar-btn";
757
+ btn.textContent = "Not interested";
758
+ btn.addEventListener("click", () => {
759
+ try {
760
+ localStorage.setItem("eeq_optout", "1");
761
+ } catch {
762
+ }
763
+ this.cleanup();
764
+ });
765
+ snack.appendChild(btn);
766
+ this.hintContainer.appendChild(snack);
767
+ const dismiss = () => {
768
+ snack.style.transform = "translateY(20px)";
769
+ snack.style.opacity = "0";
770
+ setTimeout(() => snack.remove(), 400);
771
+ };
772
+ requestAnimationFrame(() => {
773
+ snack.style.transform = "translateY(0)";
774
+ snack.style.opacity = "1";
775
+ });
776
+ setTimeout(() => {
777
+ if (snack.parentNode) dismiss();
778
+ }, 8e3);
779
+ }
780
+ _createHintContainer() {
781
+ this.hintContainer = document.createElement("div");
782
+ Object.assign(this.hintContainer.style, {
783
+ position: "fixed",
784
+ bottom: "24px",
785
+ left: "50%",
786
+ transform: "translateX(-50%)",
787
+ zIndex: "999980",
788
+ pointerEvents: "none",
789
+ display: "flex",
790
+ flexDirection: "column",
791
+ alignItems: "center",
792
+ gap: "8px"
793
+ });
794
+ document.body.appendChild(this.hintContainer);
795
+ }
796
+ _injectShimmerStyles() {
797
+ this.shimmerStyle = document.createElement("style");
798
+ this.shimmerStyle.textContent = `
799
+ .eeq-entry-target {
800
+ animation: eeq-shimmer 3s ease-in-out infinite !important;
801
+ }
802
+ @keyframes eeq-shimmer {
803
+ 0%, 100% { opacity: 1; }
804
+ 50% { opacity: 0.55; }
805
+ }
806
+ .eeq-snackbar {
807
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
808
+ font-size: 14px;
809
+ line-height: 20px;
810
+ letter-spacing: 0.01em;
811
+ color: #fff;
812
+ background: #323232;
813
+ padding: 14px 24px;
814
+ border-radius: 4px;
815
+ box-shadow: 0 3px 5px -1px rgba(0,0,0,0.2), 0 6px 10px rgba(0,0,0,0.14), 0 1px 18px rgba(0,0,0,0.12);
816
+ min-width: 288px;
817
+ max-width: 568px;
818
+ opacity: 0;
819
+ transform: translateY(20px);
820
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
821
+ pointer-events: auto;
822
+ display: flex;
823
+ align-items: center;
824
+ justify-content: space-between;
825
+ gap: 24px;
826
+ }
827
+ .eeq-snackbar-btn {
828
+ font-family: inherit;
829
+ font-size: 14px;
830
+ font-weight: 500;
831
+ letter-spacing: 0.04em;
832
+ text-transform: none;
833
+ color: #fff;
834
+ background: none;
835
+ border: none;
836
+ padding: 0;
837
+ margin: 0;
838
+ cursor: pointer;
839
+ white-space: nowrap;
840
+ flex-shrink: 0;
841
+ }
842
+ .eeq-snackbar-btn:hover {
843
+ opacity: 0.8;
844
+ }
845
+ `;
846
+ document.head.appendChild(this.shimmerStyle);
847
+ }
848
+ _createFallbackEntry() {
849
+ this.fallbackButton = document.createElement("button");
850
+ this.fallbackButton.textContent = "·";
851
+ this.fallbackButton.setAttribute("aria-label", "Hidden game entry");
852
+ Object.assign(this.fallbackButton.style, {
853
+ position: "fixed",
854
+ bottom: "16px",
855
+ right: "16px",
856
+ width: "28px",
857
+ height: "28px",
858
+ borderRadius: "50%",
859
+ border: `1px solid ${this.config.theme.accent}`,
860
+ background: "transparent",
861
+ color: this.config.theme.accent,
862
+ fontSize: "16px",
863
+ cursor: "pointer",
864
+ zIndex: "999980",
865
+ opacity: "0.4",
866
+ transition: "opacity 0.6s",
867
+ fontFamily: "serif",
868
+ lineHeight: "1",
869
+ padding: "0"
870
+ });
871
+ this.fallbackButton.addEventListener("mouseenter", () => {
872
+ if (this.fallbackButton) this.fallbackButton.style.opacity = "0.8";
873
+ });
874
+ this.fallbackButton.addEventListener("mouseleave", () => {
875
+ if (this.fallbackButton) this.fallbackButton.style.opacity = "0.4";
876
+ });
877
+ this.fallbackButton.addEventListener("click", () => {
878
+ if (this._destroyed) return;
879
+ this.cleanup();
880
+ this.onFound();
881
+ });
882
+ document.body.appendChild(this.fallbackButton);
883
+ this._createHintContainer();
884
+ this._injectShimmerStyles();
885
+ const hints = this.script.hiddenEntryHints;
886
+ for (let i = 0; i < hints.length; i++) {
887
+ const timer = setTimeout(
888
+ () => {
889
+ if (this._destroyed) return;
890
+ this._showHint(hints[i]);
891
+ },
892
+ 6e3 + i * 1e4
893
+ );
894
+ this.hintTimers.push(timer);
895
+ }
896
+ }
897
+ }
898
+ class NarrativeRenderer {
899
+ constructor(config) {
900
+ this.host = null;
901
+ this.shadow = null;
902
+ this.container = null;
903
+ this.currentLine = null;
904
+ this.clearTimer = null;
905
+ this.config = config;
906
+ }
907
+ mount() {
908
+ this.host = document.createElement("div");
909
+ this.host.id = "eeq-narrative-host";
910
+ Object.assign(this.host.style, {
911
+ position: "fixed",
912
+ top: "0",
913
+ left: "0",
914
+ width: "100%",
915
+ height: "100%",
916
+ pointerEvents: "none",
917
+ zIndex: "999990"
918
+ });
919
+ document.body.appendChild(this.host);
920
+ this.shadow = this.host.attachShadow({ mode: "closed" });
921
+ const style = document.createElement("style");
922
+ style.textContent = this._getStyles();
923
+ this.shadow.appendChild(style);
924
+ this.container = document.createElement("div");
925
+ this.container.className = "eeq-narrative";
926
+ this.shadow.appendChild(this.container);
927
+ }
928
+ /** Show a single narrative line. Fades in, replaces any previous line. */
929
+ showLine(text, className = "eeq-line") {
930
+ if (!this.container) return;
931
+ if (this.currentLine) {
932
+ const prev = this.currentLine;
933
+ prev.classList.add("eeq-narrative-exit");
934
+ setTimeout(() => prev.remove(), 750);
935
+ }
936
+ if (this.clearTimer) {
937
+ clearTimeout(this.clearTimer);
938
+ this.clearTimer = null;
939
+ }
940
+ const line = document.createElement("div");
941
+ line.className = className;
942
+ line.textContent = text;
943
+ this.container.appendChild(line);
944
+ requestAnimationFrame(() => {
945
+ requestAnimationFrame(() => {
946
+ line.classList.add("eeq-line-visible");
947
+ });
948
+ });
949
+ this.currentLine = line;
950
+ this.clearTimer = setTimeout(() => {
951
+ this.clear();
952
+ }, 5e3);
953
+ }
954
+ /** Show a sequence of lines with pauses. */
955
+ async showSequence(lines, pauseMs = 1500) {
956
+ for (const line of lines) {
957
+ this.showLine(line);
958
+ await new Promise((r) => setTimeout(r, pauseMs));
959
+ }
960
+ }
961
+ /** Show a celebratory line with golden styling. */
962
+ showCelebration(text) {
963
+ this.showLine(text, "eeq-line eeq-celebration");
964
+ }
965
+ /** Clear current narrative text. */
966
+ clear() {
967
+ if (this.clearTimer) {
968
+ clearTimeout(this.clearTimer);
969
+ this.clearTimer = null;
970
+ }
971
+ if (this.currentLine) {
972
+ this.currentLine.classList.add("eeq-narrative-exit");
973
+ const ref = this.currentLine;
974
+ setTimeout(() => ref.remove(), 750);
975
+ this.currentLine = null;
976
+ }
977
+ }
978
+ /** Remove all DOM. */
979
+ destroy() {
980
+ var _a;
981
+ this.clear();
982
+ (_a = this.host) == null ? void 0 : _a.remove();
983
+ this.host = null;
984
+ this.shadow = null;
985
+ this.container = null;
986
+ }
987
+ _getStyles() {
988
+ const t = this.config.theme;
989
+ return `
990
+ .eeq-narrative {
991
+ position: fixed;
992
+ bottom: 0;
993
+ left: 0;
994
+ width: 100%;
995
+ height: auto;
996
+ display: flex;
997
+ align-items: center;
998
+ justify-content: center;
999
+ padding-bottom: 48px;
1000
+ pointer-events: none;
1001
+ font-family: 'Georgia', 'Times New Roman', 'Noto Serif', serif;
1002
+ }
1003
+
1004
+ .eeq-line {
1005
+ position: absolute;
1006
+ bottom: 48px;
1007
+ left: 50%;
1008
+ transform: translateX(-50%);
1009
+ color: ${t.textColor};
1010
+ font-size: clamp(13px, 1.8vw, 19px);
1011
+ letter-spacing: 0.04em;
1012
+ text-align: center;
1013
+ max-width: 440px;
1014
+ padding: 12px 24px;
1015
+ opacity: 0;
1016
+ filter: blur(12px);
1017
+ transition: opacity 0.8s ease, filter 0.9s cubic-bezier(0.22, 1, 0.36, 1);
1018
+ line-height: 1.6;
1019
+
1020
+ /* Frosted glass dialogue card */
1021
+ background: rgba(10, 10, 14, 0.55);
1022
+ backdrop-filter: blur(18px) saturate(1.4);
1023
+ -webkit-backdrop-filter: blur(18px) saturate(1.4);
1024
+ border: 1px solid rgba(255, 255, 255, 0.08);
1025
+ border-radius: 16px;
1026
+ box-shadow:
1027
+ 0 4px 24px rgba(0, 0, 0, 0.25),
1028
+ 0 0 0 0.5px rgba(255, 255, 255, 0.04) inset;
1029
+ }
1030
+
1031
+ .eeq-line-visible {
1032
+ opacity: 1;
1033
+ filter: blur(0px);
1034
+ }
1035
+
1036
+ .eeq-narrative-exit {
1037
+ opacity: 0 !important;
1038
+ filter: blur(14px) !important;
1039
+ transition: opacity 0.6s ease, filter 0.7s cubic-bezier(0.22, 1, 0.36, 1) !important;
1040
+ }
1041
+
1042
+ .eeq-celebration {
1043
+ /* Override positioning: center of viewport via fixed */
1044
+ position: fixed !important;
1045
+ bottom: auto !important;
1046
+ top: 50%;
1047
+ left: 50%;
1048
+ transform: translate(-50%, -50%) scale(0.8);
1049
+ max-width: 90vw;
1050
+ padding: 20px 40px;
1051
+
1052
+ color: #ffe8a0;
1053
+ font-size: clamp(18px, 3.5vw, 28px);
1054
+ font-weight: bold;
1055
+ letter-spacing: 0.1em;
1056
+ text-transform: uppercase;
1057
+
1058
+ background: linear-gradient(135deg, rgba(50,35,10,0.85), rgba(25,18,8,0.9));
1059
+ border: 2px solid rgba(255, 215, 80, 0.35);
1060
+ border-radius: 20px;
1061
+ box-shadow:
1062
+ 0 0 60px rgba(255, 200, 60, 0.25),
1063
+ 0 8px 40px rgba(0, 0, 0, 0.5),
1064
+ inset 0 1px 0 rgba(255, 230, 140, 0.15);
1065
+ text-shadow:
1066
+ 0 0 20px rgba(255, 200, 60, 0.6),
1067
+ 0 0 40px rgba(255, 180, 40, 0.3);
1068
+ }
1069
+
1070
+ .eeq-celebration.eeq-line-visible {
1071
+ transform: translate(-50%, -50%) scale(1);
1072
+ transition: opacity 0.6s ease, filter 0.8s ease, transform 0.6s cubic-bezier(0.17, 0.67, 0.29, 1.2);
1073
+ }
1074
+
1075
+ .eeq-celebration.eeq-narrative-exit {
1076
+ transform: translate(-50%, -50%) scale(1.1) !important;
1077
+ }
1078
+ `;
1079
+ }
1080
+ }
1081
+ class ShrineRenderer {
1082
+ constructor(config) {
1083
+ this.host = null;
1084
+ this.shadow = null;
1085
+ this.slots = [];
1086
+ this._activeSlot = null;
1087
+ this._onEggClick = null;
1088
+ this.config = config;
1089
+ }
1090
+ mount(onEggClick) {
1091
+ if (!this.config.shrine.enabled) return;
1092
+ this._onEggClick = onEggClick;
1093
+ this.host = document.createElement("div");
1094
+ this.host.id = "eeq-shrine-host";
1095
+ const pos = this.config.shrine.position;
1096
+ const posStyles = {
1097
+ "bottom-right": { bottom: "16px", right: "16px" },
1098
+ "bottom-left": { bottom: "16px", left: "16px" },
1099
+ "bottom-center": { bottom: "16px", left: "50%", transform: "translateX(-50%)" }
1100
+ };
1101
+ Object.assign(this.host.style, {
1102
+ position: "fixed",
1103
+ zIndex: "999994",
1104
+ opacity: "0",
1105
+ transform: "translateY(20px)",
1106
+ transition: "opacity 0.8s ease, transform 0.8s ease",
1107
+ ...posStyles[pos] ?? posStyles["bottom-right"]
1108
+ });
1109
+ document.body.appendChild(this.host);
1110
+ this.shadow = this.host.attachShadow({ mode: "closed" });
1111
+ const style = document.createElement("style");
1112
+ style.textContent = this._getStyles();
1113
+ this.shadow.appendChild(style);
1114
+ const container = document.createElement("div");
1115
+ container.className = "eeq-shrine";
1116
+ for (let i = 0; i < 3; i++) {
1117
+ const slot = document.createElement("div");
1118
+ slot.className = "eeq-slot eeq-slot-empty";
1119
+ slot.setAttribute("data-egg", String(i));
1120
+ slot.setAttribute("aria-label", `Egg ${i + 1} slot`);
1121
+ slot.setAttribute("role", "button");
1122
+ slot.setAttribute("tabindex", "0");
1123
+ const inner = document.createElement("div");
1124
+ inner.className = "eeq-egg-inner";
1125
+ slot.appendChild(inner);
1126
+ slot.addEventListener("click", () => {
1127
+ var _a;
1128
+ if (this._activeSlot === i) {
1129
+ (_a = this._onEggClick) == null ? void 0 : _a.call(this, i);
1130
+ }
1131
+ });
1132
+ slot.addEventListener("keydown", (e) => {
1133
+ var _a;
1134
+ if (e.key === "Enter" && this._activeSlot === i) {
1135
+ (_a = this._onEggClick) == null ? void 0 : _a.call(this, i);
1136
+ }
1137
+ });
1138
+ container.appendChild(slot);
1139
+ this.slots.push(slot);
1140
+ }
1141
+ this.shadow.appendChild(container);
1142
+ }
1143
+ /** Fade in the shrine. Call after entry is found. */
1144
+ show() {
1145
+ if (!this.host) return;
1146
+ requestAnimationFrame(() => {
1147
+ if (!this.host) return;
1148
+ this.host.style.opacity = "1";
1149
+ this.host.style.transform = "translateY(0)";
1150
+ });
1151
+ }
1152
+ /** Mark an egg slot as found (filled). */
1153
+ fillSlot(index) {
1154
+ const slot = this.slots[index];
1155
+ if (!slot) return;
1156
+ slot.classList.remove("eeq-slot-empty");
1157
+ slot.classList.add("eeq-slot-filled");
1158
+ }
1159
+ /** Activate a slot for click-to-continue (pulsing). */
1160
+ activateSlot(index) {
1161
+ this._activeSlot = index;
1162
+ const slot = this.slots[index];
1163
+ if (!slot) return;
1164
+ slot.classList.add("eeq-slot-active");
1165
+ slot.style.cursor = "pointer";
1166
+ slot.style.pointerEvents = "auto";
1167
+ }
1168
+ /** Deactivate the active slot. */
1169
+ deactivateSlot(index) {
1170
+ this._activeSlot = null;
1171
+ const slot = this.slots[index];
1172
+ if (!slot) return;
1173
+ slot.classList.remove("eeq-slot-active");
1174
+ slot.style.cursor = "default";
1175
+ slot.style.pointerEvents = "none";
1176
+ }
1177
+ /** Animate all eggs leaving the shrine (for finale). */
1178
+ hideAll() {
1179
+ for (const slot of this.slots) {
1180
+ slot.classList.add("eeq-slot-finale-exit");
1181
+ }
1182
+ }
1183
+ /** Show all slots again (for reset). */
1184
+ reset() {
1185
+ this._activeSlot = null;
1186
+ for (const slot of this.slots) {
1187
+ slot.className = "eeq-slot eeq-slot-empty";
1188
+ slot.style.cursor = "default";
1189
+ slot.style.pointerEvents = "none";
1190
+ }
1191
+ }
1192
+ destroy() {
1193
+ var _a;
1194
+ (_a = this.host) == null ? void 0 : _a.remove();
1195
+ this.host = null;
1196
+ this.shadow = null;
1197
+ this.slots = [];
1198
+ }
1199
+ _getStyles() {
1200
+ const t = this.config.theme;
1201
+ return `
1202
+ .eeq-shrine {
1203
+ display: flex;
1204
+ gap: 14px;
1205
+ padding: 12px 16px;
1206
+ background: ${t.hudBg};
1207
+ backdrop-filter: blur(12px);
1208
+ -webkit-backdrop-filter: blur(12px);
1209
+ border: 1px solid rgba(255,255,255,0.08);
1210
+ border-radius: 12px;
1211
+ }
1212
+
1213
+ .eeq-slot {
1214
+ width: 36px;
1215
+ height: 44px;
1216
+ border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
1217
+ display: flex;
1218
+ align-items: center;
1219
+ justify-content: center;
1220
+ transition: all 0.6s ease;
1221
+ pointer-events: none;
1222
+ position: relative;
1223
+ }
1224
+
1225
+ .eeq-slot-empty {
1226
+ border: 1.5px dashed rgba(255,255,255,0.2);
1227
+ background: transparent;
1228
+ }
1229
+
1230
+ .eeq-slot-filled {
1231
+ border: 1.5px solid ${t.accent};
1232
+ background: radial-gradient(ellipse at 40% 30%, rgba(255,255,255,0.15), transparent);
1233
+ }
1234
+
1235
+ .eeq-slot-filled .eeq-egg-inner {
1236
+ width: 60%;
1237
+ height: 70%;
1238
+ border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
1239
+ background: linear-gradient(135deg, ${t.accent}, rgba(255,255,255,0.3));
1240
+ opacity: 0.8;
1241
+ }
1242
+
1243
+ .eeq-slot-active {
1244
+ animation: eeq-slot-pulse 2s ease-in-out infinite;
1245
+ border-color: ${t.accent} !important;
1246
+ box-shadow: 0 0 16px ${t.accentGlow};
1247
+ }
1248
+
1249
+ .eeq-slot-finale-exit {
1250
+ opacity: 0;
1251
+ transform: translateY(-20px) scale(0.6);
1252
+ transition: all 1s ease;
1253
+ }
1254
+
1255
+ .eeq-egg-inner {
1256
+ transition: all 0.4s ease;
1257
+ }
1258
+
1259
+ @keyframes eeq-slot-pulse {
1260
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
1261
+ 50% { transform: scale(1.08); opacity: 1; }
1262
+ }
1263
+ `;
1264
+ }
1265
+ }
1266
+ class ThreeRenderer {
1267
+ constructor(config) {
1268
+ this.canvas = null;
1269
+ this.THREE = null;
1270
+ this.renderer = null;
1271
+ this.scene = null;
1272
+ this.camera = null;
1273
+ this.eggs = [];
1274
+ this.eggBodies = [];
1275
+ this.lights = [];
1276
+ this._raf = 0;
1277
+ this._running = false;
1278
+ this._clock = null;
1279
+ this._eggStates = [
1280
+ { visible: false, phase: "hidden", t: 0, targetPos: [0, 0, 0] },
1281
+ { visible: false, phase: "hidden", t: 0, targetPos: [0, 0, 0] },
1282
+ { visible: false, phase: "hidden", t: 0, targetPos: [0, 0, 0] }
1283
+ ];
1284
+ this._finaleActive = false;
1285
+ this._finaleT = 0;
1286
+ this._greetingOverlay = null;
1287
+ this._trailParticles = [];
1288
+ this._ambientParticles = [];
1289
+ this._revealParticles = [];
1290
+ this._disposed = false;
1291
+ this._interactiveEgg = null;
1292
+ this._isDragging = false;
1293
+ this._dragStartX = 0;
1294
+ this._dragStartY = 0;
1295
+ this._dragRotX = 0;
1296
+ this._dragRotY = 0;
1297
+ this._dragMoved = false;
1298
+ this._longPressStartTime = 0;
1299
+ this._longPressActive = false;
1300
+ this._holdProgress = 0;
1301
+ this._holdRing = null;
1302
+ this._holdShockwave = null;
1303
+ this._miniEggs = [];
1304
+ this._collectLight = null;
1305
+ this._onCollectEgg = null;
1306
+ this._vortexParticles = [];
1307
+ this._emberParticles = [];
1308
+ this._collectFlashOpacity = 0;
1309
+ this._collectShake = 0;
1310
+ this._collectCamBaseZ = 5;
1311
+ this._onPointerDown = (e) => {
1312
+ if (this._interactiveEgg === null || this._disposed) return;
1313
+ const egg = this.eggs[this._interactiveEgg];
1314
+ if (!egg || !egg.visible) return;
1315
+ const eggScreen = this._worldToScreen(egg.position);
1316
+ const dx = e.clientX - eggScreen.x;
1317
+ const dy = e.clientY - eggScreen.y;
1318
+ const dist = Math.sqrt(dx * dx + dy * dy);
1319
+ if (dist > 120) return;
1320
+ e.preventDefault();
1321
+ e.stopPropagation();
1322
+ if (this.canvas) document.body.style.cursor = "grabbing";
1323
+ this._isDragging = true;
1324
+ this._dragMoved = false;
1325
+ this._dragStartX = e.clientX;
1326
+ this._dragStartY = e.clientY;
1327
+ this._dragRotX = egg.rotation.x;
1328
+ this._dragRotY = egg.rotation.y;
1329
+ this._longPressStartTime = Date.now();
1330
+ this._longPressActive = true;
1331
+ this._holdProgress = 0;
1332
+ };
1333
+ this._onPointerMove = (e) => {
1334
+ if (!this._isDragging || this._interactiveEgg === null) return;
1335
+ e.preventDefault();
1336
+ e.stopPropagation();
1337
+ const egg = this.eggs[this._interactiveEgg];
1338
+ if (!egg) return;
1339
+ const dx = e.clientX - this._dragStartX;
1340
+ const dy = e.clientY - this._dragStartY;
1341
+ egg.rotation.y = this._dragRotY + dx * 8e-3;
1342
+ egg.rotation.x = this._dragRotX + dy * 5e-3;
1343
+ if (Math.abs(dx) > 15 || Math.abs(dy) > 15) {
1344
+ this._dragMoved = true;
1345
+ this._cancelLongPress();
1346
+ }
1347
+ };
1348
+ this._onPointerUp = (e) => {
1349
+ if (this._isDragging) {
1350
+ e.preventDefault();
1351
+ e.stopPropagation();
1352
+ }
1353
+ this._isDragging = false;
1354
+ this._cancelLongPress();
1355
+ if (this._interactiveEgg !== null) {
1356
+ document.body.style.cursor = "grab";
1357
+ }
1358
+ };
1359
+ this._onWheel = (e) => {
1360
+ if (this._interactiveEgg === null || this._disposed) return;
1361
+ const egg = this.eggs[this._interactiveEgg];
1362
+ if (!egg || !egg.visible) return;
1363
+ const eggScreen = this._worldToScreen(egg.position);
1364
+ const dx = e.clientX - eggScreen.x;
1365
+ const dy = e.clientY - eggScreen.y;
1366
+ if (Math.sqrt(dx * dx + dy * dy) <= 120) {
1367
+ e.preventDefault();
1368
+ e.stopPropagation();
1369
+ }
1370
+ };
1371
+ this._onClickCapture = (e) => {
1372
+ if (this._interactiveEgg === null || this._disposed) return;
1373
+ const egg = this.eggs[this._interactiveEgg];
1374
+ if (!egg || !egg.visible) return;
1375
+ const eggScreen = this._worldToScreen(egg.position);
1376
+ const dx = e.clientX - eggScreen.x;
1377
+ const dy = e.clientY - eggScreen.y;
1378
+ if (Math.sqrt(dx * dx + dy * dy) <= 120) {
1379
+ e.preventDefault();
1380
+ e.stopPropagation();
1381
+ }
1382
+ };
1383
+ this._onMouseDownCapture = (e) => {
1384
+ if (this._interactiveEgg === null || this._disposed) return;
1385
+ const egg = this.eggs[this._interactiveEgg];
1386
+ if (!egg || !egg.visible) return;
1387
+ const eggScreen = this._worldToScreen(egg.position);
1388
+ const dx = e.clientX - eggScreen.x;
1389
+ const dy = e.clientY - eggScreen.y;
1390
+ if (Math.sqrt(dx * dx + dy * dy) <= 120) {
1391
+ e.preventDefault();
1392
+ e.stopPropagation();
1393
+ }
1394
+ };
1395
+ this._onMouseUpCapture = (e) => {
1396
+ if (this._isDragging) {
1397
+ e.preventDefault();
1398
+ e.stopPropagation();
1399
+ }
1400
+ };
1401
+ this._onTouchStartCapture = (e) => {
1402
+ if (this._interactiveEgg === null || this._disposed) return;
1403
+ const egg = this.eggs[this._interactiveEgg];
1404
+ if (!egg || !egg.visible) return;
1405
+ const touch = e.touches[0];
1406
+ if (!touch) return;
1407
+ const eggScreen = this._worldToScreen(egg.position);
1408
+ const dx = touch.clientX - eggScreen.x;
1409
+ const dy = touch.clientY - eggScreen.y;
1410
+ if (Math.sqrt(dx * dx + dy * dy) <= 120) {
1411
+ e.preventDefault();
1412
+ e.stopPropagation();
1413
+ }
1414
+ };
1415
+ this._onTouchMoveCapture = (e) => {
1416
+ if (this._isDragging) {
1417
+ e.preventDefault();
1418
+ e.stopPropagation();
1419
+ }
1420
+ };
1421
+ this._animate = () => {
1422
+ if (!this._running || this._disposed) return;
1423
+ this._raf = requestAnimationFrame(this._animate);
1424
+ const dt = Math.min(this._clock.getDelta(), 0.05);
1425
+ this._updateEggs(dt);
1426
+ this._updateHoldRing();
1427
+ this._updateMiniEggs(dt);
1428
+ this._updateTrails(dt);
1429
+ this._updateAmbientParticles(dt);
1430
+ this._updateRevealParticles(dt);
1431
+ this._updateFinale(dt);
1432
+ this.renderer.render(this.scene, this.camera);
1433
+ };
1434
+ this._onResize = () => {
1435
+ if (!this.camera || !this.renderer || !this.canvas) return;
1436
+ this.camera.aspect = window.innerWidth / window.innerHeight;
1437
+ this.camera.updateProjectionMatrix();
1438
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1439
+ };
1440
+ this.config = config;
1441
+ this._reducedMotion = config.accessibility.reducedMotion;
1442
+ }
1443
+ /** Set callback for when user completes long-press collect. */
1444
+ onCollectEgg(cb) {
1445
+ this._onCollectEgg = cb;
1446
+ }
1447
+ async init() {
1448
+ try {
1449
+ const globalThree = typeof window !== "undefined" && window.THREE;
1450
+ if (globalThree) {
1451
+ this.THREE = globalThree;
1452
+ } else {
1453
+ this.THREE = await import("./three.module-BYIS7JD4.mjs");
1454
+ }
1455
+ } catch {
1456
+ return false;
1457
+ }
1458
+ const T = this.THREE;
1459
+ this.canvas = document.createElement("canvas");
1460
+ this.canvas.id = "eeq-three-canvas";
1461
+ Object.assign(this.canvas.style, {
1462
+ position: "fixed",
1463
+ top: "0",
1464
+ left: "0",
1465
+ width: "100%",
1466
+ height: "100%",
1467
+ pointerEvents: "none",
1468
+ zIndex: "999992"
1469
+ });
1470
+ document.body.appendChild(this.canvas);
1471
+ this.renderer = new T.WebGLRenderer({
1472
+ canvas: this.canvas,
1473
+ alpha: true,
1474
+ antialias: true,
1475
+ powerPreference: "high-performance"
1476
+ });
1477
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1478
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1479
+ this.renderer.toneMapping = T.ACESFilmicToneMapping;
1480
+ this.renderer.toneMappingExposure = 1.1;
1481
+ this.scene = new T.Scene();
1482
+ this.camera = new T.PerspectiveCamera(
1483
+ 45,
1484
+ window.innerWidth / window.innerHeight,
1485
+ 0.1,
1486
+ 100
1487
+ );
1488
+ this.camera.position.set(0, 0, 5);
1489
+ this._setupLights(T);
1490
+ this._setupEnvironment(T);
1491
+ this._createEggs(T);
1492
+ this._createHoldRing(T);
1493
+ this._clock = new T.Clock();
1494
+ window.addEventListener("resize", this._onResize);
1495
+ document.addEventListener("pointerdown", this._onPointerDown, { capture: true });
1496
+ document.addEventListener("pointermove", this._onPointerMove, { capture: true });
1497
+ document.addEventListener("pointerup", this._onPointerUp, { capture: true });
1498
+ document.addEventListener("wheel", this._onWheel, { capture: true, passive: false });
1499
+ document.addEventListener("click", this._onClickCapture, { capture: true });
1500
+ document.addEventListener("mousedown", this._onMouseDownCapture, { capture: true });
1501
+ document.addEventListener("mouseup", this._onMouseUpCapture, { capture: true });
1502
+ document.addEventListener("touchstart", this._onTouchStartCapture, { capture: true, passive: false });
1503
+ document.addEventListener("touchmove", this._onTouchMoveCapture, { capture: true, passive: false });
1504
+ return true;
1505
+ }
1506
+ startRenderLoop() {
1507
+ if (this._running) return;
1508
+ this._running = true;
1509
+ this._animate();
1510
+ }
1511
+ stopRenderLoop() {
1512
+ this._running = false;
1513
+ if (this._raf) cancelAnimationFrame(this._raf);
1514
+ }
1515
+ /** Make an egg interactively draggable. */
1516
+ enableInteraction(index) {
1517
+ this._interactiveEgg = index;
1518
+ document.body.style.cursor = "grab";
1519
+ }
1520
+ /** Disable drag interaction. */
1521
+ disableInteraction() {
1522
+ this._interactiveEgg = null;
1523
+ this._isDragging = false;
1524
+ this._cancelLongPress();
1525
+ document.body.style.cursor = "";
1526
+ }
1527
+ /** Trigger spectacular egg reveal. */
1528
+ revealEgg(index) {
1529
+ const state = this._eggStates[index];
1530
+ if (!state) return;
1531
+ state.visible = true;
1532
+ state.phase = "reveal";
1533
+ state.t = 0;
1534
+ const group = this.eggs[index];
1535
+ if (group) {
1536
+ group.visible = true;
1537
+ group.scale.set(0.01, 0.01, 0.01);
1538
+ group.position.set(0, 0, 0);
1539
+ }
1540
+ this._spawnRevealBurst(index);
1541
+ this.enableInteraction(index);
1542
+ }
1543
+ /** Collect egg — hide immediately (mini-egg burst already happened during hold). */
1544
+ collectEgg(index) {
1545
+ const state = this._eggStates[index];
1546
+ if (!state) return;
1547
+ const group = this.eggs[index];
1548
+ if (group) group.visible = false;
1549
+ const body = this.eggBodies[index];
1550
+ if (body == null ? void 0 : body.material) {
1551
+ body.material.emissiveIntensity = 0.05;
1552
+ body.material.opacity = 1;
1553
+ body.material.transparent = false;
1554
+ }
1555
+ this.disableInteraction();
1556
+ state.phase = "shrine";
1557
+ state.t = 0;
1558
+ }
1559
+ /** Reset renderer state for game restart (preserves WebGL context). */
1560
+ resetForRestart() {
1561
+ var _a, _b, _c, _d, _e, _f, _g;
1562
+ (_a = this._greetingOverlay) == null ? void 0 : _a.remove();
1563
+ this._greetingOverlay = null;
1564
+ this.stopRenderLoop();
1565
+ for (let i = 0; i < 3; i++) {
1566
+ this._eggStates[i] = { visible: false, phase: "hidden", t: 0, targetPos: [0, 0, 0] };
1567
+ if (this.eggs[i]) {
1568
+ this.eggs[i].visible = false;
1569
+ this.eggs[i].scale.set(1, 1, 1);
1570
+ this.eggs[i].position.set(0, 0, 0);
1571
+ this.eggs[i].rotation.set(0, 0, 0);
1572
+ }
1573
+ const body = this.eggBodies[i];
1574
+ if (body == null ? void 0 : body.material) {
1575
+ body.material.emissiveIntensity = 0.05;
1576
+ body.material.opacity = 1;
1577
+ body.material.transparent = false;
1578
+ }
1579
+ }
1580
+ this._finaleActive = false;
1581
+ this._finaleT = 0;
1582
+ this.disableInteraction();
1583
+ for (const p of [...this._trailParticles, ...this._ambientParticles, ...this._revealParticles, ...this._vortexParticles, ...this._emberParticles]) {
1584
+ (_b = this.scene) == null ? void 0 : _b.remove(p.mesh);
1585
+ (_c = p.mesh.geometry) == null ? void 0 : _c.dispose();
1586
+ (_d = p.mesh.material) == null ? void 0 : _d.dispose();
1587
+ }
1588
+ this._trailParticles = [];
1589
+ this._ambientParticles = [];
1590
+ this._revealParticles = [];
1591
+ this._vortexParticles = [];
1592
+ this._emberParticles = [];
1593
+ for (const p of this._miniEggs) {
1594
+ (_e = this.scene) == null ? void 0 : _e.remove(p.mesh);
1595
+ (_f = p.mesh.geometry) == null ? void 0 : _f.dispose();
1596
+ (_g = p.mesh.material) == null ? void 0 : _g.dispose();
1597
+ }
1598
+ this._miniEggs = [];
1599
+ }
1600
+ /** Add a luminous motion trail at screen coordinates. */
1601
+ addTrail(x, y, velocity) {
1602
+ if (!this.THREE || !this.scene) return;
1603
+ const T = this.THREE;
1604
+ const worldPos = this._screenToWorld(x, y);
1605
+ const size = Math.min(0.05, velocity * 3e-5);
1606
+ const geo = this._createMiniEggGeo(T, size);
1607
+ const mat = this._createMiniEggMat(T, 0.65);
1608
+ const particle = new T.Mesh(geo, mat);
1609
+ particle.position.copy(worldPos);
1610
+ particle.rotation.set(
1611
+ Math.random() * Math.PI,
1612
+ Math.random() * Math.PI,
1613
+ Math.random() * Math.PI
1614
+ );
1615
+ this.scene.add(particle);
1616
+ this._trailParticles.push({ mesh: particle, life: 1 });
1617
+ if (this._trailParticles.length > 100) {
1618
+ const old = this._trailParticles.shift();
1619
+ if (old) {
1620
+ this.scene.remove(old.mesh);
1621
+ old.mesh.geometry.dispose();
1622
+ old.mesh.material.dispose();
1623
+ }
1624
+ }
1625
+ }
1626
+ /** Spawn gentle ambient particles to show stage progress. */
1627
+ showProgressParticles(progress) {
1628
+ if (!this.THREE || !this.scene || progress < 0.05) return;
1629
+ const T = this.THREE;
1630
+ if (Math.random() > progress * 0.3) return;
1631
+ const angle = Math.random() * Math.PI * 2;
1632
+ const radius = 2 + Math.random() * 2;
1633
+ const x = Math.cos(angle) * radius;
1634
+ const y = -2 + Math.random() * 4;
1635
+ const size = 0.02 + Math.random() * 0.025;
1636
+ const geo = this._createMiniEggGeo(T, size);
1637
+ const mat = this._createMiniEggMat(T, 0);
1638
+ const p = new T.Mesh(geo, mat);
1639
+ p.position.set(x, y, -1 + Math.random() * 0.5);
1640
+ p.rotation.set(
1641
+ Math.random() * Math.PI,
1642
+ Math.random() * Math.PI,
1643
+ Math.random() * Math.PI
1644
+ );
1645
+ this.scene.add(p);
1646
+ this._ambientParticles.push({
1647
+ mesh: p,
1648
+ life: 1,
1649
+ vy: 0.2 + Math.random() * 0.3,
1650
+ fadeIn: true
1651
+ });
1652
+ if (this._ambientParticles.length > 60) {
1653
+ const old = this._ambientParticles.shift();
1654
+ if (old) {
1655
+ this.scene.remove(old.mesh);
1656
+ old.mesh.geometry.dispose();
1657
+ old.mesh.material.dispose();
1658
+ }
1659
+ }
1660
+ }
1661
+ /** Start the finale composition. */
1662
+ startFinale() {
1663
+ this._finaleActive = true;
1664
+ this._finaleT = 0;
1665
+ this.disableInteraction();
1666
+ for (let i = 0; i < 3; i++) {
1667
+ const state = this._eggStates[i];
1668
+ state.phase = "finale";
1669
+ state.t = 0;
1670
+ if (this.eggs[i]) this.eggs[i].visible = true;
1671
+ }
1672
+ this._greetingOverlay = document.createElement("div");
1673
+ Object.assign(this._greetingOverlay.style, {
1674
+ position: "fixed",
1675
+ bottom: "12%",
1676
+ left: "0",
1677
+ width: "100%",
1678
+ display: "flex",
1679
+ flexDirection: "column",
1680
+ alignItems: "center",
1681
+ justifyContent: "center",
1682
+ zIndex: "999997",
1683
+ pointerEvents: "none",
1684
+ opacity: "0",
1685
+ transition: "opacity 2s ease",
1686
+ fontFamily: "'Georgia', 'Times New Roman', serif",
1687
+ textAlign: "center"
1688
+ });
1689
+ this._greetingOverlay.innerHTML = `
1690
+ <div style="font-size: 48px; color: #fff8e8; text-shadow: 0 0 60px rgba(212,165,80,0.7), 0 0 20px rgba(212,165,80,0.4), 0 2px 8px rgba(0,0,0,0.6); margin-bottom: 10px; letter-spacing: 0.03em; font-weight: bold;">
1691
+ Happy Easter! 🐰🥚
1692
+ </div>
1693
+ <div style="font-size: 17px; color: rgba(245,240,230,0.85); font-style: italic; text-shadow: 0 0 12px rgba(212,165,80,0.3), 0 1px 6px rgba(0,0,0,0.4);">
1694
+ Wishing you joy, peace, and light
1695
+ </div>
1696
+ `;
1697
+ document.body.appendChild(this._greetingOverlay);
1698
+ requestAnimationFrame(() => {
1699
+ requestAnimationFrame(() => {
1700
+ if (this._greetingOverlay) this._greetingOverlay.style.opacity = "1";
1701
+ });
1702
+ });
1703
+ }
1704
+ /** Re-draw the festive (3rd) egg texture with a specific seed for uniqueness. */
1705
+ setFestiveEggSeed(seed) {
1706
+ var _a;
1707
+ const body = this.eggBodies[2];
1708
+ if (!body || !this.THREE) return;
1709
+ const T = this.THREE;
1710
+ const newTex = new T.CanvasTexture(this._drawFestivePattern(seed));
1711
+ newTex.colorSpace = T.SRGBColorSpace;
1712
+ newTex.wrapS = T.RepeatWrapping;
1713
+ (_a = body.material.map) == null ? void 0 : _a.dispose();
1714
+ body.material.map = newTex;
1715
+ body.material.needsUpdate = true;
1716
+ }
1717
+ setBreathIntensity(intensity) {
1718
+ if (!this.camera) return;
1719
+ const breathScale = 0.04 * intensity;
1720
+ const time = Date.now() * 1e-3;
1721
+ this.camera.position.z = 5 + Math.sin(time * 0.8) * breathScale;
1722
+ }
1723
+ setOverlayIntensity(_progress) {
1724
+ }
1725
+ destroy() {
1726
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
1727
+ this._disposed = true;
1728
+ this.stopRenderLoop();
1729
+ this.disableInteraction();
1730
+ window.removeEventListener("resize", this._onResize);
1731
+ document.removeEventListener("pointerdown", this._onPointerDown, { capture: true });
1732
+ document.removeEventListener("pointermove", this._onPointerMove, { capture: true });
1733
+ document.removeEventListener("pointerup", this._onPointerUp, { capture: true });
1734
+ document.removeEventListener("wheel", this._onWheel, { capture: true });
1735
+ document.removeEventListener("click", this._onClickCapture, { capture: true });
1736
+ document.removeEventListener("mousedown", this._onMouseDownCapture, { capture: true });
1737
+ document.removeEventListener("mouseup", this._onMouseUpCapture, { capture: true });
1738
+ document.removeEventListener("touchstart", this._onTouchStartCapture, { capture: true });
1739
+ document.removeEventListener("touchmove", this._onTouchMoveCapture, { capture: true });
1740
+ document.body.style.cursor = "";
1741
+ const disposeList = [
1742
+ ...this._trailParticles,
1743
+ ...this._ambientParticles,
1744
+ ...this._revealParticles,
1745
+ ...this._vortexParticles,
1746
+ ...this._emberParticles
1747
+ ];
1748
+ for (const p of disposeList) {
1749
+ (_a = this.scene) == null ? void 0 : _a.remove(p.mesh);
1750
+ (_b = p.mesh.geometry) == null ? void 0 : _b.dispose();
1751
+ (_c = p.mesh.material) == null ? void 0 : _c.dispose();
1752
+ }
1753
+ this._trailParticles = [];
1754
+ this._ambientParticles = [];
1755
+ this._revealParticles = [];
1756
+ this._vortexParticles = [];
1757
+ this._emberParticles = [];
1758
+ for (const group of this.eggs) {
1759
+ group == null ? void 0 : group.traverse((child) => {
1760
+ if (child.geometry) child.geometry.dispose();
1761
+ if (child.material) {
1762
+ if (Array.isArray(child.material)) {
1763
+ child.material.forEach((m) => m.dispose());
1764
+ } else {
1765
+ child.material.dispose();
1766
+ }
1767
+ }
1768
+ });
1769
+ (_d = this.scene) == null ? void 0 : _d.remove(group);
1770
+ }
1771
+ this.eggs = [];
1772
+ this.eggBodies = [];
1773
+ if (this._holdRing) {
1774
+ (_e = this.scene) == null ? void 0 : _e.remove(this._holdRing);
1775
+ (_f = this._holdRing.geometry) == null ? void 0 : _f.dispose();
1776
+ (_g = this._holdRing.material) == null ? void 0 : _g.dispose();
1777
+ }
1778
+ if (this._holdShockwave) {
1779
+ (_h = this.scene) == null ? void 0 : _h.remove(this._holdShockwave);
1780
+ (_i = this._holdShockwave.geometry) == null ? void 0 : _i.dispose();
1781
+ (_j = this._holdShockwave.material) == null ? void 0 : _j.dispose();
1782
+ }
1783
+ for (const p of this._miniEggs) {
1784
+ (_k = this.scene) == null ? void 0 : _k.remove(p.mesh);
1785
+ (_l = p.mesh.geometry) == null ? void 0 : _l.dispose();
1786
+ (_m = p.mesh.material) == null ? void 0 : _m.dispose();
1787
+ }
1788
+ this._miniEggs = [];
1789
+ if (this._collectLight) {
1790
+ (_n = this.scene) == null ? void 0 : _n.remove(this._collectLight);
1791
+ (_p = (_o = this._collectLight).dispose) == null ? void 0 : _p.call(_o);
1792
+ this._collectLight = null;
1793
+ }
1794
+ for (const l of this.lights) {
1795
+ (_q = this.scene) == null ? void 0 : _q.remove(l);
1796
+ }
1797
+ this.lights = [];
1798
+ if ((_r = this.scene) == null ? void 0 : _r.environment) {
1799
+ this.scene.environment.dispose();
1800
+ this.scene.environment = null;
1801
+ }
1802
+ (_s = this.renderer) == null ? void 0 : _s.dispose();
1803
+ (_t = this.canvas) == null ? void 0 : _t.remove();
1804
+ (_u = this._greetingOverlay) == null ? void 0 : _u.remove();
1805
+ this._greetingOverlay = null;
1806
+ this.canvas = null;
1807
+ this.renderer = null;
1808
+ this.scene = null;
1809
+ this.camera = null;
1810
+ }
1811
+ // ═══════════════════════════════════════════════════════════════════════
1812
+ // LIGHTING
1813
+ // ═══════════════════════════════════════════════════════════════════════
1814
+ _setupLights(T) {
1815
+ const ambient = new T.AmbientLight(16775408, 0.6);
1816
+ this.scene.add(ambient);
1817
+ this.lights.push(ambient);
1818
+ const key = new T.DirectionalLight(16772306, 1);
1819
+ key.position.set(3, 5, 4);
1820
+ this.scene.add(key);
1821
+ this.lights.push(key);
1822
+ const fill = new T.DirectionalLight(13687024, 0.35);
1823
+ fill.position.set(-4, 3, 2);
1824
+ this.scene.add(fill);
1825
+ this.lights.push(fill);
1826
+ const rim = new T.PointLight(16763016, 0.5, 12);
1827
+ rim.position.set(0, -3, 3);
1828
+ this.scene.add(rim);
1829
+ this.lights.push(rim);
1830
+ const top = new T.PointLight(16777215, 0.3, 10);
1831
+ top.position.set(0, 4, 2);
1832
+ this.scene.add(top);
1833
+ this.lights.push(top);
1834
+ }
1835
+ // ═══════════════════════════════════════════════════════════════════════
1836
+ // ENVIRONMENT MAP — procedural studio HDRI for metallic reflections
1837
+ // ═══════════════════════════════════════════════════════════════════════
1838
+ _setupEnvironment(T) {
1839
+ const pmrem = new T.PMREMGenerator(this.renderer);
1840
+ pmrem.compileCubemapShader();
1841
+ const envScene = new T.Scene();
1842
+ const skyGeo = new T.SphereGeometry(50, 32, 16);
1843
+ const skyMat = new T.ShaderMaterial({
1844
+ side: T.BackSide,
1845
+ uniforms: {},
1846
+ vertexShader: `
1847
+ varying vec3 vWorldPos;
1848
+ void main() {
1849
+ vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
1850
+ gl_Position = projectionMatrix * viewMatrix * vec4(vWorldPos, 1.0);
1851
+ }
1852
+ `,
1853
+ fragmentShader: `
1854
+ varying vec3 vWorldPos;
1855
+ void main() {
1856
+ float y = normalize(vWorldPos).y;
1857
+ // Top: warm bright sky
1858
+ vec3 top = vec3(0.85, 0.88, 0.95);
1859
+ // Horizon: bright neutral white
1860
+ vec3 mid = vec3(0.95, 0.93, 0.90);
1861
+ // Bottom: warm ground bounce
1862
+ vec3 bot = vec3(0.75, 0.68, 0.62);
1863
+ vec3 color;
1864
+ if (y > 0.0) {
1865
+ color = mix(mid, top, y);
1866
+ } else {
1867
+ color = mix(mid, bot, -y);
1868
+ }
1869
+ // Bright highlight (simulates studio softbox)
1870
+ float sun = max(0.0, dot(normalize(vWorldPos), normalize(vec3(2.0, 5.0, 3.0))));
1871
+ color += vec3(1.0, 0.97, 0.92) * pow(sun, 16.0) * 0.8;
1872
+ // Fill light opposite side
1873
+ float fill = max(0.0, dot(normalize(vWorldPos), normalize(vec3(-3.0, 2.0, 1.0))));
1874
+ color += vec3(0.7, 0.8, 0.95) * pow(fill, 8.0) * 0.3;
1875
+ gl_FragColor = vec4(color, 1.0);
1876
+ }
1877
+ `
1878
+ });
1879
+ const skyMesh = new T.Mesh(skyGeo, skyMat);
1880
+ envScene.add(skyMesh);
1881
+ const envMap = pmrem.fromScene(envScene, 0.04).texture;
1882
+ this.scene.environment = envMap;
1883
+ skyGeo.dispose();
1884
+ skyMat.dispose();
1885
+ envScene.clear();
1886
+ pmrem.dispose();
1887
+ }
1888
+ // ═══════════════════════════════════════════════════════════════════════
1889
+ // EASTER EGG CREATION — Silver, Gold, and Royal Fabergé
1890
+ // ═══════════════════════════════════════════════════════════════════════
1891
+ _createEggs(T) {
1892
+ const baseGeo = this._createEggGeometry(T);
1893
+ const group1 = new T.Group();
1894
+ const silverTex = new T.CanvasTexture(this._drawSilverPolkaDots());
1895
+ silverTex.colorSpace = T.SRGBColorSpace;
1896
+ silverTex.wrapS = T.RepeatWrapping;
1897
+ const silverRoughTex = new T.CanvasTexture(this._drawSilverRoughness());
1898
+ silverRoughTex.wrapS = T.RepeatWrapping;
1899
+ const body1 = new T.Mesh(baseGeo.clone(), new T.MeshPhysicalMaterial({
1900
+ map: silverTex,
1901
+ roughness: 0.15,
1902
+ roughnessMap: silverRoughTex,
1903
+ metalness: 0.92,
1904
+ clearcoat: 0.3,
1905
+ clearcoatRoughness: 0.1,
1906
+ emissive: 2236979,
1907
+ emissiveIntensity: 0.02,
1908
+ envMapIntensity: 1
1909
+ }));
1910
+ group1.add(body1);
1911
+ group1.visible = false;
1912
+ this.scene.add(group1);
1913
+ this.eggs.push(group1);
1914
+ this.eggBodies.push(body1);
1915
+ const group2 = new T.Group();
1916
+ const goldTex = new T.CanvasTexture(this._drawGoldPattern());
1917
+ goldTex.colorSpace = T.SRGBColorSpace;
1918
+ goldTex.wrapS = T.RepeatWrapping;
1919
+ const goldRoughTex = new T.CanvasTexture(this._drawGoldRoughness());
1920
+ goldRoughTex.wrapS = T.RepeatWrapping;
1921
+ const body2 = new T.Mesh(baseGeo.clone(), new T.MeshPhysicalMaterial({
1922
+ map: goldTex,
1923
+ roughness: 0.2,
1924
+ roughnessMap: goldRoughTex,
1925
+ metalness: 0.95,
1926
+ clearcoat: 0.2,
1927
+ clearcoatRoughness: 0.15,
1928
+ emissive: 3811848,
1929
+ emissiveIntensity: 0.06,
1930
+ envMapIntensity: 1
1931
+ }));
1932
+ group2.add(body2);
1933
+ group2.visible = false;
1934
+ this.scene.add(group2);
1935
+ this.eggs.push(group2);
1936
+ this.eggBodies.push(body2);
1937
+ const group3 = new T.Group();
1938
+ const festiveTex = new T.CanvasTexture(this._drawFestivePattern());
1939
+ festiveTex.colorSpace = T.SRGBColorSpace;
1940
+ festiveTex.wrapS = T.RepeatWrapping;
1941
+ const festiveRoughTex = new T.CanvasTexture(this._drawFestiveRoughness());
1942
+ festiveRoughTex.wrapS = T.RepeatWrapping;
1943
+ const body3 = new T.Mesh(baseGeo.clone(), new T.MeshPhysicalMaterial({
1944
+ map: festiveTex,
1945
+ roughness: 0.22,
1946
+ roughnessMap: festiveRoughTex,
1947
+ metalness: 0.35,
1948
+ clearcoat: 0.8,
1949
+ clearcoatRoughness: 0.05,
1950
+ emissive: 1708064,
1951
+ emissiveIntensity: 0.04,
1952
+ envMapIntensity: 0.8,
1953
+ sheen: 0.4,
1954
+ sheenColor: new T.Color(16765088),
1955
+ sheenRoughness: 0.3
1956
+ }));
1957
+ group3.add(body3);
1958
+ group3.visible = false;
1959
+ this.scene.add(group3);
1960
+ this.eggs.push(group3);
1961
+ this.eggBodies.push(body3);
1962
+ }
1963
+ // ── Mini egg helpers for particles ─────────────────────────────────────
1964
+ /** Create a tiny egg-shaped geometry (same squash as main eggs, lower poly). */
1965
+ _createMiniEggGeo(T, radius) {
1966
+ const geo = new T.SphereGeometry(radius, 8, 8);
1967
+ const pos = geo.attributes.position;
1968
+ for (let i = 0; i < pos.count; i++) {
1969
+ let y = pos.getY(i);
1970
+ const x = pos.getX(i);
1971
+ const z = pos.getZ(i);
1972
+ const topSquash = y > 0 ? 0.92 : 1;
1973
+ y = y * 1.15 * topSquash;
1974
+ const narrowing = 1 - Math.abs(y) * 0.06;
1975
+ pos.setX(i, x * narrowing);
1976
+ pos.setY(i, y);
1977
+ pos.setZ(i, z * narrowing);
1978
+ }
1979
+ geo.computeVertexNormals();
1980
+ return geo;
1981
+ }
1982
+ /** Create a minimalist randomly-decorated mini egg material. */
1983
+ /** Create a decorated mini egg material — pastel base with visible patterns. */
1984
+ _createMiniEggMat(T, opacity) {
1985
+ const c = document.createElement("canvas");
1986
+ c.width = 64;
1987
+ c.height = 64;
1988
+ const ctx = c.getContext("2d");
1989
+ const hue = Math.random() * 360;
1990
+ const sat = 45 + Math.random() * 30;
1991
+ const light = 65 + Math.random() * 15;
1992
+ ctx.fillStyle = `hsl(${hue}, ${sat}%, ${light}%)`;
1993
+ ctx.fillRect(0, 0, 64, 64);
1994
+ const accentHue = (hue + 80 + Math.random() * 100) % 360;
1995
+ const accentSat = sat + 10;
1996
+ const accentLight = Math.max(35, light - 30);
1997
+ ctx.lineWidth = 2;
1998
+ const layers = 1 + Math.floor(Math.random() * 2);
1999
+ for (let layer = 0; layer < layers; layer++) {
2000
+ const layerHue = layer === 0 ? accentHue : (accentHue + 120) % 360;
2001
+ ctx.strokeStyle = `hsla(${layerHue}, ${accentSat}%, ${accentLight}%, 0.7)`;
2002
+ ctx.fillStyle = `hsla(${layerHue}, ${accentSat}%, ${accentLight + 10}%, 0.5)`;
2003
+ const pattern = Math.floor(Math.random() * 6);
2004
+ const yOff = layer * 16 - 8;
2005
+ switch (pattern) {
2006
+ case 0:
2007
+ ctx.beginPath();
2008
+ ctx.moveTo(0, 24 + yOff);
2009
+ ctx.lineTo(64, 24 + yOff);
2010
+ ctx.moveTo(0, 40 + yOff);
2011
+ ctx.lineTo(64, 40 + yOff);
2012
+ ctx.stroke();
2013
+ break;
2014
+ case 1:
2015
+ for (let d = 0; d < 6; d++) {
2016
+ ctx.beginPath();
2017
+ ctx.arc(
2018
+ 8 + Math.random() * 48,
2019
+ 8 + Math.random() * 48,
2020
+ 2.5 + Math.random() * 2.5,
2021
+ 0,
2022
+ Math.PI * 2
2023
+ );
2024
+ ctx.fill();
2025
+ }
2026
+ break;
2027
+ case 2:
2028
+ ctx.beginPath();
2029
+ ctx.moveTo(0, 32 + yOff);
2030
+ for (let zx = 6; zx <= 64; zx += 6) {
2031
+ ctx.lineTo(zx, 32 + yOff + (zx / 6 % 2 === 0 ? -7 : 7));
2032
+ }
2033
+ ctx.stroke();
2034
+ break;
2035
+ case 3:
2036
+ ctx.fillRect(0, 24 + yOff, 64, 16);
2037
+ break;
2038
+ case 4:
2039
+ ctx.beginPath();
2040
+ for (let lx = 0; lx < 64; lx += 12) {
2041
+ ctx.moveTo(lx, 0);
2042
+ ctx.lineTo(lx + 32, 64);
2043
+ ctx.moveTo(lx + 32, 0);
2044
+ ctx.lineTo(lx, 64);
2045
+ }
2046
+ ctx.stroke();
2047
+ break;
2048
+ default:
2049
+ ctx.beginPath();
2050
+ ctx.moveTo(0, 32 + yOff);
2051
+ for (let wx = 0; wx <= 64; wx += 2) {
2052
+ ctx.lineTo(wx, 32 + yOff + Math.sin(wx * 0.3) * 5);
2053
+ }
2054
+ ctx.stroke();
2055
+ break;
2056
+ }
2057
+ }
2058
+ const tex = new T.CanvasTexture(c);
2059
+ return new T.MeshBasicMaterial({
2060
+ map: tex,
2061
+ transparent: true,
2062
+ opacity
2063
+ });
2064
+ }
2065
+ _createEggGeometry(T) {
2066
+ const geo = new T.SphereGeometry(0.42, 48, 48);
2067
+ const pos = geo.attributes.position;
2068
+ for (let i = 0; i < pos.count; i++) {
2069
+ let y = pos.getY(i);
2070
+ const x = pos.getX(i);
2071
+ const z = pos.getZ(i);
2072
+ const topSquash = y > 0 ? 0.82 : 1;
2073
+ y = y * 1.45 * topSquash;
2074
+ const narrowing = 1 - Math.abs(y) * 0.12;
2075
+ pos.setX(i, x * narrowing);
2076
+ pos.setY(i, y);
2077
+ pos.setZ(i, z * narrowing);
2078
+ }
2079
+ geo.computeVertexNormals();
2080
+ return geo;
2081
+ }
2082
+ // ═══════════════════════════════════════════════════════════════════════
2083
+ // CANVAS TEXTURE GENERATORS — procedural PBR textures
2084
+ // ═══════════════════════════════════════════════════════════════════════
2085
+ /** Silver polka-dot color map */
2086
+ _drawSilverPolkaDots() {
2087
+ const size = 512;
2088
+ const c = document.createElement("canvas");
2089
+ c.width = size;
2090
+ c.height = size;
2091
+ const ctx = c.getContext("2d");
2092
+ const bg = ctx.createLinearGradient(0, 0, 0, size);
2093
+ bg.addColorStop(0, "#e0e4ea");
2094
+ bg.addColorStop(0.4, "#d5d9e0");
2095
+ bg.addColorStop(0.6, "#cdd1d8");
2096
+ bg.addColorStop(1, "#c8c4c0");
2097
+ ctx.fillStyle = bg;
2098
+ ctx.fillRect(0, 0, size, size);
2099
+ const dotR = 22;
2100
+ const spacingX = 64;
2101
+ const spacingY = 52;
2102
+ ctx.fillStyle = "#f4f4f6";
2103
+ ctx.shadowColor = "rgba(255,255,255,0.5)";
2104
+ ctx.shadowBlur = 6;
2105
+ for (let row = 0; row < size / spacingY + 1; row++) {
2106
+ const offsetX = row % 2 * (spacingX / 2);
2107
+ for (let col = -1; col < size / spacingX + 1; col++) {
2108
+ const cx = col * spacingX + offsetX;
2109
+ const cy = row * spacingY;
2110
+ ctx.beginPath();
2111
+ ctx.arc(cx, cy, dotR, 0, Math.PI * 2);
2112
+ ctx.fill();
2113
+ }
2114
+ }
2115
+ ctx.shadowBlur = 0;
2116
+ return c;
2117
+ }
2118
+ /** Silver roughness map (dots are more rough = less shiny) */
2119
+ _drawSilverRoughness() {
2120
+ const size = 512;
2121
+ const c = document.createElement("canvas");
2122
+ c.width = size;
2123
+ c.height = size;
2124
+ const ctx = c.getContext("2d");
2125
+ ctx.fillStyle = "#333";
2126
+ ctx.fillRect(0, 0, size, size);
2127
+ const dotR = 22;
2128
+ const spacingX = 64;
2129
+ const spacingY = 52;
2130
+ ctx.fillStyle = "#999";
2131
+ for (let row = 0; row < size / spacingY + 1; row++) {
2132
+ const offsetX = row % 2 * (spacingX / 2);
2133
+ for (let col = -1; col < size / spacingX + 1; col++) {
2134
+ const cx = col * spacingX + offsetX;
2135
+ const cy = row * spacingY;
2136
+ ctx.beginPath();
2137
+ ctx.arc(cx, cy, dotR, 0, Math.PI * 2);
2138
+ ctx.fill();
2139
+ }
2140
+ }
2141
+ return c;
2142
+ }
2143
+ /** Gold color map with ornamental scrollwork */
2144
+ _drawGoldPattern() {
2145
+ const size = 512;
2146
+ const c = document.createElement("canvas");
2147
+ c.width = size;
2148
+ c.height = size;
2149
+ const ctx = c.getContext("2d");
2150
+ const bg = ctx.createLinearGradient(0, 0, 0, size);
2151
+ bg.addColorStop(0, "#c8960a");
2152
+ bg.addColorStop(0.3, "#daa520");
2153
+ bg.addColorStop(0.5, "#e6b422");
2154
+ bg.addColorStop(0.7, "#c89618");
2155
+ bg.addColorStop(1, "#b8860b");
2156
+ ctx.fillStyle = bg;
2157
+ ctx.fillRect(0, 0, size, size);
2158
+ ctx.globalAlpha = 0.2;
2159
+ for (const yRatio of [0.25, 0.5, 0.75]) {
2160
+ const y = yRatio * size;
2161
+ const bandH = 30;
2162
+ const bg2 = ctx.createLinearGradient(0, y - bandH, 0, y + bandH);
2163
+ bg2.addColorStop(0, "#b8860b");
2164
+ bg2.addColorStop(0.5, "#f0d060");
2165
+ bg2.addColorStop(1, "#b8860b");
2166
+ ctx.fillStyle = bg2;
2167
+ ctx.fillRect(0, y - bandH, size, bandH * 2);
2168
+ }
2169
+ ctx.globalAlpha = 1;
2170
+ ctx.strokeStyle = "#a07008";
2171
+ ctx.lineWidth = 1.2;
2172
+ ctx.globalAlpha = 0.15;
2173
+ const step = 32;
2174
+ for (let x = -size; x < size * 2; x += step) {
2175
+ ctx.beginPath();
2176
+ ctx.moveTo(x, 0);
2177
+ ctx.lineTo(x + size, size);
2178
+ ctx.stroke();
2179
+ ctx.beginPath();
2180
+ ctx.moveTo(x + size, 0);
2181
+ ctx.lineTo(x, size);
2182
+ ctx.stroke();
2183
+ }
2184
+ ctx.globalAlpha = 1;
2185
+ ctx.strokeStyle = "#d4a510";
2186
+ ctx.lineWidth = 2;
2187
+ ctx.globalAlpha = 0.3;
2188
+ for (const yRatio of [0.22, 0.28, 0.47, 0.53, 0.72, 0.78]) {
2189
+ const y = yRatio * size;
2190
+ ctx.beginPath();
2191
+ ctx.moveTo(0, y);
2192
+ ctx.lineTo(size, y);
2193
+ ctx.stroke();
2194
+ }
2195
+ ctx.globalAlpha = 1;
2196
+ return c;
2197
+ }
2198
+ /** Gold roughness map */
2199
+ _drawGoldRoughness() {
2200
+ const size = 512;
2201
+ const c = document.createElement("canvas");
2202
+ c.width = size;
2203
+ c.height = size;
2204
+ const ctx = c.getContext("2d");
2205
+ ctx.fillStyle = "#444";
2206
+ ctx.fillRect(0, 0, size, size);
2207
+ for (const yRatio of [0.25, 0.5, 0.75]) {
2208
+ const y = yRatio * size;
2209
+ ctx.fillStyle = "#666";
2210
+ ctx.fillRect(0, y - 30, size, 60);
2211
+ }
2212
+ return c;
2213
+ }
2214
+ /** Festive Fabergé color map — randomized enamel bands + golden dividers */
2215
+ _drawFestivePattern(seed) {
2216
+ const size = 512;
2217
+ const c = document.createElement("canvas");
2218
+ c.width = size;
2219
+ c.height = size;
2220
+ const ctx = c.getContext("2d");
2221
+ let s = seed ?? Date.now() ^ 25214903917;
2222
+ const rand = () => {
2223
+ s = s + 1831565813 | 0;
2224
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
2225
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2226
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2227
+ };
2228
+ const hueBase = rand() * 360;
2229
+ const enamelHues = [];
2230
+ for (let i = 0; i < 5; i++) {
2231
+ enamelHues.push((hueBase + i * (50 + rand() * 30)) % 360);
2232
+ }
2233
+ const bandCount = 9;
2234
+ const bandH = size / bandCount;
2235
+ for (let i = 0; i < bandCount; i++) {
2236
+ const isGold = i % 2 === 1;
2237
+ const y = i * bandH;
2238
+ if (isGold) {
2239
+ const goldLight = 52 + rand() * 10;
2240
+ const bg = ctx.createLinearGradient(0, y, 0, y + bandH);
2241
+ bg.addColorStop(0, `hsl(43, 80%, ${goldLight}%)`);
2242
+ bg.addColorStop(1, `hsl(43, 75%, ${goldLight - 6}%)`);
2243
+ ctx.fillStyle = bg;
2244
+ } else {
2245
+ const eIdx = Math.floor(i / 2);
2246
+ const hue = enamelHues[eIdx % enamelHues.length];
2247
+ const sat = 55 + rand() * 30;
2248
+ const light = 35 + rand() * 15;
2249
+ const bg = ctx.createLinearGradient(0, y, 0, y + bandH);
2250
+ bg.addColorStop(0, `hsl(${hue}, ${sat}%, ${light}%)`);
2251
+ bg.addColorStop(1, `hsl(${hue}, ${sat}%, ${light - 8}%)`);
2252
+ ctx.fillStyle = bg;
2253
+ }
2254
+ ctx.fillRect(0, y, size, bandH + 1);
2255
+ }
2256
+ const overlayType = Math.floor(rand() * 4);
2257
+ ctx.strokeStyle = `hsla(43, 70%, 60%, 0.2)`;
2258
+ ctx.lineWidth = 1.5;
2259
+ if (overlayType === 0) {
2260
+ const step = rand() < 0.5 ? 32 : 64;
2261
+ for (let x = -size; x < size * 2; x += step) {
2262
+ ctx.beginPath();
2263
+ ctx.moveTo(x, 0);
2264
+ ctx.lineTo(x + size, size);
2265
+ ctx.stroke();
2266
+ ctx.beginPath();
2267
+ ctx.moveTo(x + size, 0);
2268
+ ctx.lineTo(x, size);
2269
+ ctx.stroke();
2270
+ }
2271
+ } else if (overlayType === 1) {
2272
+ const scW = 32;
2273
+ for (let bi = 0; bi < bandCount; bi += 2) {
2274
+ const cy = (bi + 0.5) * bandH;
2275
+ for (let sx = scW / 2; sx < size; sx += scW) {
2276
+ ctx.beginPath();
2277
+ ctx.arc(sx, cy, scW * 0.38, 0, Math.PI * 2);
2278
+ ctx.stroke();
2279
+ }
2280
+ }
2281
+ } else if (overlayType === 2) {
2282
+ const amp = 8 + rand() * 8;
2283
+ const freq = [16, 32, 64][Math.floor(rand() * 3)];
2284
+ for (let bi = 0; bi < bandCount; bi += 2) {
2285
+ const cy = (bi + 0.5) * bandH;
2286
+ ctx.beginPath();
2287
+ ctx.moveTo(0, cy);
2288
+ for (let x = 0; x <= size; x += freq) {
2289
+ ctx.lineTo(x, cy + (x / freq % 2 === 0 ? -amp : amp));
2290
+ }
2291
+ ctx.stroke();
2292
+ }
2293
+ } else {
2294
+ for (let bi = 0; bi < bandCount; bi += 2) {
2295
+ const yCenter = (bi + 0.5) * bandH;
2296
+ for (const off of [-bandH * 0.2, bandH * 0.2]) {
2297
+ ctx.beginPath();
2298
+ for (let x = 0; x <= size; x += 2) {
2299
+ ctx.lineTo(x, yCenter + off + Math.sin(x * (Math.PI * 8 / size) + rand() * 0.5) * 4);
2300
+ }
2301
+ ctx.stroke();
2302
+ }
2303
+ }
2304
+ }
2305
+ const gemCount = 10 + Math.floor(rand() * 10);
2306
+ for (let di = 1; di < bandCount; di += 2) {
2307
+ const cy = (di + 0.5) * bandH;
2308
+ for (let j = 0; j < gemCount; j++) {
2309
+ const cx = (j + 0.5) * (size / gemCount);
2310
+ const gh = enamelHues[Math.floor(rand() * enamelHues.length)];
2311
+ ctx.fillStyle = `hsl(${gh}, 70%, 55%)`;
2312
+ ctx.beginPath();
2313
+ const gemPick = rand();
2314
+ if (gemPick < 0.5) {
2315
+ ctx.arc(cx, cy, 3 + rand() * 3, 0, Math.PI * 2);
2316
+ } else {
2317
+ const r = 3 + rand() * 3;
2318
+ ctx.moveTo(cx, cy - r);
2319
+ ctx.lineTo(cx + r, cy);
2320
+ ctx.lineTo(cx, cy + r);
2321
+ ctx.lineTo(cx - r, cy);
2322
+ }
2323
+ ctx.fill();
2324
+ }
2325
+ }
2326
+ return c;
2327
+ }
2328
+ /** Festive roughness map */
2329
+ _drawFestiveRoughness() {
2330
+ const size = 512;
2331
+ const c = document.createElement("canvas");
2332
+ c.width = size;
2333
+ c.height = size;
2334
+ const ctx = c.getContext("2d");
2335
+ ctx.fillStyle = "#555";
2336
+ ctx.fillRect(0, 0, size, size);
2337
+ const bands = 9;
2338
+ const bandH = size / bands;
2339
+ const dividerYs = [1, 3, 5, 7];
2340
+ for (const dIdx of dividerYs) {
2341
+ ctx.fillStyle = "#333";
2342
+ ctx.fillRect(0, dIdx * bandH, size, bandH);
2343
+ }
2344
+ return c;
2345
+ }
2346
+ // ═══════════════════════════════════════════════════════════════════════
2347
+ // HOLD RING (visual feedback for long-press)
2348
+ // ═══════════════════════════════════════════════════════════════════════
2349
+ _createHoldRing(_T) {
2350
+ }
2351
+ // ═══════════════════════════════════════════════════════════════════════
2352
+ // REVEAL BURST — particle explosion when egg appears
2353
+ // ═══════════════════════════════════════════════════════════════════════
2354
+ _spawnRevealBurst(eggIndex) {
2355
+ if (!this.THREE || !this.scene) return;
2356
+ const T = this.THREE;
2357
+ const burstColors = [
2358
+ [12632264, 14540270, 16777215],
2359
+ [16766720, 13145600, 16769152],
2360
+ [13369376, 17612, 43568, 16766720, 8913066]
2361
+ ];
2362
+ const colors = burstColors[eggIndex] ?? burstColors[0];
2363
+ const count = 40 + eggIndex * 20;
2364
+ for (let i = 0; i < count; i++) {
2365
+ const size = 0.015 + Math.random() * 0.03;
2366
+ const geo = new T.SphereGeometry(size, 5, 5);
2367
+ const c = colors[Math.floor(Math.random() * colors.length)];
2368
+ const mat = new T.MeshBasicMaterial({
2369
+ color: c,
2370
+ transparent: true,
2371
+ opacity: 0.8
2372
+ });
2373
+ const p = new T.Mesh(geo, mat);
2374
+ const theta = Math.random() * Math.PI * 2;
2375
+ const phi = Math.random() * Math.PI;
2376
+ const speed = 1.5 + Math.random() * 2.5;
2377
+ const vx = Math.sin(phi) * Math.cos(theta) * speed;
2378
+ const vy = Math.sin(phi) * Math.sin(theta) * speed;
2379
+ const vz = Math.cos(phi) * speed;
2380
+ p.position.set(0, 0, 0);
2381
+ this.scene.add(p);
2382
+ this._revealParticles.push({ mesh: p, life: 1, vx, vy, vz });
2383
+ }
2384
+ }
2385
+ /** Spawn golden energy vortex particles that orbit and spiral inward. */
2386
+ _spawnVortex(eggIndex) {
2387
+ if (!this.THREE || !this.scene) return;
2388
+ const T = this.THREE;
2389
+ const egg = this.eggs[eggIndex];
2390
+ const origin = egg ? egg.position.clone() : new T.Vector3(0, 0, 0);
2391
+ const count = 60;
2392
+ for (let n = 0; n < count; n++) {
2393
+ const size = 8e-3 + Math.random() * 0.015;
2394
+ const geo = new T.SphereGeometry(size, 6, 6);
2395
+ const hue = 0.1 + Math.random() * 0.06;
2396
+ const col = new T.Color().setHSL(hue, 0.8, 0.6 + Math.random() * 0.3);
2397
+ const mat = new T.MeshBasicMaterial({
2398
+ color: col,
2399
+ transparent: true,
2400
+ opacity: 0
2401
+ });
2402
+ const p = new T.Mesh(geo, mat);
2403
+ const angle = n / count * Math.PI * 2 + Math.random() * 0.3;
2404
+ const radius = 1.2 + Math.random() * 0.8;
2405
+ const yOff = (Math.random() - 0.5) * 1;
2406
+ p.position.set(
2407
+ origin.x + Math.cos(angle) * radius,
2408
+ origin.y + yOff,
2409
+ origin.z + Math.sin(angle) * radius
2410
+ );
2411
+ this.scene.add(p);
2412
+ this._vortexParticles.push({
2413
+ mesh: p,
2414
+ life: 1,
2415
+ angle,
2416
+ radius,
2417
+ yOff,
2418
+ originX: origin.x,
2419
+ originY: origin.y,
2420
+ originZ: origin.z,
2421
+ speed: 1.5 + Math.random() * 1.5,
2422
+ delay: n * 8e-3 + Math.random() * 0.15
2423
+ });
2424
+ }
2425
+ }
2426
+ /** Spawn upward-drifting golden ember particles after implosion. */
2427
+ _spawnEmbers(eggIndex) {
2428
+ if (!this.THREE || !this.scene) return;
2429
+ const T = this.THREE;
2430
+ const egg = this.eggs[eggIndex];
2431
+ const origin = egg ? egg.position.clone() : new T.Vector3(0, 0, 0);
2432
+ const count = 35;
2433
+ for (let n = 0; n < count; n++) {
2434
+ const size = 6e-3 + Math.random() * 0.012;
2435
+ const geo = new T.SphereGeometry(size, 4, 4);
2436
+ const hue = 0.08 + Math.random() * 0.1;
2437
+ const col = new T.Color().setHSL(hue, 0.7, 0.5 + Math.random() * 0.4);
2438
+ const mat = new T.MeshBasicMaterial({
2439
+ color: col,
2440
+ transparent: true,
2441
+ opacity: 0.9
2442
+ });
2443
+ const p = new T.Mesh(geo, mat);
2444
+ const spread = 0.3;
2445
+ p.position.set(
2446
+ origin.x + (Math.random() - 0.5) * spread,
2447
+ origin.y + (Math.random() - 0.5) * spread,
2448
+ origin.z + (Math.random() - 0.5) * spread * 0.3
2449
+ );
2450
+ this.scene.add(p);
2451
+ this._emberParticles.push({
2452
+ mesh: p,
2453
+ life: 1,
2454
+ vx: (Math.random() - 0.5) * 0.3,
2455
+ vy: 0.4 + Math.random() * 0.6,
2456
+ vz: (Math.random() - 0.5) * 0.1,
2457
+ flicker: Math.random() * Math.PI * 2
2458
+ });
2459
+ }
2460
+ }
2461
+ /** Update vortex particles — orbit, spiral inward, fade in then out. */
2462
+ _updateVortexParticles(dt, collectProgress) {
2463
+ var _a;
2464
+ for (let i = this._vortexParticles.length - 1; i >= 0; i--) {
2465
+ const p = this._vortexParticles[i];
2466
+ if (collectProgress < p.delay) continue;
2467
+ const localP = Math.min(1, (collectProgress - p.delay) / (0.55 - p.delay));
2468
+ const currentRadius = p.radius * (1 - localP * localP * 0.95);
2469
+ p.angle += dt * p.speed * (1 + localP * 4);
2470
+ const yDrift = p.yOff * (1 - localP);
2471
+ p.mesh.position.x = p.originX + Math.cos(p.angle) * currentRadius;
2472
+ p.mesh.position.y = p.originY + yDrift;
2473
+ p.mesh.position.z = p.originZ + Math.sin(p.angle) * currentRadius;
2474
+ if (localP < 0.2) {
2475
+ p.mesh.material.opacity = localP / 0.2 * 0.85;
2476
+ } else if (localP > 0.85) {
2477
+ p.mesh.material.opacity = (1 - localP) / 0.15 * 0.85;
2478
+ } else {
2479
+ p.mesh.material.opacity = 0.85;
2480
+ }
2481
+ p.mesh.scale.setScalar(0.8 + Math.sin(p.angle * 3) * 0.3);
2482
+ if (localP >= 1) {
2483
+ (_a = this.scene) == null ? void 0 : _a.remove(p.mesh);
2484
+ p.mesh.geometry.dispose();
2485
+ p.mesh.material.dispose();
2486
+ this._vortexParticles.splice(i, 1);
2487
+ }
2488
+ }
2489
+ }
2490
+ /** Update ember particles — drift up, sway, flicker, fade. */
2491
+ _updateEmberParticles(dt) {
2492
+ var _a;
2493
+ for (let i = this._emberParticles.length - 1; i >= 0; i--) {
2494
+ const p = this._emberParticles[i];
2495
+ p.life -= dt * 0.4;
2496
+ p.mesh.position.x += p.vx * dt;
2497
+ p.mesh.position.y += p.vy * dt;
2498
+ p.mesh.position.z += p.vz * dt;
2499
+ p.flicker += dt * 3;
2500
+ p.mesh.position.x += Math.sin(p.flicker) * dt * 0.15;
2501
+ p.vx *= 0.995;
2502
+ p.vz *= 0.995;
2503
+ const flick = 0.7 + Math.sin(p.flicker * 4) * 0.3;
2504
+ p.mesh.material.opacity = Math.max(0, p.life * flick);
2505
+ p.mesh.scale.setScalar(0.5 + p.life * 0.5);
2506
+ if (p.life <= 0) {
2507
+ (_a = this.scene) == null ? void 0 : _a.remove(p.mesh);
2508
+ p.mesh.geometry.dispose();
2509
+ p.mesh.material.dispose();
2510
+ this._emberParticles.splice(i, 1);
2511
+ }
2512
+ }
2513
+ }
2514
+ _cancelLongPress() {
2515
+ var _a;
2516
+ this._longPressActive = false;
2517
+ this._holdProgress = 0;
2518
+ if (this._interactiveEgg !== null) {
2519
+ const egg = this.eggs[this._interactiveEgg];
2520
+ if (egg) {
2521
+ egg.scale.setScalar(0.9);
2522
+ egg.rotation.z = 0;
2523
+ }
2524
+ const body = this.eggBodies[this._interactiveEgg];
2525
+ if (((_a = body == null ? void 0 : body.material) == null ? void 0 : _a.emissiveIntensity) !== void 0) {
2526
+ body.material.emissiveIntensity = 0.05;
2527
+ }
2528
+ }
2529
+ }
2530
+ _worldToScreen(pos) {
2531
+ if (!this.THREE || !this.camera) return { x: 0, y: 0 };
2532
+ const T = this.THREE;
2533
+ const v = new T.Vector3().copy(pos);
2534
+ v.project(this.camera);
2535
+ return {
2536
+ x: (v.x * 0.5 + 0.5) * window.innerWidth,
2537
+ y: (-v.y * 0.5 + 0.5) * window.innerHeight
2538
+ };
2539
+ }
2540
+ _screenToWorld(sx, sy) {
2541
+ const T = this.THREE;
2542
+ const ndcX = sx / window.innerWidth * 2 - 1;
2543
+ const ndcY = -(sy / window.innerHeight) * 2 + 1;
2544
+ const vec = new T.Vector3(ndcX, ndcY, 0.5);
2545
+ vec.unproject(this.camera);
2546
+ const dir = vec.sub(this.camera.position).normalize();
2547
+ const dist = -this.camera.position.z / dir.z;
2548
+ return this.camera.position.clone().add(dir.multiplyScalar(dist));
2549
+ }
2550
+ _updateEggs(dt) {
2551
+ var _a, _b;
2552
+ if (!this.THREE) return;
2553
+ for (let i = 0; i < 3; i++) {
2554
+ const group = this.eggs[i];
2555
+ const state = this._eggStates[i];
2556
+ if (!state.visible || !group) continue;
2557
+ state.t += dt;
2558
+ switch (state.phase) {
2559
+ case "reveal": {
2560
+ const dur = 3;
2561
+ const p = Math.min(1, state.t / dur);
2562
+ const c4 = 2 * Math.PI / 3;
2563
+ const ease = p === 0 ? 0 : p === 1 ? 1 : Math.pow(2, -10 * p) * Math.sin((p * 10 - 0.75) * c4) + 1;
2564
+ group.scale.setScalar(ease * 0.9);
2565
+ group.rotation.y = state.t * 1.2;
2566
+ group.position.y = Math.sin(state.t * 2) * 0.06 * (1 - p);
2567
+ const body = this.eggBodies[i];
2568
+ if (((_a = body == null ? void 0 : body.material) == null ? void 0 : _a.emissiveIntensity) !== void 0) {
2569
+ body.material.emissiveIntensity = 0.15 + Math.sin(state.t * 4) * 0.1;
2570
+ }
2571
+ if (p >= 1) {
2572
+ state.phase = "interactive";
2573
+ state.t = 0;
2574
+ }
2575
+ break;
2576
+ }
2577
+ case "interactive": {
2578
+ if (!this._isDragging || this._interactiveEgg !== i) {
2579
+ group.rotation.y += dt * 0.25;
2580
+ }
2581
+ group.position.y = Math.sin(state.t * 0.7) * 0.04;
2582
+ const body = this.eggBodies[i];
2583
+ if (((_b = body == null ? void 0 : body.material) == null ? void 0 : _b.emissiveIntensity) !== void 0) {
2584
+ body.material.emissiveIntensity = 0.05 + Math.sin(state.t * 1.5) * 0.04;
2585
+ }
2586
+ break;
2587
+ }
2588
+ case "collecting": {
2589
+ group.visible = false;
2590
+ state.phase = "shrine";
2591
+ state.t = 0;
2592
+ break;
2593
+ }
2594
+ case "shrine": {
2595
+ group.rotation.y += dt * 0.15;
2596
+ const scale = 0.32 + Math.sin(state.t * 1) * 0.01;
2597
+ group.scale.setScalar(scale);
2598
+ break;
2599
+ }
2600
+ }
2601
+ }
2602
+ }
2603
+ _updateHoldRing() {
2604
+ var _a;
2605
+ if (!this._longPressActive || this._interactiveEgg === null) return;
2606
+ const elapsed = Date.now() - this._longPressStartTime;
2607
+ const holdDuration = 1200;
2608
+ this._holdProgress = Math.min(1, elapsed / holdDuration);
2609
+ const p = this._holdProgress;
2610
+ const time = elapsed * 1e-3;
2611
+ const egg = this.eggs[this._interactiveEgg];
2612
+ if (!egg) return;
2613
+ const baseScale = 0.9;
2614
+ const growScale = baseScale * (1 + p * 0.35);
2615
+ egg.scale.setScalar(growScale);
2616
+ if (p > 0.4) {
2617
+ const wobble = (p - 0.4) * 0.03;
2618
+ egg.rotation.z = Math.sin(time * 12) * wobble;
2619
+ egg.rotation.x = Math.cos(time * 10) * wobble * 0.5;
2620
+ }
2621
+ if (p >= 1) {
2622
+ const idx = this._interactiveEgg;
2623
+ this._spawnMiniEggBurst(idx);
2624
+ this._cancelLongPress();
2625
+ (_a = this._onCollectEgg) == null ? void 0 : _a.call(this, idx);
2626
+ }
2627
+ }
2628
+ /** Spawn a burst of shards flying outward from the collected egg, in the egg's own colour. */
2629
+ _spawnMiniEggBurst(eggIndex) {
2630
+ if (!this.THREE || !this.scene) return;
2631
+ const T = this.THREE;
2632
+ const egg = this.eggs[eggIndex];
2633
+ if (!egg) return;
2634
+ const origin = egg.position.clone();
2635
+ const eggPalettes = [
2636
+ { main: 12634328, accent: 8425648 },
2637
+ // Egg 1: Silver
2638
+ { main: 15253568, accent: 13934624 },
2639
+ // Egg 2: Gold
2640
+ { main: 13647968, accent: 8401056 }
2641
+ // Egg 3: Fabergé (ruby/amethyst)
2642
+ ];
2643
+ const palette = eggPalettes[eggIndex] ?? eggPalettes[0];
2644
+ const count = 18;
2645
+ const miniGeo = new T.SphereGeometry(0.06, 12, 12);
2646
+ const pos = miniGeo.attributes.position;
2647
+ for (let vi = 0; vi < pos.count; vi++) {
2648
+ let y = pos.getY(vi);
2649
+ const topSquash = y > 0 ? 0.82 : 1;
2650
+ y = y * 1.4 * topSquash;
2651
+ pos.setY(vi, y);
2652
+ }
2653
+ miniGeo.computeVertexNormals();
2654
+ for (let i = 0; i < count; i++) {
2655
+ const color = Math.random() > 0.35 ? palette.main : palette.accent;
2656
+ const mat = new T.MeshStandardMaterial({
2657
+ color,
2658
+ roughness: 0.3,
2659
+ metalness: 0.5,
2660
+ emissive: color,
2661
+ emissiveIntensity: 0.15,
2662
+ transparent: true,
2663
+ opacity: 1
2664
+ });
2665
+ const mini = new T.Mesh(miniGeo, mat);
2666
+ mini.position.copy(origin);
2667
+ const s = 0.5 + Math.random() * 0.8;
2668
+ mini.scale.setScalar(s);
2669
+ mini.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
2670
+ const theta = Math.random() * Math.PI * 2;
2671
+ const phi = Math.acos(2 * Math.random() - 1);
2672
+ const speed = 2 + Math.random() * 3;
2673
+ const vx = Math.sin(phi) * Math.cos(theta) * speed;
2674
+ const vy = Math.sin(phi) * Math.sin(theta) * speed * 0.8 + 1;
2675
+ const vz = Math.cos(phi) * speed;
2676
+ this.scene.add(mini);
2677
+ this._miniEggs.push({
2678
+ mesh: mini,
2679
+ vx,
2680
+ vy,
2681
+ vz,
2682
+ life: 1,
2683
+ spin: (Math.random() - 0.5) * 8
2684
+ });
2685
+ }
2686
+ }
2687
+ _updateMiniEggs(dt) {
2688
+ var _a, _b;
2689
+ for (let i = this._miniEggs.length - 1; i >= 0; i--) {
2690
+ const p = this._miniEggs[i];
2691
+ p.life -= dt * 1.2;
2692
+ p.vy -= dt * 5;
2693
+ p.mesh.position.x += p.vx * dt;
2694
+ p.mesh.position.y += p.vy * dt;
2695
+ p.mesh.position.z += p.vz * dt;
2696
+ p.mesh.rotation.y += p.spin * dt;
2697
+ p.mesh.rotation.x += p.spin * dt * 0.5;
2698
+ const fade = Math.max(0, p.life);
2699
+ p.mesh.material.opacity = fade;
2700
+ p.mesh.scale.multiplyScalar(1 - dt * 0.5);
2701
+ if (p.life <= 0) {
2702
+ (_a = this.scene) == null ? void 0 : _a.remove(p.mesh);
2703
+ (_b = p.mesh.material) == null ? void 0 : _b.dispose();
2704
+ this._miniEggs.splice(i, 1);
2705
+ }
2706
+ }
2707
+ }
2708
+ _updateFinale(dt) {
2709
+ var _a;
2710
+ if (!this._finaleActive || !this.THREE) return;
2711
+ this._finaleT += dt;
2712
+ const angle = this._finaleT * 0.35;
2713
+ const radius = 1.4;
2714
+ const eggScale = 0.55;
2715
+ for (let i = 0; i < 3; i++) {
2716
+ const group = this.eggs[i];
2717
+ if (!group) continue;
2718
+ group.visible = true;
2719
+ const eggAngle = angle + i * (Math.PI * 2 / 3);
2720
+ const tx = Math.sin(eggAngle) * radius;
2721
+ const ty = -0.1 + Math.sin(this._finaleT * 0.6 + i * 1.2) * 0.05;
2722
+ const tz = Math.cos(eggAngle) * radius * 0.3;
2723
+ group.position.x += (tx - group.position.x) * 0.06;
2724
+ group.position.y += (ty - group.position.y) * 0.06;
2725
+ group.position.z += (tz - group.position.z) * 0.06;
2726
+ group.rotation.y = this._finaleT * 0.4 + i * (Math.PI * 2 / 3);
2727
+ group.scale.setScalar(
2728
+ group.scale.x + (eggScale - group.scale.x) * 0.04
2729
+ );
2730
+ const body = this.eggBodies[i];
2731
+ if (((_a = body == null ? void 0 : body.material) == null ? void 0 : _a.emissiveIntensity) !== void 0) {
2732
+ const glow = 0.15 + Math.sin(this._finaleT * 2 + i * 1.5) * 0.08;
2733
+ body.material.emissiveIntensity += (glow - body.material.emissiveIntensity) * 0.05;
2734
+ }
2735
+ }
2736
+ if (this._finaleT > 5 && this._finaleT < 5.1 && this.renderer) {
2737
+ this.renderer.toneMappingExposure = 2.8;
2738
+ }
2739
+ if (this._finaleT > 5.1 && this._finaleT < 8 && this.renderer) {
2740
+ this.renderer.toneMappingExposure = Math.max(
2741
+ 1.1,
2742
+ 2.8 - (this._finaleT - 5.1) * 0.55
2743
+ );
2744
+ }
2745
+ }
2746
+ _updateTrails(dt) {
2747
+ for (let i = this._trailParticles.length - 1; i >= 0; i--) {
2748
+ const p = this._trailParticles[i];
2749
+ p.life -= dt * 0.5;
2750
+ if (p.life <= 0) {
2751
+ this.scene.remove(p.mesh);
2752
+ p.mesh.geometry.dispose();
2753
+ p.mesh.material.dispose();
2754
+ this._trailParticles.splice(i, 1);
2755
+ } else {
2756
+ p.mesh.material.opacity = p.life * 0.65;
2757
+ p.mesh.position.y += dt * 0.15;
2758
+ p.mesh.scale.setScalar(p.life);
2759
+ p.mesh.rotation.z += dt * 0.8;
2760
+ }
2761
+ }
2762
+ }
2763
+ _updateAmbientParticles(dt) {
2764
+ for (let i = this._ambientParticles.length - 1; i >= 0; i--) {
2765
+ const p = this._ambientParticles[i];
2766
+ p.life -= dt * 0.25;
2767
+ p.mesh.position.y += p.vy * dt;
2768
+ p.mesh.rotation.z += dt * 0.5;
2769
+ if (p.fadeIn && p.life > 0.7) {
2770
+ p.mesh.material.opacity = Math.min(
2771
+ 0.55,
2772
+ p.mesh.material.opacity + dt * 0.6
2773
+ );
2774
+ if (p.mesh.material.opacity >= 0.5) p.fadeIn = false;
2775
+ } else {
2776
+ p.mesh.material.opacity = Math.max(0, p.life * 0.55);
2777
+ }
2778
+ if (p.life <= 0) {
2779
+ this.scene.remove(p.mesh);
2780
+ p.mesh.geometry.dispose();
2781
+ p.mesh.material.dispose();
2782
+ this._ambientParticles.splice(i, 1);
2783
+ }
2784
+ }
2785
+ }
2786
+ _updateRevealParticles(dt) {
2787
+ for (let i = this._revealParticles.length - 1; i >= 0; i--) {
2788
+ const p = this._revealParticles[i];
2789
+ p.life -= dt * 0.6;
2790
+ p.mesh.position.x += p.vx * dt;
2791
+ p.mesh.position.y += p.vy * dt;
2792
+ p.mesh.position.z += p.vz * dt;
2793
+ p.vy -= dt * 1.5;
2794
+ p.mesh.material.opacity = Math.max(0, p.life * 0.8);
2795
+ p.mesh.scale.setScalar(Math.max(0.01, p.life));
2796
+ if (p.life <= 0) {
2797
+ this.scene.remove(p.mesh);
2798
+ p.mesh.geometry.dispose();
2799
+ p.mesh.material.dispose();
2800
+ this._revealParticles.splice(i, 1);
2801
+ }
2802
+ }
2803
+ }
2804
+ }
2805
+ class FallbackRenderer {
2806
+ constructor(config) {
2807
+ this.host = null;
2808
+ this.shadow = null;
2809
+ this.container = null;
2810
+ this.eggs = [];
2811
+ this._finaleActive = false;
2812
+ this.config = config;
2813
+ }
2814
+ async init() {
2815
+ this.host = document.createElement("div");
2816
+ this.host.id = "eeq-fallback-host";
2817
+ Object.assign(this.host.style, {
2818
+ position: "fixed",
2819
+ top: "0",
2820
+ left: "0",
2821
+ width: "100%",
2822
+ height: "100%",
2823
+ pointerEvents: "none",
2824
+ zIndex: "999992"
2825
+ });
2826
+ document.body.appendChild(this.host);
2827
+ this.shadow = this.host.attachShadow({ mode: "closed" });
2828
+ const style = document.createElement("style");
2829
+ style.textContent = this._getStyles();
2830
+ this.shadow.appendChild(style);
2831
+ this.container = document.createElement("div");
2832
+ this.container.className = "eeq-f-container";
2833
+ this.shadow.appendChild(this.container);
2834
+ for (let i = 0; i < 3; i++) {
2835
+ const egg = document.createElement("div");
2836
+ egg.className = `eeq-f-egg eeq-f-egg-${i + 1} eeq-f-hidden`;
2837
+ const inner = document.createElement("div");
2838
+ inner.className = "eeq-f-egg-inner";
2839
+ const shine = document.createElement("div");
2840
+ shine.className = "eeq-f-egg-shine";
2841
+ inner.appendChild(shine);
2842
+ egg.appendChild(inner);
2843
+ this.container.appendChild(egg);
2844
+ this.eggs.push(egg);
2845
+ }
2846
+ return true;
2847
+ }
2848
+ startRenderLoop() {
2849
+ }
2850
+ stopRenderLoop() {
2851
+ }
2852
+ revealEgg(index) {
2853
+ const egg = this.eggs[index];
2854
+ if (!egg) return;
2855
+ egg.classList.remove("eeq-f-hidden");
2856
+ egg.classList.add("eeq-f-reveal");
2857
+ }
2858
+ collectEgg(index) {
2859
+ const egg = this.eggs[index];
2860
+ if (!egg) return;
2861
+ egg.classList.remove("eeq-f-reveal");
2862
+ egg.classList.add("eeq-f-collected");
2863
+ }
2864
+ addTrail(x, y, _velocity) {
2865
+ if (!this.container) return;
2866
+ const dot = document.createElement("div");
2867
+ dot.className = "eeq-f-trail";
2868
+ const hue = Math.random() * 360;
2869
+ dot.style.background = `hsl(${hue}, 50%, 78%)`;
2870
+ dot.style.left = `${x}px`;
2871
+ dot.style.top = `${y}px`;
2872
+ this.container.appendChild(dot);
2873
+ setTimeout(() => dot.remove(), 1200);
2874
+ }
2875
+ startFinale() {
2876
+ this._finaleActive = true;
2877
+ for (const egg of this.eggs) {
2878
+ egg.classList.remove("eeq-f-collected", "eeq-f-hidden");
2879
+ egg.classList.add("eeq-f-finale");
2880
+ }
2881
+ if (this.container) {
2882
+ this.container.classList.add("eeq-f-finale-active");
2883
+ }
2884
+ }
2885
+ setBreathIntensity(_intensity) {
2886
+ }
2887
+ setOverlayIntensity(_progress) {
2888
+ }
2889
+ destroy() {
2890
+ var _a;
2891
+ (_a = this.host) == null ? void 0 : _a.remove();
2892
+ this.host = null;
2893
+ this.shadow = null;
2894
+ this.container = null;
2895
+ this.eggs = [];
2896
+ }
2897
+ _getStyles() {
2898
+ const t = this.config.theme;
2899
+ return `
2900
+ .eeq-f-container {
2901
+ position: fixed;
2902
+ top: 0; left: 0;
2903
+ width: 100%; height: 100%;
2904
+ display: flex;
2905
+ align-items: center;
2906
+ justify-content: center;
2907
+ gap: 32px;
2908
+ pointer-events: none;
2909
+ }
2910
+
2911
+ .eeq-f-egg {
2912
+ width: 60px;
2913
+ height: 80px;
2914
+ border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
2915
+ position: relative;
2916
+ transition: all 1.5s cubic-bezier(0.22, 1, 0.36, 1);
2917
+ }
2918
+
2919
+ .eeq-f-hidden {
2920
+ opacity: 0;
2921
+ transform: scale(0.1);
2922
+ }
2923
+
2924
+ .eeq-f-reveal {
2925
+ opacity: 1;
2926
+ transform: scale(1);
2927
+ animation: eeq-f-float 3s ease-in-out infinite;
2928
+ }
2929
+
2930
+ .eeq-f-collected {
2931
+ opacity: 0.6;
2932
+ transform: scale(0.5) translateY(200px);
2933
+ position: fixed;
2934
+ bottom: 30px;
2935
+ }
2936
+
2937
+ .eeq-f-finale {
2938
+ opacity: 1;
2939
+ transform: scale(1);
2940
+ animation: eeq-f-finale-spin 3s linear infinite;
2941
+ }
2942
+
2943
+ .eeq-f-finale-active {
2944
+ animation: eeq-f-glow-bg 2s ease-in-out forwards;
2945
+ }
2946
+
2947
+ .eeq-f-egg-inner {
2948
+ width: 100%;
2949
+ height: 100%;
2950
+ border-radius: inherit;
2951
+ position: relative;
2952
+ overflow: hidden;
2953
+ }
2954
+
2955
+ .eeq-f-egg-shine {
2956
+ position: absolute;
2957
+ top: 15%;
2958
+ left: 20%;
2959
+ width: 30%;
2960
+ height: 25%;
2961
+ border-radius: 50%;
2962
+ background: rgba(255,255,255,0.4);
2963
+ filter: blur(4px);
2964
+ }
2965
+
2966
+ /* Egg 1: Porcelain */
2967
+ .eeq-f-egg-1 .eeq-f-egg-inner {
2968
+ background: linear-gradient(145deg, #f5f0eb 0%, #e8e0d8 50%, #d8d0c8 100%);
2969
+ box-shadow: 0 4px 24px rgba(212,165,116,0.2), inset 0 2px 8px rgba(255,255,255,0.3);
2970
+ }
2971
+
2972
+ /* Egg 2: Luminous pearl */
2973
+ .eeq-f-egg-2 .eeq-f-egg-inner {
2974
+ background: linear-gradient(145deg, #faf5f0 0%, #f0e8e0 40%, #e5ddd5 100%);
2975
+ box-shadow: 0 4px 28px rgba(212,165,116,0.3), inset 0 2px 12px rgba(255,255,255,0.4);
2976
+ }
2977
+
2978
+ /* Egg 3: Golden core */
2979
+ .eeq-f-egg-3 .eeq-f-egg-inner {
2980
+ background: linear-gradient(145deg, #fff8f0 0%, #f5e8d5 40%, ${t.accent} 100%);
2981
+ box-shadow: 0 4px 32px ${t.accentGlow}, inset 0 2px 16px rgba(255,255,255,0.4);
2982
+ }
2983
+
2984
+ .eeq-f-trail {
2985
+ position: fixed;
2986
+ width: 8px;
2987
+ height: 11px;
2988
+ border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
2989
+ background: ${t.accent};
2990
+ opacity: 0.4;
2991
+ pointer-events: none;
2992
+ animation: eeq-f-trail-fade 1.2s ease-out forwards;
2993
+ }
2994
+
2995
+ @keyframes eeq-f-float {
2996
+ 0%, 100% { transform: scale(1) translateY(0); }
2997
+ 50% { transform: scale(1) translateY(-8px); }
2998
+ }
2999
+
3000
+ @keyframes eeq-f-finale-spin {
3001
+ from { transform: scale(1) rotate(0deg); }
3002
+ to { transform: scale(1) rotate(360deg); }
3003
+ }
3004
+
3005
+ @keyframes eeq-f-trail-fade {
3006
+ 0% { opacity: 0.5; transform: scale(1); }
3007
+ 100% { opacity: 0; transform: scale(0.2) translateY(-10px); }
3008
+ }
3009
+
3010
+ @keyframes eeq-f-glow-bg {
3011
+ 0% { background: transparent; }
3012
+ 50% { background: rgba(212,165,116,0.08); }
3013
+ 100% { background: transparent; }
3014
+ }
3015
+ `;
3016
+ }
3017
+ }
3018
+ class OverlayManager {
3019
+ constructor(config) {
3020
+ this.overlay = null;
3021
+ this._visible = false;
3022
+ this.config = config;
3023
+ }
3024
+ mount() {
3025
+ this.overlay = document.createElement("div");
3026
+ this.overlay.id = "eeq-overlay";
3027
+ Object.assign(this.overlay.style, {
3028
+ position: "fixed",
3029
+ top: "0",
3030
+ left: "0",
3031
+ width: "100%",
3032
+ height: "100%",
3033
+ background: "transparent",
3034
+ backdropFilter: "none",
3035
+ WebkitBackdropFilter: "none",
3036
+ zIndex: "999985",
3037
+ pointerEvents: "none",
3038
+ transition: "background 1.5s ease, backdrop-filter 1.5s ease",
3039
+ opacity: "0"
3040
+ });
3041
+ document.body.appendChild(this.overlay);
3042
+ }
3043
+ /** Fade in the overlay. */
3044
+ show() {
3045
+ if (!this.overlay || this._visible) return;
3046
+ this._visible = true;
3047
+ requestAnimationFrame(() => {
3048
+ if (!this.overlay) return;
3049
+ this.overlay.style.opacity = "1";
3050
+ this.overlay.style.background = this.config.theme.overlayBg;
3051
+ this.overlay.style.backdropFilter = "blur(2px)";
3052
+ this.overlay.style.WebkitBackdropFilter = "blur(2px)";
3053
+ });
3054
+ }
3055
+ /** Adjust overlay intensity for visual softening (0–1). */
3056
+ setIntensity(progress) {
3057
+ if (!this.overlay) return;
3058
+ const baseAlpha = 0.25;
3059
+ const maxAlpha = 0.5;
3060
+ const alpha = baseAlpha + progress * (maxAlpha - baseAlpha);
3061
+ this.overlay.style.background = `rgba(0,0,0,${alpha})`;
3062
+ const blur = 2 + progress * 4;
3063
+ this.overlay.style.backdropFilter = `blur(${blur}px)`;
3064
+ this.overlay.style.WebkitBackdropFilter = `blur(${blur}px)`;
3065
+ }
3066
+ /** Fade out the overlay. */
3067
+ hide() {
3068
+ if (!this.overlay) return;
3069
+ this._visible = false;
3070
+ this.overlay.style.opacity = "0";
3071
+ this.overlay.style.background = "transparent";
3072
+ this.overlay.style.backdropFilter = "none";
3073
+ this.overlay.style.WebkitBackdropFilter = "none";
3074
+ }
3075
+ destroy() {
3076
+ var _a;
3077
+ (_a = this.overlay) == null ? void 0 : _a.remove();
3078
+ this.overlay = null;
3079
+ }
3080
+ }
3081
+ const _ResultsRenderer = class _ResultsRenderer {
3082
+ constructor(config) {
3083
+ this.host = null;
3084
+ this.shadow = null;
3085
+ this._onFinish = null;
3086
+ this.config = config;
3087
+ }
3088
+ show(score, onFinish) {
3089
+ var _a, _b;
3090
+ this._onFinish = onFinish;
3091
+ this.host = document.createElement("div");
3092
+ this.host.id = "eeq-results-host";
3093
+ Object.assign(this.host.style, {
3094
+ position: "fixed",
3095
+ top: "0",
3096
+ left: "0",
3097
+ width: "100%",
3098
+ height: "100%",
3099
+ zIndex: "999998",
3100
+ display: "flex",
3101
+ alignItems: "center",
3102
+ justifyContent: "center",
3103
+ pointerEvents: "none"
3104
+ });
3105
+ document.body.appendChild(this.host);
3106
+ this.shadow = this.host.attachShadow({ mode: "closed" });
3107
+ const style = document.createElement("style");
3108
+ style.textContent = this._getStyles();
3109
+ this.shadow.appendChild(style);
3110
+ const panel = document.createElement("div");
3111
+ panel.className = "eeq-results";
3112
+ panel.innerHTML = this._buildContent(score);
3113
+ this.shadow.appendChild(panel);
3114
+ requestAnimationFrame(() => {
3115
+ requestAnimationFrame(() => {
3116
+ panel.classList.add("eeq-results-visible");
3117
+ });
3118
+ });
3119
+ (_a = this.shadow.querySelector(".eeq-btn-finish")) == null ? void 0 : _a.addEventListener("click", () => {
3120
+ var _a2;
3121
+ return (_a2 = this._onFinish) == null ? void 0 : _a2.call(this);
3122
+ });
3123
+ (_b = this.shadow.querySelector(".eeq-btn-share")) == null ? void 0 : _b.addEventListener("click", () => this._copyShareImage(score));
3124
+ }
3125
+ destroy() {
3126
+ var _a;
3127
+ (_a = this.host) == null ? void 0 : _a.remove();
3128
+ this.host = null;
3129
+ this.shadow = null;
3130
+ }
3131
+ // ─── Content ───────────────────────────────────────────────────────────
3132
+ _buildContent(s) {
3133
+ const bp = s.behaviorProfile;
3134
+ const actionIcons = {
3135
+ clicker: "🖱",
3136
+ scroller: "📜",
3137
+ typist: "⌨",
3138
+ minimalist: "🧘",
3139
+ mover: "🖱"
3140
+ };
3141
+ const mouseIcons = {
3142
+ tornado: "🌪",
3143
+ conductor: "🎼",
3144
+ surgeon: "🎯",
3145
+ ghost: "👻"
3146
+ };
3147
+ const paceIcons = {
3148
+ speedster: "⚡",
3149
+ steady: "🚶",
3150
+ explorer: "🧭",
3151
+ philosopher: "🕊"
3152
+ };
3153
+ const badges = [];
3154
+ const ai = actionIcons[bp.dominantAction] ?? "";
3155
+ const mi = mouseIcons[bp.mousePersonality] ?? "";
3156
+ const pi = paceIcons[bp.pace] ?? "";
3157
+ if (ai) badges.push(`<span class="eeq-badge" title="${bp.dominantAction}">${ai}</span>`);
3158
+ if (mi) badges.push(`<span class="eeq-badge" title="${bp.mousePersonality}">${mi}</span>`);
3159
+ if (pi) badges.push(`<span class="eeq-badge" title="${bp.pace}">${pi}</span>`);
3160
+ return `
3161
+ <div class="eeq-results-inner">
3162
+ <div class="eeq-results-title">« ${this._escapeHtml(bp.title)} »</div>
3163
+ <div class="eeq-time">${formatTime(s.totalTime)}</div>
3164
+ <div class="eeq-badges">${badges.join("")}</div>
3165
+ <div class="eeq-share-invite">share your result with friends 🥚</div>
3166
+ <div class="eeq-results-actions">
3167
+ <button class="eeq-btn eeq-btn-share" title="share result">
3168
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4.5 10.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm7-4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" stroke="currentColor" stroke-width="1.2"/><path d="M6.3 9.2l3.4 1.6M6.3 7.8l3.4-1.6" stroke="currentColor" stroke-width="1.2"/></svg>
3169
+ <span>share</span>
3170
+ </button>
3171
+ <button class="eeq-btn eeq-btn-finish" title="finish">
3172
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 8.5l3 3 5-6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
3173
+ <span>finish</span>
3174
+ </button>
3175
+ </div>
3176
+ </div>
3177
+ `;
3178
+ }
3179
+ // ─── 2D Egg Drawing (type-accurate) ─────────────────────────────────
3180
+ _makeRand(seed) {
3181
+ let s = seed;
3182
+ return () => {
3183
+ s = s + 1831565813 | 0;
3184
+ let t = Math.imul(s ^ s >>> 15, 1 | s);
3185
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3186
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3187
+ };
3188
+ }
3189
+ /** Draw a type-accurate egg on canvas. eggIdx: 0=silver, 1=gold, 2=festive. */
3190
+ _drawTypedEgg2D(canvas, eggIdx, found, seed) {
3191
+ const w = 72;
3192
+ const h = 96;
3193
+ const dpr = Math.min(window.devicePixelRatio || 1, 3);
3194
+ canvas.width = w * dpr;
3195
+ canvas.height = h * dpr;
3196
+ canvas.style.width = `${w}px`;
3197
+ canvas.style.height = `${h}px`;
3198
+ const ctx = canvas.getContext("2d");
3199
+ ctx.scale(dpr, dpr);
3200
+ const cx = w / 2;
3201
+ const cy = h / 2;
3202
+ const rx = w * 0.42;
3203
+ const ry = h * 0.46;
3204
+ if (!found) {
3205
+ this._drawEggOutline(ctx, cx, cy, rx, ry, true);
3206
+ return;
3207
+ }
3208
+ if (eggIdx === 0) this._paintSilverEgg(ctx, cx, cy, rx, ry);
3209
+ else if (eggIdx === 1) this._paintGoldEgg(ctx, cx, cy, rx, ry);
3210
+ else this._paintFestiveEgg(ctx, cx, cy, rx, ry, seed);
3211
+ }
3212
+ /** Egg path helper — builds egg-shaped path. */
3213
+ _eggPath(ctx, cx, cy, rx, ry) {
3214
+ ctx.beginPath();
3215
+ for (let a = 0; a <= Math.PI * 2; a += 0.02) {
3216
+ const r = 1 - 0.15 * Math.sin(a);
3217
+ const x = cx + Math.cos(a) * rx * r;
3218
+ const y = cy - Math.sin(a) * ry;
3219
+ if (a === 0) ctx.moveTo(x, y);
3220
+ else ctx.lineTo(x, y);
3221
+ }
3222
+ ctx.closePath();
3223
+ }
3224
+ /** Dashed or solid egg outline. */
3225
+ _drawEggOutline(ctx, cx, cy, rx, ry, dashed) {
3226
+ ctx.save();
3227
+ if (dashed) ctx.setLineDash([5, 4]);
3228
+ this._eggPath(ctx, cx, cy, rx, ry);
3229
+ ctx.strokeStyle = "rgba(255,255,255,0.15)";
3230
+ ctx.lineWidth = 1.5;
3231
+ ctx.stroke();
3232
+ ctx.restore();
3233
+ }
3234
+ /** Specular highlight on top-left of egg. */
3235
+ _drawSpecular(ctx, cx, cy, rx, ry) {
3236
+ const hl = ctx.createRadialGradient(cx - rx * 0.25, cy - ry * 0.4, 0, cx - rx * 0.25, cy - ry * 0.4, rx * 0.7);
3237
+ hl.addColorStop(0, "rgba(255,255,255,0.4)");
3238
+ hl.addColorStop(1, "rgba(255,255,255,0)");
3239
+ ctx.fillStyle = hl;
3240
+ ctx.fillRect(cx - rx - 2, cy - ry - 2, rx * 2 + 4, ry * 2 + 4);
3241
+ }
3242
+ /** Silver egg — cool chrome gradient with white polka dots and metallic sheen. */
3243
+ _paintSilverEgg(ctx, cx, cy, rx, ry) {
3244
+ ctx.save();
3245
+ this._eggPath(ctx, cx, cy, rx, ry);
3246
+ ctx.clip();
3247
+ const bg = ctx.createLinearGradient(cx, cy - ry, cx, cy + ry);
3248
+ bg.addColorStop(0, "#e8ecf2");
3249
+ bg.addColorStop(0.3, "#dde1e8");
3250
+ bg.addColorStop(0.6, "#d0d4dc");
3251
+ bg.addColorStop(1, "#c4c0bc");
3252
+ ctx.fillStyle = bg;
3253
+ ctx.fillRect(cx - rx - 2, cy - ry - 2, rx * 2 + 4, ry * 2 + 4);
3254
+ const dotR = 3.5;
3255
+ const spX = 12;
3256
+ const spY = 10;
3257
+ ctx.fillStyle = "rgba(255,255,255,0.55)";
3258
+ for (let row = -1; row < ry * 2 / spY + 2; row++) {
3259
+ const offX = row % 2 * (spX / 2);
3260
+ for (let col = -1; col < rx * 2 / spX + 2; col++) {
3261
+ const dx = cx - rx + col * spX + offX;
3262
+ const dy = cy - ry + row * spY;
3263
+ ctx.beginPath();
3264
+ ctx.arc(dx, dy, dotR, 0, Math.PI * 2);
3265
+ ctx.fill();
3266
+ }
3267
+ }
3268
+ const sheen = ctx.createLinearGradient(cx - rx, cy - ry * 0.2, cx + rx, cy + ry * 0.2);
3269
+ sheen.addColorStop(0, "rgba(255,255,255,0)");
3270
+ sheen.addColorStop(0.4, "rgba(255,255,255,0.12)");
3271
+ sheen.addColorStop(0.6, "rgba(255,255,255,0.12)");
3272
+ sheen.addColorStop(1, "rgba(255,255,255,0)");
3273
+ ctx.fillStyle = sheen;
3274
+ ctx.fillRect(cx - rx - 2, cy - ry - 2, rx * 2 + 4, ry * 2 + 4);
3275
+ this._drawSpecular(ctx, cx, cy, rx, ry);
3276
+ ctx.restore();
3277
+ ctx.save();
3278
+ this._eggPath(ctx, cx, cy, rx, ry);
3279
+ ctx.strokeStyle = "rgba(200,205,215,0.5)";
3280
+ ctx.lineWidth = 1.2;
3281
+ ctx.stroke();
3282
+ ctx.restore();
3283
+ }
3284
+ /** Gold egg — warm yellow gradient, horizontal ornate bands, diamond lattice. */
3285
+ _paintGoldEgg(ctx, cx, cy, rx, ry) {
3286
+ ctx.save();
3287
+ this._eggPath(ctx, cx, cy, rx, ry);
3288
+ ctx.clip();
3289
+ const bg = ctx.createLinearGradient(cx, cy - ry, cx, cy + ry);
3290
+ bg.addColorStop(0, "#d4a520");
3291
+ bg.addColorStop(0.25, "#e6c040");
3292
+ bg.addColorStop(0.5, "#daa520");
3293
+ bg.addColorStop(0.75, "#c89618");
3294
+ bg.addColorStop(1, "#b8860b");
3295
+ ctx.fillStyle = bg;
3296
+ ctx.fillRect(cx - rx - 2, cy - ry - 2, rx * 2 + 4, ry * 2 + 4);
3297
+ ctx.globalAlpha = 0.25;
3298
+ for (const yRatio of [0.28, 0.5, 0.72]) {
3299
+ const y = cy - ry + yRatio * ry * 2;
3300
+ const bandH = 5;
3301
+ const bg2 = ctx.createLinearGradient(cx, y - bandH, cx, y + bandH);
3302
+ bg2.addColorStop(0, "#b8860b");
3303
+ bg2.addColorStop(0.5, "#f0d060");
3304
+ bg2.addColorStop(1, "#b8860b");
3305
+ ctx.fillStyle = bg2;
3306
+ ctx.fillRect(cx - rx - 2, y - bandH, rx * 2 + 4, bandH * 2);
3307
+ }
3308
+ ctx.globalAlpha = 1;
3309
+ ctx.strokeStyle = "rgba(160,112,8,0.18)";
3310
+ ctx.lineWidth = 0.8;
3311
+ const step = 8;
3312
+ for (let x = cx - rx * 2; x < cx + rx * 2; x += step) {
3313
+ ctx.beginPath();
3314
+ ctx.moveTo(x, cy - ry);
3315
+ ctx.lineTo(x + ry * 2, cy + ry);
3316
+ ctx.stroke();
3317
+ ctx.beginPath();
3318
+ ctx.moveTo(x + ry * 2, cy - ry);
3319
+ ctx.lineTo(x, cy + ry);
3320
+ ctx.stroke();
3321
+ }
3322
+ ctx.strokeStyle = "rgba(212,165,16,0.35)";
3323
+ ctx.lineWidth = 0.7;
3324
+ for (const yRatio of [0.24, 0.32, 0.46, 0.54, 0.68, 0.76]) {
3325
+ const y = cy - ry + yRatio * ry * 2;
3326
+ ctx.beginPath();
3327
+ ctx.moveTo(cx - rx, y);
3328
+ ctx.lineTo(cx + rx, y);
3329
+ ctx.stroke();
3330
+ }
3331
+ this._drawSpecular(ctx, cx, cy, rx, ry);
3332
+ ctx.restore();
3333
+ ctx.save();
3334
+ this._eggPath(ctx, cx, cy, rx, ry);
3335
+ ctx.strokeStyle = "rgba(200,165,30,0.5)";
3336
+ ctx.lineWidth = 1.2;
3337
+ ctx.stroke();
3338
+ ctx.restore();
3339
+ }
3340
+ /** Festive Fabergé egg — seeded enamel bands + golden dividers + gem dots. */
3341
+ _paintFestiveEgg(ctx, cx, cy, rx, ry, seed) {
3342
+ const rand = this._makeRand(seed);
3343
+ ctx.save();
3344
+ this._eggPath(ctx, cx, cy, rx, ry);
3345
+ ctx.clip();
3346
+ const hueBase = rand() * 360;
3347
+ const enamelHues = [];
3348
+ for (let i = 0; i < 5; i++) enamelHues.push((hueBase + i * (50 + rand() * 30)) % 360);
3349
+ const bandCount = 9;
3350
+ const fullH = ry * 2;
3351
+ const bandH = fullH / bandCount;
3352
+ const top = cy - ry;
3353
+ for (let i = 0; i < bandCount; i++) {
3354
+ const isGold = i % 2 === 1;
3355
+ const y = top + i * bandH;
3356
+ if (isGold) {
3357
+ const gl = 52 + rand() * 10;
3358
+ const gb = ctx.createLinearGradient(cx, y, cx, y + bandH);
3359
+ gb.addColorStop(0, `hsl(43, 80%, ${gl}%)`);
3360
+ gb.addColorStop(1, `hsl(43, 75%, ${gl - 6}%)`);
3361
+ ctx.fillStyle = gb;
3362
+ } else {
3363
+ const eIdx = Math.floor(i / 2);
3364
+ const hue = enamelHues[eIdx % enamelHues.length];
3365
+ const sat = 55 + rand() * 30;
3366
+ const light = 35 + rand() * 15;
3367
+ const eb = ctx.createLinearGradient(cx, y, cx, y + bandH);
3368
+ eb.addColorStop(0, `hsl(${hue}, ${sat}%, ${light}%)`);
3369
+ eb.addColorStop(1, `hsl(${hue}, ${sat}%, ${light - 8}%)`);
3370
+ ctx.fillStyle = eb;
3371
+ }
3372
+ ctx.fillRect(cx - rx - 2, y, rx * 2 + 4, bandH + 1);
3373
+ }
3374
+ const gemCount = 5 + Math.floor(rand() * 4);
3375
+ for (let di = 1; di < bandCount; di += 2) {
3376
+ const gemY = top + (di + 0.5) * bandH;
3377
+ for (let j = 0; j < gemCount; j++) {
3378
+ const gx = cx - rx * 0.7 + (j + 0.5) * (rx * 1.4 / gemCount);
3379
+ const gh = enamelHues[Math.floor(rand() * enamelHues.length)];
3380
+ ctx.fillStyle = `hsl(${gh}, 70%, 55%)`;
3381
+ ctx.beginPath();
3382
+ ctx.arc(gx, gemY, 1.5 + rand(), 0, Math.PI * 2);
3383
+ ctx.fill();
3384
+ }
3385
+ }
3386
+ this._drawSpecular(ctx, cx, cy, rx, ry);
3387
+ ctx.restore();
3388
+ ctx.save();
3389
+ this._eggPath(ctx, cx, cy, rx, ry);
3390
+ ctx.strokeStyle = "rgba(200,150,100,0.45)";
3391
+ ctx.lineWidth = 1.2;
3392
+ ctx.stroke();
3393
+ ctx.restore();
3394
+ }
3395
+ // ─── Combined copy & share ──────────────────────────────────────────
3396
+ _copyShareImage(s) {
3397
+ const canvas = this._renderCard(s);
3398
+ canvas.toBlob((blob) => {
3399
+ var _a;
3400
+ if (!blob) return;
3401
+ const isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) && window.innerWidth < 768;
3402
+ if (isMobile && navigator.share) {
3403
+ const file = new File([blob], "easter-egg-quest.png", { type: "image/png" });
3404
+ if ((_a = navigator.canShare) == null ? void 0 : _a.call(navigator, { files: [file] })) {
3405
+ navigator.share({
3406
+ files: [file],
3407
+ title: "Easter Egg Quest",
3408
+ text: `«${s.behaviorProfile.title}»`
3409
+ }).catch(() => {
3410
+ });
3411
+ return;
3412
+ }
3413
+ }
3414
+ if (navigator.clipboard && typeof ClipboardItem !== "undefined") {
3415
+ navigator.clipboard.write([
3416
+ new ClipboardItem({ "image/png": blob })
3417
+ ]).then(() => {
3418
+ this._flashButton(".eeq-btn-share", "copied!");
3419
+ }).catch(() => {
3420
+ this._downloadBlob(blob);
3421
+ });
3422
+ return;
3423
+ }
3424
+ this._downloadBlob(blob);
3425
+ }, "image/png");
3426
+ }
3427
+ _downloadBlob(blob) {
3428
+ const url = URL.createObjectURL(blob);
3429
+ const a = document.createElement("a");
3430
+ a.href = url;
3431
+ a.download = "easter-egg-quest.png";
3432
+ a.click();
3433
+ setTimeout(() => URL.revokeObjectURL(url), 5e3);
3434
+ this._flashButton(".eeq-btn-share", "saved!");
3435
+ }
3436
+ _renderCard(s) {
3437
+ const W = 480;
3438
+ const H = 400;
3439
+ const dpr = Math.min(window.devicePixelRatio || 1, 3);
3440
+ const c = document.createElement("canvas");
3441
+ c.width = W * dpr;
3442
+ c.height = H * dpr;
3443
+ const ctx = c.getContext("2d");
3444
+ ctx.scale(dpr, dpr);
3445
+ const grad = ctx.createLinearGradient(0, 0, 0, H);
3446
+ grad.addColorStop(0, "#1e1815");
3447
+ grad.addColorStop(0.5, "#14110e");
3448
+ grad.addColorStop(1, "#0d0b09");
3449
+ ctx.fillStyle = grad;
3450
+ this._roundRect(ctx, 0, 0, W, H, 16);
3451
+ ctx.fill();
3452
+ ctx.strokeStyle = "rgba(212,165,80,0.18)";
3453
+ ctx.lineWidth = 1;
3454
+ this._roundRect(ctx, 0.5, 0.5, W - 1, H - 1, 16);
3455
+ ctx.stroke();
3456
+ ctx.textAlign = "center";
3457
+ ctx.font = "11px Georgia, serif";
3458
+ ctx.fillStyle = "rgba(232,224,214,0.3)";
3459
+ ctx.fillText("🥚 Easter Egg Quest", W / 2, 28);
3460
+ const eggW = 48;
3461
+ const eggH = 64;
3462
+ const eggRx = eggW * 0.42;
3463
+ const eggRy = eggH * 0.46;
3464
+ const eggY = 72;
3465
+ const gap = 16;
3466
+ const totalRow = eggW * 3 + gap * 2;
3467
+ const startX = (W - totalRow) / 2 + eggW / 2;
3468
+ for (let i = 0; i < 3; i++) {
3469
+ const ecx = startX + i * (eggW + gap);
3470
+ const found = i < s.eggsFound;
3471
+ if (found) {
3472
+ if (i === 0) this._paintSilverEgg(ctx, ecx, eggY, eggRx, eggRy);
3473
+ else if (i === 1) this._paintGoldEgg(ctx, ecx, eggY, eggRx, eggRy);
3474
+ else this._paintFestiveEgg(ctx, ecx, eggY, eggRx, eggRy, s.eggSeed);
3475
+ } else {
3476
+ this._drawEggOutline(ctx, ecx, eggY, eggRx, eggRy, true);
3477
+ }
3478
+ }
3479
+ const countText = s.eggsFound === 3 ? "All eggs collected!" : `${s.eggsFound} of 3 eggs collected`;
3480
+ ctx.font = "10px Georgia, serif";
3481
+ ctx.fillStyle = "rgba(232,224,214,0.35)";
3482
+ ctx.fillText(countText, W / 2, eggY + eggRy + 18);
3483
+ const divY = eggY + eggRy + 30;
3484
+ ctx.strokeStyle = "rgba(212,165,80,0.12)";
3485
+ ctx.lineWidth = 0.5;
3486
+ ctx.beginPath();
3487
+ ctx.moveTo(W * 0.2, divY);
3488
+ ctx.lineTo(W * 0.8, divY);
3489
+ ctx.stroke();
3490
+ ctx.font = "italic 18px Georgia, serif";
3491
+ ctx.fillStyle = this.config.theme.accent;
3492
+ ctx.fillText(`« ${s.behaviorProfile.title} »`, W / 2, divY + 28);
3493
+ ctx.font = '32px "SF Mono", "Fira Code", Consolas, monospace';
3494
+ ctx.fillStyle = "#e8e0d6";
3495
+ ctx.fillText(formatTime(s.totalTime), W / 2, divY + 68);
3496
+ const labels = [
3497
+ s.behaviorProfile.dominantAction,
3498
+ s.behaviorProfile.mousePersonality,
3499
+ s.behaviorProfile.pace
3500
+ ];
3501
+ ctx.font = "12px Georgia, serif";
3502
+ ctx.fillStyle = "rgba(232,224,214,0.4)";
3503
+ ctx.fillText(labels.join(" · "), W / 2, divY + 92);
3504
+ ctx.font = "14px Georgia, serif";
3505
+ ctx.fillStyle = "rgba(232,224,214,0.55)";
3506
+ ctx.fillText("Happy Easter! 🐰🥚", W / 2, divY + 130);
3507
+ ctx.font = "11px Georgia, serif";
3508
+ ctx.fillStyle = "rgba(232,224,214,0.3)";
3509
+ ctx.fillText("Wishing you joy, peace, and light", W / 2, divY + 150);
3510
+ ctx.font = "9px Georgia, serif";
3511
+ ctx.fillStyle = "rgba(232,224,214,0.12)";
3512
+ ctx.fillText("easter egg quest", W / 2, H - 16);
3513
+ return c;
3514
+ }
3515
+ _roundRect(ctx, x, y, w, h, r) {
3516
+ ctx.beginPath();
3517
+ ctx.moveTo(x + r, y);
3518
+ ctx.lineTo(x + w - r, y);
3519
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
3520
+ ctx.lineTo(x + w, y + h - r);
3521
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
3522
+ ctx.lineTo(x + r, y + h);
3523
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
3524
+ ctx.lineTo(x, y + r);
3525
+ ctx.quadraticCurveTo(x, y, x + r, y);
3526
+ ctx.closePath();
3527
+ }
3528
+ // ─── Helpers ───────────────────────────────────────────────────────────
3529
+ _flashButton(selector, text) {
3530
+ var _a;
3531
+ const span = (_a = this.shadow) == null ? void 0 : _a.querySelector(`${selector} span`);
3532
+ if (!span) return;
3533
+ const orig = span.textContent;
3534
+ span.textContent = text;
3535
+ setTimeout(() => {
3536
+ span.textContent = orig;
3537
+ }, 1500);
3538
+ }
3539
+ _escapeHtml(s) {
3540
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3541
+ }
3542
+ // ─── Styles ────────────────────────────────────────────────────────────
3543
+ _getStyles() {
3544
+ const t = this.config.theme;
3545
+ return `
3546
+ .eeq-results {
3547
+ pointer-events: auto;
3548
+ background: linear-gradient(175deg, rgba(28,22,18,0.95) 0%, rgba(10,8,6,0.97) 100%);
3549
+ backdrop-filter: blur(28px);
3550
+ -webkit-backdrop-filter: blur(28px);
3551
+ border: 1px solid rgba(212,165,80,0.1);
3552
+ border-radius: 20px;
3553
+ padding: 28px 36px 24px;
3554
+ max-width: 320px;
3555
+ width: 85vw;
3556
+ opacity: 0;
3557
+ transform: translateY(24px) scale(0.95);
3558
+ transition: opacity 0.9s ease, transform 0.9s cubic-bezier(0.23, 1, 0.32, 1);
3559
+ font-family: 'Georgia', 'Times New Roman', serif;
3560
+ color: ${t.textColor};
3561
+ text-align: center;
3562
+ box-shadow: 0 24px 80px rgba(0,0,0,0.6), 0 4px 16px rgba(0,0,0,0.4),
3563
+ inset 0 1px 0 rgba(255,255,255,0.04);
3564
+ }
3565
+
3566
+ .eeq-results-visible {
3567
+ opacity: 1;
3568
+ transform: translateY(0) scale(1);
3569
+ }
3570
+
3571
+ .eeq-results-title {
3572
+ font-size: 18px;
3573
+ font-style: italic;
3574
+ letter-spacing: 0.04em;
3575
+ color: ${t.accent};
3576
+ margin-bottom: 10px;
3577
+ line-height: 1.35;
3578
+ text-shadow: 0 0 30px rgba(212,165,80,0.15);
3579
+ }
3580
+
3581
+ .eeq-time {
3582
+ font-size: 30px;
3583
+ font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
3584
+ letter-spacing: 0.06em;
3585
+ margin-bottom: 12px;
3586
+ background: linear-gradient(180deg, #f0ece6, #c8c0b4);
3587
+ -webkit-background-clip: text;
3588
+ -webkit-text-fill-color: transparent;
3589
+ background-clip: text;
3590
+ }
3591
+
3592
+ .eeq-badges {
3593
+ display: flex;
3594
+ justify-content: center;
3595
+ gap: 10px;
3596
+ margin-bottom: 12px;
3597
+ font-size: 18px;
3598
+ }
3599
+
3600
+ .eeq-share-invite {
3601
+ font-size: 13px;
3602
+ color: rgba(232,224,214,0.5);
3603
+ margin-bottom: 16px;
3604
+ letter-spacing: 0.03em;
3605
+ }
3606
+
3607
+ .eeq-badge {
3608
+ display: inline-flex;
3609
+ align-items: center;
3610
+ justify-content: center;
3611
+ width: 36px;
3612
+ height: 36px;
3613
+ border-radius: 50%;
3614
+ background: rgba(255,255,255,0.03);
3615
+ border: 1px solid rgba(255,255,255,0.07);
3616
+ cursor: default;
3617
+ transition: background 0.3s, border-color 0.3s;
3618
+ }
3619
+
3620
+ .eeq-badge:hover {
3621
+ background: rgba(212,165,80,0.08);
3622
+ border-color: rgba(212,165,80,0.2);
3623
+ }
3624
+
3625
+ .eeq-results-actions {
3626
+ display: flex;
3627
+ justify-content: center;
3628
+ gap: 10px;
3629
+ }
3630
+
3631
+ .eeq-btn {
3632
+ display: inline-flex;
3633
+ align-items: center;
3634
+ gap: 6px;
3635
+ background: rgba(255,255,255,0.02);
3636
+ border: 1px solid rgba(255,255,255,0.1);
3637
+ color: ${t.textColor};
3638
+ padding: 8px 18px;
3639
+ border-radius: 10px;
3640
+ font-family: 'Georgia', serif;
3641
+ font-size: 12px;
3642
+ letter-spacing: 0.04em;
3643
+ cursor: pointer;
3644
+ transition: all 0.3s ease;
3645
+ white-space: nowrap;
3646
+ }
3647
+
3648
+ .eeq-btn svg {
3649
+ flex-shrink: 0;
3650
+ opacity: 0.7;
3651
+ transition: opacity 0.3s;
3652
+ }
3653
+
3654
+ .eeq-btn:hover {
3655
+ border-color: ${t.accent};
3656
+ color: ${t.accent};
3657
+ background: rgba(212,165,80,0.06);
3658
+ box-shadow: 0 0 20px rgba(212,165,80,0.08);
3659
+ }
3660
+
3661
+ .eeq-btn:hover svg {
3662
+ opacity: 1;
3663
+ }
3664
+
3665
+ .eeq-btn:focus {
3666
+ outline: 2px solid ${t.accent};
3667
+ outline-offset: 2px;
3668
+ }
3669
+
3670
+ .eeq-btn:active {
3671
+ transform: scale(0.96);
3672
+ }
3673
+ `;
3674
+ }
3675
+ };
3676
+ _ResultsRenderer.EGG_PALETTES = [
3677
+ {
3678
+ // Silver — polished chrome with white polka dots
3679
+ base1: "#e0e4ea",
3680
+ base2: "#c8c4c0",
3681
+ accent: "#f4f4f6",
3682
+ outline: "rgba(200,200,210,0.6)",
3683
+ label: "Silver"
3684
+ },
3685
+ {
3686
+ // Gold — rich 24k with ornate bands
3687
+ base1: "#daa520",
3688
+ base2: "#b8860b",
3689
+ accent: "#f0d060",
3690
+ outline: "rgba(218,165,32,0.6)",
3691
+ label: "Gold"
3692
+ },
3693
+ {
3694
+ // Festive — colorful Fabergé enamel
3695
+ base1: "#c05070",
3696
+ base2: "#3060a0",
3697
+ accent: "#e6b422",
3698
+ outline: "rgba(200,100,120,0.6)",
3699
+ label: "Fabergé"
3700
+ }
3701
+ ];
3702
+ let ResultsRenderer = _ResultsRenderer;
3703
+ class ScoringEngine {
3704
+ constructor() {
3705
+ this._startTime = 0;
3706
+ this._entryTime = 0;
3707
+ this._stageResults = [];
3708
+ this._extraClicks = 0;
3709
+ this._chaoticMoveFrames = 0;
3710
+ this._hesitations = 0;
3711
+ this._failedAttempts = 0;
3712
+ this._overcorrections = 0;
3713
+ }
3714
+ start() {
3715
+ this._startTime = Date.now();
3716
+ this._stageResults = [];
3717
+ this._extraClicks = 0;
3718
+ this._chaoticMoveFrames = 0;
3719
+ this._hesitations = 0;
3720
+ this._failedAttempts = 0;
3721
+ this._overcorrections = 0;
3722
+ }
3723
+ recordEntryFound() {
3724
+ this._entryTime = Date.now() - this._startTime;
3725
+ }
3726
+ recordStageResult(result) {
3727
+ this._stageResults.push(result);
3728
+ this._failedAttempts += Math.max(0, result.attempts - 1);
3729
+ }
3730
+ /** Called each frame to analyse input patterns for predictability. */
3731
+ analyseFrame(snap) {
3732
+ if (snap.velocity > 2e3) this._chaoticMoveFrames++;
3733
+ }
3734
+ /** Increment extra clicks (clicks outside meaningful interaction). */
3735
+ recordExtraClick() {
3736
+ this._extraClicks++;
3737
+ }
3738
+ /** Record hesitation events (start-stop-start within 600 ms). */
3739
+ recordHesitation() {
3740
+ this._hesitations++;
3741
+ }
3742
+ recordOvercorrection() {
3743
+ this._overcorrections++;
3744
+ }
3745
+ computeFinalScore(inputStats) {
3746
+ const totalTime = Date.now() - this._startTime;
3747
+ const s1 = this._stageResults[0];
3748
+ const s2 = this._stageResults[1];
3749
+ const s3 = this._stageResults[2];
3750
+ const predictability = this._computePredictability();
3751
+ const clicks = (inputStats == null ? void 0 : inputStats.totalClicks) ?? 0;
3752
+ const scrolls = (inputStats == null ? void 0 : inputStats.totalScrolls) ?? 0;
3753
+ const keys = (inputStats == null ? void 0 : inputStats.totalKeyPresses) ?? 0;
3754
+ const distance = (inputStats == null ? void 0 : inputStats.totalDistance) ?? 0;
3755
+ const behaviorProfile = this._computeBehaviorProfile({
3756
+ totalTime,
3757
+ clicks,
3758
+ scrolls,
3759
+ keys,
3760
+ distance,
3761
+ maxVelocity: (inputStats == null ? void 0 : inputStats.maxVelocity) ?? 0,
3762
+ stillnessBreaks: (s1 == null ? void 0 : s1.breaks) ?? 0,
3763
+ motionFades: (s2 == null ? void 0 : s2.fades) ?? 0,
3764
+ rhythmAccuracy: (s3 == null ? void 0 : s3.rhythmAccuracy) ?? 0,
3765
+ predictabilityScore: predictability.score
3766
+ });
3767
+ return {
3768
+ eggsFound: this._stageResults.length,
3769
+ totalTime,
3770
+ entryTime: this._entryTime,
3771
+ egg1Time: (s1 == null ? void 0 : s1.stageTime) ?? 0,
3772
+ egg2Time: (s2 == null ? void 0 : s2.stageTime) ?? 0,
3773
+ egg3Time: (s3 == null ? void 0 : s3.stageTime) ?? 0,
3774
+ stillnessBreaks: (s1 == null ? void 0 : s1.breaks) ?? 0,
3775
+ motionFades: (s2 == null ? void 0 : s2.fades) ?? 0,
3776
+ rhythmAccuracy: (s3 == null ? void 0 : s3.rhythmAccuracy) ?? 0,
3777
+ predictabilityScore: predictability.score,
3778
+ predictabilityLabel: predictability.label,
3779
+ stageResults: this._stageResults,
3780
+ behaviorProfile,
3781
+ totalClicks: clicks,
3782
+ totalScrolls: scrolls,
3783
+ totalKeyPresses: keys,
3784
+ totalDistance: distance,
3785
+ eggSeed: Math.floor(Math.random() * 2147483647)
3786
+ };
3787
+ }
3788
+ // ─── Behavior Profile & Title ──────────────────────────────────────────
3789
+ _computeBehaviorProfile(d) {
3790
+ const actionMax = Math.max(d.clicks, d.scrolls, d.keys);
3791
+ let dominantAction;
3792
+ if (actionMax === 0) {
3793
+ dominantAction = "minimalist";
3794
+ } else if (d.clicks >= d.scrolls && d.clicks >= d.keys) {
3795
+ dominantAction = "clicker";
3796
+ } else if (d.scrolls >= d.clicks && d.scrolls >= d.keys) {
3797
+ dominantAction = "scroller";
3798
+ } else {
3799
+ dominantAction = "typist";
3800
+ }
3801
+ const dps = d.distance / Math.max(d.totalTime / 1e3, 1);
3802
+ let mousePersonality;
3803
+ if (dps > 600) mousePersonality = "tornado";
3804
+ else if (dps > 250) mousePersonality = "conductor";
3805
+ else if (dps > 60) mousePersonality = "surgeon";
3806
+ else mousePersonality = "ghost";
3807
+ const sec = d.totalTime / 1e3;
3808
+ let pace;
3809
+ if (sec < 45) pace = "speedster";
3810
+ else if (sec < 120) pace = "steady";
3811
+ else if (sec < 360) pace = "explorer";
3812
+ else pace = "philosopher";
3813
+ const title = this._pickTitle(d, dominantAction, mousePersonality, pace);
3814
+ return { title, dominantAction, mousePersonality, pace };
3815
+ }
3816
+ _pickTitle(d, dominant, mouse, pace) {
3817
+ const traits = [];
3818
+ const sec = d.totalTime / 1e3;
3819
+ if (sec < 30) traits.push({ score: 0.95, title: "Quantum Egg Locator" });
3820
+ else if (sec < 45) traits.push({ score: 0.8, title: "Easter Lightning" });
3821
+ else if (sec > 600) traits.push({ score: 0.85, title: "Egg Philosopher Supreme" });
3822
+ else if (sec > 360) traits.push({ score: 0.7, title: "Scenic Route Connoisseur" });
3823
+ const cpm = d.clicks / Math.max(sec / 60, 0.5);
3824
+ if (cpm > 40) traits.push({ score: 0.9, title: "The Click Machine" });
3825
+ else if (cpm > 20) traits.push({ score: 0.7, title: "Click Enthusiast" });
3826
+ else if (d.clicks <= 3 && sec > 30) traits.push({ score: 0.75, title: "Clickless Wonder" });
3827
+ if (d.scrolls > 100) traits.push({ score: 0.85, title: "Scroll Archaeologist" });
3828
+ else if (d.scrolls > 50) traits.push({ score: 0.65, title: "Infinite Scroller" });
3829
+ if (d.keys > 50) traits.push({ score: 0.8, title: "Keyboard Adventurer" });
3830
+ else if (d.keys > 20) traits.push({ score: 0.6, title: "Secret Code Seeker" });
3831
+ const km = d.distance / 1e3;
3832
+ if (km > 50) traits.push({ score: 0.85, title: "Mouse Marathon Runner" });
3833
+ else if (km > 20) traits.push({ score: 0.65, title: "Cursor Voyager" });
3834
+ else if (km < 2 && sec > 30) traits.push({ score: 0.7, title: "Laser Pointer Specialist" });
3835
+ if (d.maxVelocity > 8e3) traits.push({ score: 0.75, title: "Speed Demon" });
3836
+ if (d.predictabilityScore >= 92) traits.push({ score: 0.85, title: "Human Clockwork" });
3837
+ else if (d.predictabilityScore >= 80) traits.push({ score: 0.6, title: "The Algorithm" });
3838
+ else if (d.predictabilityScore <= 12) traits.push({ score: 0.9, title: "Agent of Chaos" });
3839
+ else if (d.predictabilityScore <= 25) traits.push({ score: 0.7, title: "Chaos Butterfly" });
3840
+ if (d.rhythmAccuracy >= 95) traits.push({ score: 0.85, title: "Rhythm Oracle" });
3841
+ else if (d.rhythmAccuracy >= 80) traits.push({ score: 0.6, title: "Beat Keeper" });
3842
+ else if (d.rhythmAccuracy < 20 && d.rhythmAccuracy > 0) traits.push({ score: 0.75, title: "Jazz Improviser" });
3843
+ if (d.stillnessBreaks === 0) traits.push({ score: 0.8, title: "Stone Buddha" });
3844
+ else if (d.stillnessBreaks >= 15) traits.push({ score: 0.7, title: "Caffeinated Explorer" });
3845
+ if (d.motionFades === 0 && sec > 20) traits.push({ score: 0.65, title: "Perpetual Motion" });
3846
+ if (pace === "speedster" && dominant === "clicker") traits.push({ score: 0.92, title: "Turbo Clicker" });
3847
+ if (pace === "philosopher" && mouse === "ghost") traits.push({ score: 0.88, title: "Meditation Champion" });
3848
+ if (mouse === "tornado" && dominant === "typist") traits.push({ score: 0.87, title: "Keyboard Tornado" });
3849
+ if (d.rhythmAccuracy >= 85 && d.clicks <= 5) traits.push({ score: 0.82, title: "Silent Perfectionist" });
3850
+ if (d.predictabilityScore <= 20 && pace === "speedster") traits.push({ score: 0.88, title: "Controlled Chaos" });
3851
+ if (dominant === "scroller" && pace === "explorer") traits.push({ score: 0.78, title: "Digital Archaeologist" });
3852
+ if (mouse === "conductor" && d.rhythmAccuracy >= 70) traits.push({ score: 0.8, title: "Cursor Choreographer" });
3853
+ if (dominant === "minimalist" && pace === "steady") traits.push({ score: 0.75, title: "Zen Navigator" });
3854
+ if (d.stillnessBreaks >= 10 && d.rhythmAccuracy >= 80) traits.push({ score: 0.78, title: "Restless Virtuoso" });
3855
+ if (km > 30 && pace === "speedster") traits.push({ score: 0.82, title: "Hyperdrive Activated" });
3856
+ traits.sort((a, b) => b.score - a.score);
3857
+ if (traits.length > 0) return traits[0].title;
3858
+ const fallback = {
3859
+ speedster: "Swift Egg Seeker",
3860
+ steady: "Determined Hunter",
3861
+ explorer: "Curious Wanderer",
3862
+ philosopher: "Patient Sage"
3863
+ };
3864
+ return fallback[pace] ?? "Egg Explorer";
3865
+ }
3866
+ // ─── Predictability ────────────────────────────────────────────────────
3867
+ _computePredictability() {
3868
+ const clickNoise = Math.min(this._extraClicks / 30, 1);
3869
+ const chaosNoise = Math.min(this._chaoticMoveFrames / 200, 1);
3870
+ const hesitationNoise = Math.min(this._hesitations / 20, 1);
3871
+ const failNoise = Math.min(this._failedAttempts / 10, 1);
3872
+ const correctionNoise = Math.min(this._overcorrections / 15, 1);
3873
+ const noise = clickNoise * 0.2 + chaosNoise * 0.25 + hesitationNoise * 0.2 + failNoise * 0.2 + correctionNoise * 0.15;
3874
+ const predictability = Math.round((1 - noise) * 100);
3875
+ const score = Math.max(0, Math.min(100, predictability));
3876
+ return { score, label: this._labelForScore(score) };
3877
+ }
3878
+ _labelForScore(score) {
3879
+ if (score >= 90) return "highly predictable";
3880
+ if (score >= 75) return `more predictable than ${score}% of players`;
3881
+ if (score >= 55) return "moderately predictable";
3882
+ if (score >= 35) return `less predictable than ${100 - score}% of players`;
3883
+ if (score >= 15) return "rare pattern";
3884
+ return "deeply unpredictable";
3885
+ }
3886
+ }
3887
+ const STORAGE_KEY = "eeq_scores";
3888
+ const PROGRESS_KEY = "eeq_progress";
3889
+ class Persistence {
3890
+ constructor(enabled) {
3891
+ this._enabled = enabled;
3892
+ }
3893
+ saveScore(score) {
3894
+ if (!this._enabled) return;
3895
+ try {
3896
+ const existing = this._loadAll();
3897
+ existing.push({
3898
+ ...score,
3899
+ date: (/* @__PURE__ */ new Date()).toISOString()
3900
+ });
3901
+ const trimmed = existing.slice(-50);
3902
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
3903
+ } catch {
3904
+ }
3905
+ }
3906
+ getBestTime() {
3907
+ const all = this._loadAll();
3908
+ if (all.length === 0) return null;
3909
+ return Math.min(...all.map((s) => s.totalTime));
3910
+ }
3911
+ getScoreCount() {
3912
+ return this._loadAll().length;
3913
+ }
3914
+ getLastScore() {
3915
+ const all = this._loadAll();
3916
+ return all.length > 0 ? all[all.length - 1] : null;
3917
+ }
3918
+ clearScores() {
3919
+ try {
3920
+ localStorage.removeItem(STORAGE_KEY);
3921
+ } catch {
3922
+ }
3923
+ }
3924
+ _loadAll() {
3925
+ try {
3926
+ const raw = localStorage.getItem(STORAGE_KEY);
3927
+ if (!raw) return [];
3928
+ return JSON.parse(raw);
3929
+ } catch {
3930
+ return [];
3931
+ }
3932
+ }
3933
+ // ─── Mid-game progress checkpointing ─────────────────────────────────
3934
+ saveProgress(data) {
3935
+ if (!this._enabled) return;
3936
+ try {
3937
+ localStorage.setItem(PROGRESS_KEY, JSON.stringify(data));
3938
+ } catch {
3939
+ }
3940
+ }
3941
+ loadProgress() {
3942
+ if (!this._enabled) return null;
3943
+ try {
3944
+ const raw = localStorage.getItem(PROGRESS_KEY);
3945
+ if (!raw) return null;
3946
+ return JSON.parse(raw);
3947
+ } catch {
3948
+ return null;
3949
+ }
3950
+ }
3951
+ clearProgress() {
3952
+ try {
3953
+ localStorage.removeItem(PROGRESS_KEY);
3954
+ } catch {
3955
+ }
3956
+ }
3957
+ }
3958
+ class StillnessStage {
3959
+ constructor(config, script, input, bus) {
3960
+ this._status = "active";
3961
+ this._startTime = 0;
3962
+ this._breaks = 0;
3963
+ this._attempts = 0;
3964
+ this._lastBreakNarrativeTime = 0;
3965
+ this._narrativeIndex = 0;
3966
+ this._bestProgress = 0;
3967
+ this._introPlayed = false;
3968
+ this.config = config;
3969
+ this.script = script;
3970
+ this.input = input;
3971
+ this.bus = bus;
3972
+ }
3973
+ async start() {
3974
+ this._startTime = Date.now();
3975
+ this._attempts = 1;
3976
+ this.input.resetPhases();
3977
+ this.input.resetCounts();
3978
+ this._introPlayed = false;
3979
+ const introLines = this.script.stage1Intro;
3980
+ for (let i = 0; i < introLines.length; i++) {
3981
+ this.bus.emit("narrative:show", introLines[i]);
3982
+ await this._wait(1500);
3983
+ }
3984
+ this.bus.emit("narrative:clear");
3985
+ await this._wait(1e3);
3986
+ this._introPlayed = true;
3987
+ }
3988
+ update(_dt) {
3989
+ if (this._status === "complete" || !this._introPlayed) return this._status;
3990
+ const requiredMs = this.config.stageDurations.stillnessMs;
3991
+ const stillMs = this.input.stillDuration;
3992
+ const progress = Math.min(1, stillMs / requiredMs);
3993
+ if (progress > this._bestProgress) {
3994
+ this._bestProgress = progress;
3995
+ }
3996
+ this.bus.emit("stage:progress", progress);
3997
+ if (this.input.snapshot.isMoving && progress > 0.05) {
3998
+ this._handleBreak();
3999
+ return this._status;
4000
+ }
4001
+ if (stillMs >= requiredMs) {
4002
+ this._status = "complete";
4003
+ this.bus.emit("stage:progress", 1);
4004
+ this.bus.emit("egg:reveal", 0);
4005
+ return this._status;
4006
+ }
4007
+ return this._status;
4008
+ }
4009
+ cleanup() {
4010
+ }
4011
+ getResult() {
4012
+ return {
4013
+ stageTime: Date.now() - this._startTime,
4014
+ attempts: this._attempts,
4015
+ breaks: this._breaks
4016
+ };
4017
+ }
4018
+ // ─── Internal ──────────────────────────────────────────────────────────
4019
+ _handleBreak() {
4020
+ this._breaks++;
4021
+ const now = Date.now();
4022
+ if (now - this._lastBreakNarrativeTime > 5e3) {
4023
+ this._lastBreakNarrativeTime = now;
4024
+ const reactions = this.script.stage1Reactions;
4025
+ const line = reactions[this._narrativeIndex % reactions.length];
4026
+ this._narrativeIndex++;
4027
+ this.bus.emit("narrative:show", line);
4028
+ }
4029
+ }
4030
+ _wait(ms) {
4031
+ return new Promise((r) => setTimeout(r, ms));
4032
+ }
4033
+ }
4034
+ class MotionStage {
4035
+ constructor(config, script, input, bus) {
4036
+ this._status = "active";
4037
+ this._startTime = 0;
4038
+ this._fades = 0;
4039
+ this._attempts = 0;
4040
+ this._movingAccum = 0;
4041
+ this._lastUpdateTime = 0;
4042
+ this._narrativeIndex = 0;
4043
+ this._lastFadeNarrativeTime = 0;
4044
+ this._introPlayed = false;
4045
+ this.FADE_THRESHOLD_MS = 2500;
4046
+ this.config = config;
4047
+ this.script = script;
4048
+ this.input = input;
4049
+ this.bus = bus;
4050
+ }
4051
+ async start() {
4052
+ this._startTime = Date.now();
4053
+ this._lastUpdateTime = Date.now();
4054
+ this._movingAccum = 0;
4055
+ this._attempts = 1;
4056
+ this.input.resetPhases();
4057
+ this.input.resetCounts();
4058
+ this._introPlayed = false;
4059
+ const introLines = this.script.stage2Intro;
4060
+ for (let i = 0; i < introLines.length; i++) {
4061
+ this.bus.emit("narrative:show", introLines[i]);
4062
+ await this._wait(1500);
4063
+ }
4064
+ this.bus.emit("narrative:clear");
4065
+ await this._wait(1e3);
4066
+ this._introPlayed = true;
4067
+ }
4068
+ update(_dt) {
4069
+ if (this._status === "complete" || !this._introPlayed) return this._status;
4070
+ const now = Date.now();
4071
+ const elapsed = now - this._lastUpdateTime;
4072
+ this._lastUpdateTime = now;
4073
+ const requiredMs = this.config.stageDurations.motionMs;
4074
+ if (this.input.snapshot.isMoving) {
4075
+ this._movingAccum += elapsed;
4076
+ const vel = this.input.snapshot.velocity;
4077
+ if (vel > 50) {
4078
+ const spawnChance = Math.max(0.15, 1 - vel / 1200);
4079
+ if (Math.random() < spawnChance) {
4080
+ this.bus.emit("motion:trail", {
4081
+ x: this.input.snapshot.mouseX,
4082
+ y: this.input.snapshot.mouseY,
4083
+ velocity: vel
4084
+ });
4085
+ }
4086
+ }
4087
+ } else {
4088
+ const stillDuration = this.input.stillDuration;
4089
+ if (stillDuration > this.FADE_THRESHOLD_MS && this._movingAccum > 500) {
4090
+ this._fades++;
4091
+ this._movingAccum = Math.max(0, this._movingAccum - stillDuration * 0.6);
4092
+ this._handleFade();
4093
+ }
4094
+ }
4095
+ const progress = Math.min(1, this._movingAccum / requiredMs);
4096
+ this.bus.emit("stage:progress", progress);
4097
+ if (this._movingAccum >= requiredMs) {
4098
+ this._status = "complete";
4099
+ this.bus.emit("stage:progress", 1);
4100
+ this.bus.emit("egg:reveal", 1);
4101
+ return this._status;
4102
+ }
4103
+ return this._status;
4104
+ }
4105
+ cleanup() {
4106
+ }
4107
+ getResult() {
4108
+ return {
4109
+ stageTime: Date.now() - this._startTime,
4110
+ attempts: this._attempts,
4111
+ fades: this._fades
4112
+ };
4113
+ }
4114
+ // ─── Internal ──────────────────────────────────────────────────────────
4115
+ _handleFade() {
4116
+ const now = Date.now();
4117
+ if (now - this._lastFadeNarrativeTime > 5e3) {
4118
+ this._lastFadeNarrativeTime = now;
4119
+ const reactions = this.script.stage2Reactions;
4120
+ const line = reactions[this._narrativeIndex % reactions.length];
4121
+ this._narrativeIndex++;
4122
+ this.bus.emit("narrative:show", line);
4123
+ }
4124
+ }
4125
+ _wait(ms) {
4126
+ return new Promise((r) => setTimeout(r, ms));
4127
+ }
4128
+ }
4129
+ class RhythmStage {
4130
+ constructor(config, script, input, bus) {
4131
+ this._status = "active";
4132
+ this._startTime = 0;
4133
+ this._attempts = 0;
4134
+ this._narrativeIndex = 0;
4135
+ this._lastNarrativeTime = 0;
4136
+ this._introPlayed = false;
4137
+ this._goodCycles = 0;
4138
+ this._totalCycles = 0;
4139
+ this._lastPhaseCount = 0;
4140
+ this._bestAccuracy = 0;
4141
+ this.IDEAL_MOVE_MIN = 1800;
4142
+ this.IDEAL_MOVE_MAX = 5e3;
4143
+ this.IDEAL_PAUSE_MIN = 1200;
4144
+ this.IDEAL_PAUSE_MAX = 4e3;
4145
+ this.config = config;
4146
+ this.script = script;
4147
+ this.input = input;
4148
+ this.bus = bus;
4149
+ }
4150
+ async start() {
4151
+ this._startTime = Date.now();
4152
+ this._attempts = 1;
4153
+ this._goodCycles = 0;
4154
+ this._totalCycles = 0;
4155
+ this.input.resetPhases();
4156
+ this.input.resetCounts();
4157
+ this._introPlayed = false;
4158
+ const introLines = this.script.stage3Intro;
4159
+ for (let i = 0; i < introLines.length; i++) {
4160
+ this.bus.emit("narrative:show", introLines[i]);
4161
+ await this._wait(1500);
4162
+ }
4163
+ this.bus.emit("narrative:clear");
4164
+ await this._wait(1e3);
4165
+ this._introPlayed = true;
4166
+ this._lastPhaseCount = this.input.phases.length;
4167
+ }
4168
+ update(_dt) {
4169
+ if (this._status === "complete" || !this._introPlayed) return this._status;
4170
+ const phases = this.input.phases;
4171
+ const requiredCycles = this.config.stageDurations.rhythmCycles;
4172
+ while (this._lastPhaseCount + 1 < phases.length) {
4173
+ const p1 = phases[this._lastPhaseCount];
4174
+ const p2 = phases[this._lastPhaseCount + 1];
4175
+ if (p1 && p2) {
4176
+ const isGood = this._evaluateCycle(p1, p2);
4177
+ this._totalCycles++;
4178
+ if (isGood) {
4179
+ this._goodCycles++;
4180
+ } else {
4181
+ this._showReaction();
4182
+ }
4183
+ this._lastPhaseCount += 2;
4184
+ } else {
4185
+ break;
4186
+ }
4187
+ }
4188
+ const accuracy = this._totalCycles > 0 ? this._goodCycles / this._totalCycles : 0;
4189
+ if (accuracy > this._bestAccuracy) this._bestAccuracy = accuracy;
4190
+ const progress = Math.min(1, this._goodCycles / requiredCycles);
4191
+ this.bus.emit("stage:progress", progress);
4192
+ this.bus.emit("rhythm:breathe", { accuracy, isMoving: this.input.snapshot.isMoving });
4193
+ if (this._goodCycles >= requiredCycles) {
4194
+ this._status = "complete";
4195
+ this.bus.emit("stage:progress", 1);
4196
+ this.bus.emit("egg:reveal", 2);
4197
+ return this._status;
4198
+ }
4199
+ return this._status;
4200
+ }
4201
+ cleanup() {
4202
+ }
4203
+ getResult() {
4204
+ return {
4205
+ stageTime: Date.now() - this._startTime,
4206
+ attempts: this._attempts,
4207
+ rhythmAccuracy: this._totalCycles > 0 ? Math.round(this._goodCycles / this._totalCycles * 100) : 0
4208
+ };
4209
+ }
4210
+ // ─── Internal ──────────────────────────────────────────────────────────
4211
+ _evaluateCycle(a, b) {
4212
+ const movePhase = a.type === "move" ? a : b.type === "move" ? b : null;
4213
+ const stillPhase = a.type === "still" ? a : b.type === "still" ? b : null;
4214
+ if (!movePhase || !stillPhase) return false;
4215
+ const moveOk = movePhase.duration >= this.IDEAL_MOVE_MIN && movePhase.duration <= this.IDEAL_MOVE_MAX;
4216
+ const stillOk = stillPhase.duration >= this.IDEAL_PAUSE_MIN && stillPhase.duration <= this.IDEAL_PAUSE_MAX;
4217
+ return moveOk && stillOk;
4218
+ }
4219
+ _showReaction() {
4220
+ const now = Date.now();
4221
+ if (now - this._lastNarrativeTime > 5e3) {
4222
+ this._lastNarrativeTime = now;
4223
+ const reactions = this.script.stage3Reactions;
4224
+ const line = reactions[this._narrativeIndex % reactions.length];
4225
+ this._narrativeIndex++;
4226
+ this.bus.emit("narrative:show", line);
4227
+ }
4228
+ }
4229
+ _wait(ms) {
4230
+ return new Promise((r) => setTimeout(r, ms));
4231
+ }
4232
+ }
4233
+ class PageReactor {
4234
+ constructor() {
4235
+ this._styleEl = null;
4236
+ this._reactClass = "eeq-page-react";
4237
+ this._injectStyles();
4238
+ }
4239
+ /** Trigger a reaction wave on 2-4 random visible page elements. */
4240
+ react() {
4241
+ const candidates = this._findCandidates();
4242
+ if (!candidates.length) return;
4243
+ const count = Math.min(candidates.length, 2 + Math.floor(Math.random() * 2));
4244
+ const chosen = this._pickRandom(candidates, count);
4245
+ const effects = ["eeq-react-pulse", "eeq-react-glow", "eeq-react-lift"];
4246
+ for (let i = 0; i < chosen.length; i++) {
4247
+ const el = chosen[i];
4248
+ const effect = effects[Math.floor(Math.random() * effects.length)];
4249
+ const delay = i * 200;
4250
+ setTimeout(() => {
4251
+ el.classList.add(this._reactClass, effect);
4252
+ setTimeout(() => {
4253
+ el.classList.remove(this._reactClass, effect);
4254
+ }, 1100);
4255
+ }, delay);
4256
+ }
4257
+ }
4258
+ destroy() {
4259
+ var _a;
4260
+ document.querySelectorAll(`.${this._reactClass}`).forEach((el) => {
4261
+ el.className = el.className.replace(/\beeq-react-\S+/g, "").trim();
4262
+ });
4263
+ (_a = this._styleEl) == null ? void 0 : _a.remove();
4264
+ this._styleEl = null;
4265
+ }
4266
+ _findCandidates() {
4267
+ var _a;
4268
+ const all = document.querySelectorAll(
4269
+ "p, h1, h2, h3, h4, h5, li, a, img, button, blockquote, figcaption, td, th"
4270
+ );
4271
+ const result = [];
4272
+ for (const el of all) {
4273
+ if ((_a = el.id) == null ? void 0 : _a.startsWith("eeq-")) continue;
4274
+ if (el.closest('[id^="eeq-"]')) continue;
4275
+ const rect = el.getBoundingClientRect();
4276
+ if (rect.width === 0 || rect.height === 0) continue;
4277
+ if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
4278
+ if (el.classList.contains(this._reactClass)) continue;
4279
+ result.push(el);
4280
+ }
4281
+ return result;
4282
+ }
4283
+ _pickRandom(arr, count) {
4284
+ const copy = [...arr];
4285
+ const result = [];
4286
+ for (let i = 0; i < count && copy.length; i++) {
4287
+ const idx = Math.floor(Math.random() * copy.length);
4288
+ result.push(copy.splice(idx, 1)[0]);
4289
+ }
4290
+ return result;
4291
+ }
4292
+ _injectStyles() {
4293
+ this._styleEl = document.createElement("style");
4294
+ this._styleEl.textContent = `
4295
+ @keyframes eeq-pulse-kf {
4296
+ 0%, 100% { transform: scale(1); }
4297
+ 50% { transform: scale(1.018); }
4298
+ }
4299
+ @keyframes eeq-glow-kf {
4300
+ 0%, 100% { filter: brightness(1); }
4301
+ 50% { filter: brightness(1.15) saturate(1.1); }
4302
+ }
4303
+ @keyframes eeq-lift-kf {
4304
+ 0%, 100% { transform: translateY(0); }
4305
+ 50% { transform: translateY(-2px); }
4306
+ }
4307
+ .eeq-react-pulse {
4308
+ animation: eeq-pulse-kf 1s ease-in-out !important;
4309
+ }
4310
+ .eeq-react-glow {
4311
+ animation: eeq-glow-kf 1s ease-in-out !important;
4312
+ }
4313
+ .eeq-react-lift {
4314
+ animation: eeq-lift-kf 1s ease-in-out !important;
4315
+ }
4316
+ `;
4317
+ document.head.appendChild(this._styleEl);
4318
+ }
4319
+ }
4320
+ class PageBreather {
4321
+ constructor() {
4322
+ this._active = false;
4323
+ this._intensity = 0;
4324
+ this._targetIntensity = 0;
4325
+ this._rafId = 0;
4326
+ this._overlay = null;
4327
+ this._startTime = 0;
4328
+ this._isUserMoving = false;
4329
+ this._inSync = false;
4330
+ this.CYCLE_MS = 6e3;
4331
+ this._tick = () => {
4332
+ if (!this._active || !this._overlay) return;
4333
+ this._rafId = requestAnimationFrame(this._tick);
4334
+ this._intensity += (this._targetIntensity - this._intensity) * 0.06;
4335
+ const elapsed = Date.now() - this._startTime;
4336
+ const phase = elapsed % this.CYCLE_MS / this.CYCLE_MS;
4337
+ const wave = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) * 0.5;
4338
+ const shouldMove = phase < 0.5;
4339
+ this._inSync = shouldMove === this._isUserMoving;
4340
+ const baseOpacity = wave * 0.85;
4341
+ const syncBonus = this._inSync ? 0.15 : 0;
4342
+ const opacity = this._intensity * (baseOpacity + (1 - wave) * 0.05 + syncBonus);
4343
+ this._overlay.style.opacity = String(Math.min(1, opacity));
4344
+ };
4345
+ }
4346
+ start() {
4347
+ if (this._active) return;
4348
+ this._active = true;
4349
+ this._startTime = Date.now();
4350
+ const el = document.createElement("div");
4351
+ el.style.cssText = [
4352
+ "position:fixed",
4353
+ "top:0",
4354
+ "left:0",
4355
+ "width:100%",
4356
+ "height:100%",
4357
+ "pointer-events:none",
4358
+ "z-index:999980",
4359
+ "opacity:0",
4360
+ "background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.7) 100%)",
4361
+ "transition:opacity 0.3s ease"
4362
+ ].join(";");
4363
+ document.body.appendChild(el);
4364
+ this._overlay = el;
4365
+ this._tick();
4366
+ }
4367
+ update(accuracy, isMoving) {
4368
+ if (!this._active) return;
4369
+ this._isUserMoving = isMoving;
4370
+ const raw = Math.max(0, Math.min(1, (accuracy - 0.05) / 0.5));
4371
+ this._targetIntensity = 0.5 + raw * 0.5;
4372
+ }
4373
+ /** Whether the user is currently in sync with the breathing rhythm. */
4374
+ isInSync() {
4375
+ return this._inSync;
4376
+ }
4377
+ stop() {
4378
+ var _a;
4379
+ if (!this._active) return;
4380
+ this._active = false;
4381
+ cancelAnimationFrame(this._rafId);
4382
+ (_a = this._overlay) == null ? void 0 : _a.remove();
4383
+ this._overlay = null;
4384
+ this._intensity = 0;
4385
+ this._targetIntensity = 0;
4386
+ this._inSync = false;
4387
+ }
4388
+ destroy() {
4389
+ this.stop();
4390
+ }
4391
+ }
4392
+ class GameController {
4393
+ constructor(config, script) {
4394
+ this.bus = new EventBus();
4395
+ this.fsm = new StateMachine();
4396
+ this.input = new InputTracker();
4397
+ this.scoring = new ScoringEngine();
4398
+ this.hiddenEntry = null;
4399
+ this.narrative = null;
4400
+ this.hud = null;
4401
+ this.shrine = null;
4402
+ this.threeRenderer = null;
4403
+ this.fallbackRenderer = null;
4404
+ this.overlay = null;
4405
+ this.results = null;
4406
+ this.eggRenderer = null;
4407
+ this.activeStage = null;
4408
+ this.pageReactor = null;
4409
+ this._lastPageReactTime = 0;
4410
+ this._lastReactProgress = 0;
4411
+ this.pageBreather = null;
4412
+ this._updateRaf = 0;
4413
+ this._running = false;
4414
+ this._destroyed = false;
4415
+ this._onPageHide = () => this._handlePageClose();
4416
+ this._tremorStyle = null;
4417
+ this._tremorClass = "eeq-tremor";
4418
+ this._lastFrameTime = 0;
4419
+ this._gameLoop = () => {
4420
+ var _a;
4421
+ if (!this._running || this._destroyed) return;
4422
+ this._updateRaf = requestAnimationFrame(this._gameLoop);
4423
+ if (this.state.isPaused) return;
4424
+ const now = performance.now();
4425
+ const dt = (now - this._lastFrameTime) / 1e3;
4426
+ this._lastFrameTime = now;
4427
+ this.scoring.analyseFrame(this.input.snapshot);
4428
+ (_a = this.hud) == null ? void 0 : _a.update(this.state, this.fsm.label);
4429
+ if (this.activeStage) {
4430
+ const status = this.activeStage.update(dt);
4431
+ if (status === "complete") {
4432
+ this._running = false;
4433
+ cancelAnimationFrame(this._updateRaf);
4434
+ const currentStage = this.fsm.state;
4435
+ if (currentStage === "stage1-active") {
4436
+ this.fsm.transitionTo("stage1-success");
4437
+ } else if (currentStage === "stage2-active") {
4438
+ this.fsm.transitionTo("stage2-success");
4439
+ } else if (currentStage === "stage3-active") {
4440
+ this.fsm.transitionTo("stage3-success");
4441
+ }
4442
+ }
4443
+ }
4444
+ };
4445
+ this.config = config;
4446
+ this.script = script;
4447
+ this.persistence = new Persistence(config.scoring.enableLocal);
4448
+ this.state = {
4449
+ stage: "idle",
4450
+ eggsFound: 0,
4451
+ startTime: 0,
4452
+ stageStartTime: 0,
4453
+ isPaused: false,
4454
+ eggs: [
4455
+ { found: false },
4456
+ { found: false },
4457
+ { found: false }
4458
+ ]
4459
+ };
4460
+ }
4461
+ // ─── Lifecycle ─────────────────────────────────────────────────────────
4462
+ async init() {
4463
+ var _a, _b;
4464
+ this.fsm.onTransition((from, to) => this._onTransition(from, to));
4465
+ this.bus.on("narrative:show", (text) => {
4466
+ var _a2;
4467
+ return (_a2 = this.narrative) == null ? void 0 : _a2.showLine(text);
4468
+ });
4469
+ this.bus.on("narrative:clear", () => {
4470
+ var _a2;
4471
+ return (_a2 = this.narrative) == null ? void 0 : _a2.clear();
4472
+ });
4473
+ this.bus.on("egg:reveal", (index) => this._handleEggReveal(index));
4474
+ this.bus.on("stage:progress", (p) => this._handleStageProgress(p));
4475
+ this.bus.on("motion:trail", (data) => {
4476
+ var _a2;
4477
+ (_a2 = this.eggRenderer) == null ? void 0 : _a2.addTrail(data.x, data.y, data.velocity);
4478
+ });
4479
+ this.bus.on("rhythm:breathe", (data) => {
4480
+ var _a2, _b2, _c;
4481
+ (_a2 = this.eggRenderer) == null ? void 0 : _a2.setBreathIntensity(data.accuracy);
4482
+ (_b2 = this.pageBreather) == null ? void 0 : _b2.update(data.accuracy, data.isMoving);
4483
+ if (((_c = this.pageBreather) == null ? void 0 : _c.isInSync()) && this.threeRenderer) {
4484
+ this.threeRenderer.showProgressParticles(0.6 + data.accuracy * 0.4);
4485
+ }
4486
+ });
4487
+ this.overlay = new OverlayManager(this.config);
4488
+ this.overlay.mount();
4489
+ this.narrative = new NarrativeRenderer(this.config);
4490
+ this.narrative.mount();
4491
+ this.shrine = new ShrineRenderer(this.config);
4492
+ this.shrine.mount((eggIndex) => this._onShrineEggClick(eggIndex));
4493
+ await this._initEggRenderer();
4494
+ if (this.threeRenderer) {
4495
+ this.threeRenderer.onCollectEgg((index) => {
4496
+ this._onEggLongPressCollect(index);
4497
+ });
4498
+ }
4499
+ this.scoring.start();
4500
+ (_b = (_a = this.config.callbacks).onInit) == null ? void 0 : _b.call(_a);
4501
+ window.addEventListener("pagehide", this._onPageHide);
4502
+ window.addEventListener("beforeunload", this._onPageHide);
4503
+ const checkpoint = this.persistence.loadProgress();
4504
+ if (checkpoint) {
4505
+ this._resumeFromCheckpoint(checkpoint);
4506
+ } else {
4507
+ this.fsm.transitionTo("entry");
4508
+ }
4509
+ }
4510
+ pause() {
4511
+ var _a;
4512
+ this.state.isPaused = true;
4513
+ (_a = this.hud) == null ? void 0 : _a.pause();
4514
+ }
4515
+ resume() {
4516
+ var _a;
4517
+ this.state.isPaused = false;
4518
+ (_a = this.hud) == null ? void 0 : _a.resume();
4519
+ }
4520
+ async restart() {
4521
+ this._cleanup();
4522
+ this.fsm.reset();
4523
+ this.persistence.clearProgress();
4524
+ this.state = {
4525
+ stage: "idle",
4526
+ eggsFound: 0,
4527
+ startTime: 0,
4528
+ stageStartTime: 0,
4529
+ isPaused: false,
4530
+ eggs: [
4531
+ { found: false },
4532
+ { found: false },
4533
+ { found: false }
4534
+ ]
4535
+ };
4536
+ this.scoring.start();
4537
+ this.fsm.transitionTo("entry");
4538
+ }
4539
+ destroy() {
4540
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
4541
+ this._destroyed = true;
4542
+ window.removeEventListener("pagehide", this._onPageHide);
4543
+ window.removeEventListener("beforeunload", this._onPageHide);
4544
+ this._cleanup();
4545
+ (_a = this.narrative) == null ? void 0 : _a.destroy();
4546
+ (_b = this.hud) == null ? void 0 : _b.destroy();
4547
+ (_c = this.shrine) == null ? void 0 : _c.destroy();
4548
+ (_d = this.threeRenderer) == null ? void 0 : _d.destroy();
4549
+ (_e = this.fallbackRenderer) == null ? void 0 : _e.destroy();
4550
+ (_f = this.overlay) == null ? void 0 : _f.destroy();
4551
+ (_g = this.results) == null ? void 0 : _g.destroy();
4552
+ this._stopAnticipationTremor();
4553
+ (_h = this.pageReactor) == null ? void 0 : _h.destroy();
4554
+ this.pageReactor = null;
4555
+ (_i = this.pageBreather) == null ? void 0 : _i.destroy();
4556
+ this.pageBreather = null;
4557
+ this.input.stop();
4558
+ this.bus.clear();
4559
+ this.fsm.reset();
4560
+ (_k = (_j = this.config.callbacks).onDestroy) == null ? void 0 : _k.call(_j);
4561
+ }
4562
+ _handlePageClose() {
4563
+ if (this._destroyed) return;
4564
+ const stage = this.fsm.state;
4565
+ if (stage !== "idle" && stage !== "entry" && stage !== "results" && stage !== "complete") {
4566
+ this._saveCheckpoint(stage);
4567
+ }
4568
+ this.destroy();
4569
+ }
4570
+ // ─── FSM Transition Handler ────────────────────────────────────────────
4571
+ async _onTransition(from, to) {
4572
+ var _a;
4573
+ this.state.stage = to;
4574
+ this.state.stageStartTime = Date.now();
4575
+ (_a = this.hud) == null ? void 0 : _a.update(this.state, this.fsm.label);
4576
+ const checkpointStages = [
4577
+ "stage1-intro",
4578
+ "stage2-intro",
4579
+ "stage3-intro",
4580
+ "finale"
4581
+ ];
4582
+ if (checkpointStages.includes(to)) {
4583
+ this._saveCheckpoint(to);
4584
+ }
4585
+ switch (to) {
4586
+ case "entry":
4587
+ await this._startEntry();
4588
+ break;
4589
+ case "stage1-intro":
4590
+ await this._startStage1();
4591
+ break;
4592
+ case "stage1-active":
4593
+ this._startGameLoop();
4594
+ break;
4595
+ case "stage1-success":
4596
+ await this._handleStageSuccess(0);
4597
+ break;
4598
+ case "stage2-intro":
4599
+ await this._startStage2();
4600
+ break;
4601
+ case "stage2-active":
4602
+ this._startGameLoop();
4603
+ break;
4604
+ case "stage2-success":
4605
+ await this._handleStageSuccess(1);
4606
+ break;
4607
+ case "stage3-intro":
4608
+ await this._startStage3();
4609
+ break;
4610
+ case "stage3-active":
4611
+ this._startGameLoop();
4612
+ break;
4613
+ case "stage3-success":
4614
+ await this._handleStageSuccess(2);
4615
+ break;
4616
+ case "finale":
4617
+ await this._startFinale();
4618
+ break;
4619
+ case "results":
4620
+ this._showResults();
4621
+ break;
4622
+ }
4623
+ }
4624
+ // ─── Progress Checkpointing ────────────────────────────────────────────
4625
+ _saveCheckpoint(stage) {
4626
+ this.persistence.saveProgress({
4627
+ stage,
4628
+ eggsFound: this.state.eggsFound,
4629
+ eggsState: [
4630
+ this.state.eggs[0].found,
4631
+ this.state.eggs[1].found,
4632
+ this.state.eggs[2].found
4633
+ ],
4634
+ savedAt: Date.now()
4635
+ });
4636
+ }
4637
+ _resumeFromCheckpoint(cp) {
4638
+ var _a, _b;
4639
+ const validResumeStages = [
4640
+ "stage1-intro",
4641
+ "stage2-intro",
4642
+ "stage3-intro",
4643
+ "finale"
4644
+ ];
4645
+ if (!validResumeStages.includes(cp.stage)) {
4646
+ this.persistence.clearProgress();
4647
+ this.fsm.transitionTo("entry");
4648
+ return;
4649
+ }
4650
+ this.state.eggsFound = cp.eggsFound;
4651
+ this.state.startTime = Date.now();
4652
+ for (let i = 0; i < 3; i++) {
4653
+ this.state.eggs[i].found = cp.eggsState[i];
4654
+ }
4655
+ (_a = this.shrine) == null ? void 0 : _a.show();
4656
+ for (let i = 0; i < cp.eggsFound; i++) {
4657
+ (_b = this.shrine) == null ? void 0 : _b.fillSlot(i);
4658
+ }
4659
+ this.fsm.forceState(cp.stage);
4660
+ this._onTransition("idle", cp.stage);
4661
+ }
4662
+ // ─── Entry ─────────────────────────────────────────────────────────────
4663
+ async _startEntry() {
4664
+ var _a;
4665
+ try {
4666
+ if (localStorage.getItem("eeq_optout") === "1") return;
4667
+ } catch {
4668
+ }
4669
+ this.state.startTime = Date.now();
4670
+ (_a = this.hud) == null ? void 0 : _a.startTimer();
4671
+ this.hiddenEntry = new HiddenEntry(this.config, this.script, () => {
4672
+ var _a2, _b;
4673
+ this.scoring.recordEntryFound();
4674
+ (_b = (_a2 = this.config.callbacks).onEntryFound) == null ? void 0 : _b.call(_a2);
4675
+ this._onEntryFound();
4676
+ });
4677
+ this.hiddenEntry.start();
4678
+ }
4679
+ async _onEntryFound() {
4680
+ var _a, _b, _c, _d;
4681
+ (_a = this.hiddenEntry) == null ? void 0 : _a.cleanup();
4682
+ this.hiddenEntry = null;
4683
+ this._startAnticipationTremor();
4684
+ (_b = this.shrine) == null ? void 0 : _b.show();
4685
+ await ((_c = this.narrative) == null ? void 0 : _c.showSequence(this.script.entryConfirmation, 1500));
4686
+ (_d = this.narrative) == null ? void 0 : _d.clear();
4687
+ this._stopAnticipationTremor();
4688
+ await this._wait(800);
4689
+ this.input.start();
4690
+ this.fsm.transitionTo("stage1-intro");
4691
+ }
4692
+ _startAnticipationTremor() {
4693
+ this._tremorStyle = document.createElement("style");
4694
+ this._tremorStyle.textContent = `
4695
+ @keyframes eeq-tremor {
4696
+ 0%, 100% { transform: translate(0, 0); }
4697
+ 10% { transform: translate(-0.5px, 0.3px); }
4698
+ 20% { transform: translate(0.6px, -0.4px); }
4699
+ 30% { transform: translate(-0.3px, 0.5px); }
4700
+ 40% { transform: translate(0.4px, 0.2px); }
4701
+ 50% { transform: translate(-0.4px, -0.3px); }
4702
+ 60% { transform: translate(0.3px, 0.5px); }
4703
+ 70% { transform: translate(-0.5px, -0.2px); }
4704
+ 80% { transform: translate(0.2px, 0.4px); }
4705
+ 90% { transform: translate(-0.3px, -0.5px); }
4706
+ }
4707
+ .${this._tremorClass} {
4708
+ animation: eeq-tremor 0.6s ease-in-out infinite !important;
4709
+ }
4710
+ `;
4711
+ document.head.appendChild(this._tremorStyle);
4712
+ const candidates = document.querySelectorAll(
4713
+ 'a, button, span, small, label, svg, img, i, [role="button"], nav li'
4714
+ );
4715
+ let count = 0;
4716
+ candidates.forEach((el) => {
4717
+ var _a;
4718
+ const rect = el.getBoundingClientRect();
4719
+ if (rect.width > 300 || rect.height > 100) return;
4720
+ if (rect.width === 0 || rect.height === 0) return;
4721
+ if (rect.bottom < 0 || rect.top > window.innerHeight) return;
4722
+ if ((_a = el.id) == null ? void 0 : _a.startsWith("eeq-")) return;
4723
+ if (el.closest('[id^="eeq-"]')) return;
4724
+ const delay = Math.random() * 800;
4725
+ const htmlEl = el;
4726
+ setTimeout(() => {
4727
+ if (this._tremorStyle) htmlEl.classList.add(this._tremorClass);
4728
+ }, delay);
4729
+ count++;
4730
+ if (count > 40) return;
4731
+ });
4732
+ }
4733
+ _stopAnticipationTremor() {
4734
+ var _a;
4735
+ document.querySelectorAll(`.${this._tremorClass}`).forEach((el) => {
4736
+ el.classList.remove(this._tremorClass);
4737
+ });
4738
+ (_a = this._tremorStyle) == null ? void 0 : _a.remove();
4739
+ this._tremorStyle = null;
4740
+ }
4741
+ // ─── Stage 1: Stillness ────────────────────────────────────────────────
4742
+ async _startStage1() {
4743
+ var _a, _b, _c, _d;
4744
+ (_b = (_a = this.config.callbacks).onStageStart) == null ? void 0 : _b.call(_a, "stage1-intro");
4745
+ (_c = this.hud) == null ? void 0 : _c.update(this.state, this.fsm.label);
4746
+ (_d = this.eggRenderer) == null ? void 0 : _d.startRenderLoop();
4747
+ const stage = new StillnessStage(this.config, this.script, this.input, this.bus);
4748
+ this.activeStage = stage;
4749
+ await stage.start();
4750
+ this.fsm.transitionTo("stage1-active");
4751
+ }
4752
+ // ─── Stage 2: Motion ───────────────────────────────────────────────────
4753
+ async _startStage2() {
4754
+ var _a, _b, _c;
4755
+ (_b = (_a = this.config.callbacks).onStageStart) == null ? void 0 : _b.call(_a, "stage2-intro");
4756
+ (_c = this.hud) == null ? void 0 : _c.update(this.state, this.fsm.label);
4757
+ const stage = new MotionStage(this.config, this.script, this.input, this.bus);
4758
+ this.activeStage = stage;
4759
+ await stage.start();
4760
+ this.fsm.transitionTo("stage2-active");
4761
+ }
4762
+ // ─── Stage 3: Rhythm ──────────────────────────────────────────────────
4763
+ async _startStage3() {
4764
+ var _a, _b, _c;
4765
+ (_b = (_a = this.config.callbacks).onStageStart) == null ? void 0 : _b.call(_a, "stage3-intro");
4766
+ (_c = this.hud) == null ? void 0 : _c.update(this.state, this.fsm.label);
4767
+ const stage = new RhythmStage(this.config, this.script, this.input, this.bus);
4768
+ this.activeStage = stage;
4769
+ await stage.start();
4770
+ this.fsm.transitionTo("stage3-active");
4771
+ this.pageBreather = new PageBreather();
4772
+ this.pageBreather.start();
4773
+ }
4774
+ // ─── Game Loop ─────────────────────────────────────────────────────────
4775
+ _startGameLoop() {
4776
+ if (this._running) return;
4777
+ this._running = true;
4778
+ this._lastFrameTime = performance.now();
4779
+ this._gameLoop();
4780
+ }
4781
+ // ─── Egg Reveal ────────────────────────────────────────────────────────
4782
+ _handleEggReveal(index) {
4783
+ var _a, _b, _c, _d;
4784
+ this.state.eggs[index].found = true;
4785
+ this.state.eggs[index].foundTime = Date.now();
4786
+ this.state.eggsFound++;
4787
+ (_a = this.eggRenderer) == null ? void 0 : _a.revealEgg(index);
4788
+ (_b = this.hud) == null ? void 0 : _b.flashEggFound(index);
4789
+ (_d = (_c = this.config.callbacks).onEggFound) == null ? void 0 : _d.call(_c, index);
4790
+ }
4791
+ async _handleStageSuccess(eggIndex) {
4792
+ var _a, _b, _c, _d, _e, _f, _g;
4793
+ if (this.activeStage) {
4794
+ const result = this.activeStage.getResult();
4795
+ this.scoring.recordStageResult(result);
4796
+ this.activeStage.cleanup();
4797
+ this.activeStage = null;
4798
+ (_b = (_a = this.config.callbacks).onStageComplete) == null ? void 0 : _b.call(_a, this.fsm.state, result);
4799
+ }
4800
+ const successLines = eggIndex === 0 ? this.script.stage1Success : eggIndex === 1 ? this.script.stage2Success : this.script.stage3Success;
4801
+ await ((_c = this.narrative) == null ? void 0 : _c.showSequence(successLines, 1500));
4802
+ (_d = this.narrative) == null ? void 0 : _d.clear();
4803
+ await this._wait(800);
4804
+ if (this.threeRenderer) {
4805
+ this.threeRenderer.enableInteraction(eggIndex);
4806
+ } else {
4807
+ (_e = this.eggRenderer) == null ? void 0 : _e.collectEgg(eggIndex);
4808
+ (_f = this.shrine) == null ? void 0 : _f.fillSlot(eggIndex);
4809
+ await this._wait(1500);
4810
+ if (eggIndex < 2) {
4811
+ (_g = this.shrine) == null ? void 0 : _g.activateSlot(eggIndex);
4812
+ } else {
4813
+ await this._wait(1e3);
4814
+ this.fsm.transitionTo("finale");
4815
+ }
4816
+ }
4817
+ }
4818
+ // ─── Shrine Click (advance to next stage) ──────────────────────────────
4819
+ _onShrineEggClick(eggIndex) {
4820
+ var _a;
4821
+ (_a = this.shrine) == null ? void 0 : _a.deactivateSlot(eggIndex);
4822
+ if (eggIndex === 0) {
4823
+ this.fsm.transitionTo("stage2-intro");
4824
+ } else if (eggIndex === 1) {
4825
+ this.fsm.transitionTo("stage3-intro");
4826
+ }
4827
+ }
4828
+ // ─── Long-press collect (3D egg interaction) ───────────────────────────
4829
+ async _onEggLongPressCollect(eggIndex) {
4830
+ var _a, _b, _c, _d, _e;
4831
+ (_a = this.eggRenderer) == null ? void 0 : _a.collectEgg(eggIndex);
4832
+ (_b = this.shrine) == null ? void 0 : _b.fillSlot(eggIndex);
4833
+ if (eggIndex === 2) {
4834
+ (_c = this.pageBreather) == null ? void 0 : _c.destroy();
4835
+ this.pageBreather = null;
4836
+ }
4837
+ const congrats = [
4838
+ "egg found — well done",
4839
+ "another one — you're getting closer",
4840
+ "all three — remarkable"
4841
+ ];
4842
+ (_d = this.narrative) == null ? void 0 : _d.showCelebration(congrats[eggIndex] ?? "egg collected");
4843
+ await this._wait(2200);
4844
+ (_e = this.narrative) == null ? void 0 : _e.clear();
4845
+ await this._wait(400);
4846
+ if (eggIndex < 2) {
4847
+ if (eggIndex === 0) {
4848
+ this.fsm.transitionTo("stage2-intro");
4849
+ } else {
4850
+ this.fsm.transitionTo("stage3-intro");
4851
+ }
4852
+ } else {
4853
+ await this._wait(800);
4854
+ this.fsm.transitionTo("finale");
4855
+ }
4856
+ }
4857
+ // ─── Stage Progress ────────────────────────────────────────────────────
4858
+ _handleStageProgress(progress) {
4859
+ if (this.threeRenderer) {
4860
+ this.threeRenderer.showProgressParticles(progress);
4861
+ }
4862
+ const now = Date.now();
4863
+ if (progress > this._lastReactProgress + 0.03 && now - this._lastPageReactTime > 8e3) {
4864
+ this._lastPageReactTime = now;
4865
+ this._lastReactProgress = progress;
4866
+ if (!this.pageReactor) {
4867
+ this.pageReactor = new PageReactor();
4868
+ }
4869
+ this.pageReactor.react();
4870
+ }
4871
+ if (progress < this._lastReactProgress - 0.1) {
4872
+ this._lastReactProgress = progress;
4873
+ }
4874
+ }
4875
+ // ─── Finale ────────────────────────────────────────────────────────────
4876
+ async _startFinale() {
4877
+ var _a, _b, _c, _d, _e, _f;
4878
+ (_b = (_a = this.config.callbacks).onFinaleStart) == null ? void 0 : _b.call(_a);
4879
+ (_c = this.shrine) == null ? void 0 : _c.hideAll();
4880
+ await this._wait(1e3);
4881
+ (_d = this.eggRenderer) == null ? void 0 : _d.startFinale();
4882
+ await ((_e = this.narrative) == null ? void 0 : _e.showSequence(this.script.finale, 1500));
4883
+ (_f = this.narrative) == null ? void 0 : _f.clear();
4884
+ await this._wait(4e3);
4885
+ this.fsm.transitionTo("results");
4886
+ }
4887
+ // ─── Results ───────────────────────────────────────────────────────────
4888
+ _showResults() {
4889
+ var _a, _b, _c;
4890
+ const snap = this.input.snapshot;
4891
+ const score = this.scoring.computeFinalScore({
4892
+ totalClicks: snap.totalClicks,
4893
+ totalScrolls: snap.totalScrolls,
4894
+ totalKeyPresses: snap.totalKeyPresses,
4895
+ totalDistance: snap.totalDistance,
4896
+ maxVelocity: snap.maxVelocity
4897
+ });
4898
+ if (this.threeRenderer) {
4899
+ this.threeRenderer.setFestiveEggSeed(score.eggSeed);
4900
+ }
4901
+ this.persistence.saveScore(score);
4902
+ this.persistence.clearProgress();
4903
+ if (this.config.scoring.leaderboardAdapter) {
4904
+ this.config.scoring.leaderboardAdapter.submitScore(score).catch(() => {
4905
+ });
4906
+ }
4907
+ (_a = this.eggRenderer) == null ? void 0 : _a.stopRenderLoop();
4908
+ this.results = new ResultsRenderer(this.config);
4909
+ this.results.show(
4910
+ score,
4911
+ () => {
4912
+ this.destroy();
4913
+ }
4914
+ );
4915
+ (_c = (_b = this.config.callbacks).onComplete) == null ? void 0 : _c.call(_b, score);
4916
+ }
4917
+ // ─── Renderer Init ─────────────────────────────────────────────────────
4918
+ async _initEggRenderer() {
4919
+ const mode = this.config.renderer;
4920
+ if (mode === "3d" || mode === "auto") {
4921
+ this.threeRenderer = new ThreeRenderer(this.config);
4922
+ const ok = await this.threeRenderer.init();
4923
+ if (ok) {
4924
+ this.eggRenderer = this.threeRenderer;
4925
+ return;
4926
+ }
4927
+ this.threeRenderer = null;
4928
+ }
4929
+ this.fallbackRenderer = new FallbackRenderer(this.config);
4930
+ await this.fallbackRenderer.init();
4931
+ this.eggRenderer = this.fallbackRenderer;
4932
+ }
4933
+ // ─── Helpers ───────────────────────────────────────────────────────────
4934
+ _cleanup() {
4935
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4936
+ this._running = false;
4937
+ if (this._updateRaf) cancelAnimationFrame(this._updateRaf);
4938
+ (_a = this.hiddenEntry) == null ? void 0 : _a.cleanup();
4939
+ (_b = this.activeStage) == null ? void 0 : _b.cleanup();
4940
+ this.activeStage = null;
4941
+ (_c = this.shrine) == null ? void 0 : _c.reset();
4942
+ (_d = this.narrative) == null ? void 0 : _d.clear();
4943
+ (_e = this.overlay) == null ? void 0 : _e.hide();
4944
+ this._lastPageReactTime = 0;
4945
+ this._lastReactProgress = 0;
4946
+ this._stopAnticipationTremor();
4947
+ (_f = this.pageReactor) == null ? void 0 : _f.destroy();
4948
+ this.pageReactor = null;
4949
+ (_g = this.pageBreather) == null ? void 0 : _g.destroy();
4950
+ this.pageBreather = null;
4951
+ (_h = this.results) == null ? void 0 : _h.destroy();
4952
+ this.results = null;
4953
+ if (this.threeRenderer) {
4954
+ this.threeRenderer.resetForRestart();
4955
+ }
4956
+ }
4957
+ _wait(ms) {
4958
+ return new Promise((r) => setTimeout(r, ms));
4959
+ }
4960
+ }
4961
+ let _instance = null;
4962
+ const EasterEggQuest = {
4963
+ /**
4964
+ * Initialize and start the Easter Egg Quest experience.
4965
+ * Safe to call multiple times — destroys any previous instance first.
4966
+ */
4967
+ async init(config = {}) {
4968
+ if (_instance) {
4969
+ _instance.destroy();
4970
+ _instance = null;
4971
+ }
4972
+ const resolved = resolveConfig(config);
4973
+ const script = resolveNarrative(config.narrative);
4974
+ _instance = new GameController(resolved, script);
4975
+ await _instance.init();
4976
+ },
4977
+ /** Pause the game. */
4978
+ pause() {
4979
+ _instance == null ? void 0 : _instance.pause();
4980
+ },
4981
+ /** Resume the game. */
4982
+ resume() {
4983
+ _instance == null ? void 0 : _instance.resume();
4984
+ },
4985
+ /** Restart from the beginning. */
4986
+ async restart() {
4987
+ await (_instance == null ? void 0 : _instance.restart());
4988
+ },
4989
+ /** Destroy the instance, remove all overlays, restore DOM. */
4990
+ destroy() {
4991
+ _instance == null ? void 0 : _instance.destroy();
4992
+ _instance = null;
4993
+ }
4994
+ };
4995
+ export {
4996
+ EasterEggQuest
4997
+ };