easter-egg-quest 1.0.14 → 1.0.16

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.
@@ -462,6 +462,12 @@ class InputTracker {
462
462
  this._phases = [];
463
463
  this._currentPhaseType = "still";
464
464
  this._currentPhaseStart = Date.now();
465
+ this._snapshot.isMoving = false;
466
+ this._snapshot.velocity = 0;
467
+ if (this._stillTimer) {
468
+ clearTimeout(this._stillTimer);
469
+ this._stillTimer = null;
470
+ }
465
471
  }
466
472
  resetCounts() {
467
473
  this._snapshot.totalClicks = 0;
@@ -567,6 +573,10 @@ class HiddenEntry {
567
573
  this._hintVisibleStart = 0;
568
574
  this._hintCheckInterval = null;
569
575
  this._hintVisibilityHandler = null;
576
+ this._domObserver = null;
577
+ this._intersectionObserver = null;
578
+ this._mutationDebounce = null;
579
+ this._navigationHandler = null;
570
580
  this.config = config;
571
581
  this.script = script;
572
582
  this.onFound = onFound;
@@ -600,6 +610,7 @@ class HiddenEntry {
600
610
  this.targetElement.style.removeProperty("animation");
601
611
  this.targetElement.classList.remove("eeq-entry-target");
602
612
  }
613
+ this._stopWatchdog();
603
614
  (_a2 = this._injectedElement) == null ? void 0 : _a2.remove();
604
615
  this._injectedElement = null;
605
616
  (_b2 = this.hintContainer) == null ? void 0 : _b2.remove();
@@ -620,8 +631,8 @@ class HiddenEntry {
620
631
  ).filter((el) => {
621
632
  var _a2;
622
633
  const text = ((_a2 = el.textContent) == null ? void 0 : _a2.trim()) ?? "";
623
- const rect = el.getBoundingClientRect();
624
- return text.length >= 2 && rect.width > 0 && rect.height > 0 && !el.closest("[data-eeq]");
634
+ if (text.length < 2 || el.closest("[data-eeq]")) return false;
635
+ return isElementVisible(el);
625
636
  });
626
637
  const sorted = candidates.sort((a, b) => {
627
638
  const aNav = a.closest("nav") !== null || a.closest("header") !== null ? 1 : 0;
@@ -708,10 +719,113 @@ class HiddenEntry {
708
719
  this._injectedElement = span;
709
720
  this.targetElement = span;
710
721
  this._attachToElement(span);
722
+ this._startWatchdog();
711
723
  return;
712
724
  }
713
725
  this._createFallbackEntry();
714
726
  }
727
+ /**
728
+ * Watch for the injected trigger element being removed from the DOM
729
+ * (SPA re-render, route change, virtual scroll) or scrolled out of view.
730
+ *
731
+ * Uses IntersectionObserver (zero layout cost) for visibility,
732
+ * a MutationObserver on a stable ancestor (survives Angular/React re-renders),
733
+ * and navigation event listeners for SPA route changes.
734
+ */
735
+ _startWatchdog() {
736
+ this._stopWatchdog();
737
+ if (!this._injectedElement) return;
738
+ this._intersectionObserver = new IntersectionObserver(
739
+ (entries) => {
740
+ if (this._destroyed || !this._injectedElement) return;
741
+ const entry = entries[0];
742
+ if (entry && !entry.isIntersecting) {
743
+ this._reinject();
744
+ }
745
+ },
746
+ { threshold: 0 }
747
+ );
748
+ this._intersectionObserver.observe(this._injectedElement);
749
+ const observeTarget = this._findStableAncestor(this._injectedElement);
750
+ this._domObserver = new MutationObserver(() => {
751
+ if (this._destroyed || !this._injectedElement) return;
752
+ if (this._mutationDebounce) clearTimeout(this._mutationDebounce);
753
+ this._mutationDebounce = setTimeout(() => {
754
+ if (this._destroyed || !this._injectedElement) return;
755
+ if (!document.body.contains(this._injectedElement)) {
756
+ this._reinject();
757
+ }
758
+ }, 300);
759
+ });
760
+ this._domObserver.observe(observeTarget, { childList: true, subtree: true });
761
+ this._navigationHandler = () => {
762
+ if (this._destroyed || !this._injectedElement) return;
763
+ setTimeout(() => {
764
+ if (this._destroyed || !this._injectedElement) return;
765
+ if (!document.body.contains(this._injectedElement)) {
766
+ this._reinject();
767
+ }
768
+ }, 500);
769
+ };
770
+ window.addEventListener("popstate", this._navigationHandler);
771
+ window.addEventListener("hashchange", this._navigationHandler);
772
+ }
773
+ /**
774
+ * Walk up the DOM to find a stable ancestor that won't be replaced
775
+ * by SPA framework routing (Angular <router-outlet>, React root, etc.).
776
+ */
777
+ _findStableAncestor(el) {
778
+ let node = el.parentElement;
779
+ while (node && node !== document.body) {
780
+ const tag = node.tagName.toLowerCase();
781
+ if (tag === "main" || tag === "body" || tag.includes("app-") || // Angular app-root, app-component, etc.
782
+ tag.includes("-root") || // custom element roots
783
+ node.querySelector("router-outlet") !== null || node.id === "app" || node.id === "root" || node.id === "__next") {
784
+ return node;
785
+ }
786
+ node = node.parentElement;
787
+ }
788
+ return document.body;
789
+ }
790
+ _stopWatchdog() {
791
+ if (this._domObserver) {
792
+ this._domObserver.disconnect();
793
+ this._domObserver = null;
794
+ }
795
+ if (this._intersectionObserver) {
796
+ this._intersectionObserver.disconnect();
797
+ this._intersectionObserver = null;
798
+ }
799
+ if (this._mutationDebounce) {
800
+ clearTimeout(this._mutationDebounce);
801
+ this._mutationDebounce = null;
802
+ }
803
+ if (this._navigationHandler) {
804
+ window.removeEventListener("popstate", this._navigationHandler);
805
+ window.removeEventListener("hashchange", this._navigationHandler);
806
+ this._navigationHandler = null;
807
+ }
808
+ }
809
+ /** Remove old trigger (if still in DOM) and re-inject into a new element. */
810
+ _reinject() {
811
+ this._stopWatchdog();
812
+ if (this.clickHandler && this.targetElement) {
813
+ this.targetElement.removeEventListener("click", this.clickHandler);
814
+ }
815
+ if (this._injectedElement && document.body.contains(this._injectedElement)) {
816
+ this._injectedElement.remove();
817
+ }
818
+ this._injectedElement = null;
819
+ this.targetElement = null;
820
+ setTimeout(() => {
821
+ if (this._destroyed) return;
822
+ const eligible = findEligibleEntryElements(
823
+ this.config.hiddenEntry.selector,
824
+ this.config.hiddenEntry.excludeSelectors
825
+ );
826
+ this._injectIntoExisting(eligible);
827
+ }, 1200);
828
+ }
715
829
  /**
716
830
  * Try to insert the trigger span between words inside a text node.
717
831
  * Picks a random word boundary in the element's first direct text node.
@@ -1059,7 +1173,7 @@ class NarrativeRenderer {
1059
1173
  width: "100%",
1060
1174
  height: "100%",
1061
1175
  pointerEvents: "none",
1062
- zIndex: "999990"
1176
+ zIndex: "999993"
1063
1177
  });
1064
1178
  document.body.appendChild(this.host);
1065
1179
  this.shadow = this.host.attachShadow({ mode: "closed" });
@@ -1889,6 +2003,13 @@ class ThreeRenderer {
1889
2003
  }
1890
2004
  }
1891
2005
  }
2006
+ /** Mark all ambient particles for rapid fade-out (rhythm lost). */
2007
+ fadeOutAmbientParticles() {
2008
+ for (const p of this._ambientParticles) {
2009
+ p.fadeIn = false;
2010
+ if (p.life > 0.3) p.life = 0.3;
2011
+ }
2012
+ }
1892
2013
  /** Start the finale composition. */
1893
2014
  startFinale() {
1894
2015
  this._finaleActive = true;
@@ -2209,17 +2330,17 @@ class ThreeRenderer {
2209
2330
  this.eggBodies.push(body3);
2210
2331
  }
2211
2332
  // ── Mini egg helpers for particles ─────────────────────────────────────
2212
- /** Create a tiny egg-shaped geometry (same squash as main eggs, lower poly). */
2333
+ /** Create a tiny egg-shaped geometry rounder, less elongated. */
2213
2334
  _createMiniEggGeo(T, radius) {
2214
- const geo = new T.SphereGeometry(radius, 8, 8);
2335
+ const geo = new T.SphereGeometry(radius, 12, 12);
2215
2336
  const pos = geo.attributes.position;
2216
2337
  for (let i = 0; i < pos.count; i++) {
2217
2338
  let y = pos.getY(i);
2218
2339
  const x = pos.getX(i);
2219
2340
  const z = pos.getZ(i);
2220
- const topSquash = y > 0 ? 0.92 : 1;
2221
- y = y * 1.15 * topSquash;
2222
- const narrowing = 1 - Math.abs(y) * 0.06;
2341
+ const topSquash = y > 0 ? 0.95 : 1;
2342
+ y = y * 1.08 * topSquash;
2343
+ const narrowing = 1 - Math.abs(y) * 0.03;
2223
2344
  pos.setX(i, x * narrowing);
2224
2345
  pos.setY(i, y);
2225
2346
  pos.setZ(i, z * narrowing);
@@ -2235,8 +2356,8 @@ class ThreeRenderer {
2235
2356
  c.height = 64;
2236
2357
  const ctx = c.getContext("2d");
2237
2358
  const hue = Math.random() * 360;
2238
- const sat = 45 + Math.random() * 30;
2239
- const light = 65 + Math.random() * 15;
2359
+ const sat = 60 + Math.random() * 25;
2360
+ const light = 68 + Math.random() * 14;
2240
2361
  ctx.fillStyle = `hsl(${hue}, ${sat}%, ${light}%)`;
2241
2362
  ctx.fillRect(0, 0, 64, 64);
2242
2363
  const accentHue = (hue + 80 + Math.random() * 100) % 360;
@@ -2307,7 +2428,7 @@ class ThreeRenderer {
2307
2428
  return new T.MeshBasicMaterial({
2308
2429
  map: tex,
2309
2430
  transparent: true,
2310
- opacity
2431
+ opacity: opacity || 0.85
2311
2432
  });
2312
2433
  }
2313
2434
  _createEggGeometry(T) {
@@ -3037,12 +3158,12 @@ class ThreeRenderer {
3037
3158
  p.mesh.rotation.z += dt * 0.5;
3038
3159
  if (p.fadeIn && p.life > 0.7) {
3039
3160
  p.mesh.material.opacity = Math.min(
3040
- 0.55,
3041
- p.mesh.material.opacity + dt * 0.6
3161
+ 0.9,
3162
+ p.mesh.material.opacity + dt * 1
3042
3163
  );
3043
- if (p.mesh.material.opacity >= 0.5) p.fadeIn = false;
3164
+ if (p.mesh.material.opacity >= 0.85) p.fadeIn = false;
3044
3165
  } else {
3045
- p.mesh.material.opacity = Math.max(0, p.life * 0.55);
3166
+ p.mesh.material.opacity = Math.max(0, p.life * 0.85);
3046
3167
  }
3047
3168
  if (p.life <= 0) {
3048
3169
  this.scene.remove(p.mesh);
@@ -4511,6 +4632,7 @@ class RhythmStage {
4511
4632
  if (isGood) {
4512
4633
  this._goodCycles++;
4513
4634
  } else {
4635
+ this._goodCycles = Math.max(0, this._goodCycles - 1);
4514
4636
  this._showReaction();
4515
4637
  }
4516
4638
  this._lastPhaseCount += 2;
@@ -4670,6 +4792,7 @@ class PageBreather {
4670
4792
  this._startTime = 0;
4671
4793
  this._isUserMoving = false;
4672
4794
  this._inSync = false;
4795
+ this._accuracy = 0;
4673
4796
  this.CYCLE_MS = 6e3;
4674
4797
  this._tick = () => {
4675
4798
  if (!this._active || !this._overlay) return;
@@ -4679,7 +4802,7 @@ class PageBreather {
4679
4802
  const phase = elapsed % this.CYCLE_MS / this.CYCLE_MS;
4680
4803
  const wave = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) * 0.5;
4681
4804
  const shouldMove = phase < 0.5;
4682
- this._inSync = shouldMove === this._isUserMoving;
4805
+ this._inSync = this._accuracy > 0 && shouldMove === this._isUserMoving;
4683
4806
  const baseOpacity = wave * 0.85;
4684
4807
  const syncBonus = this._inSync ? 0.15 : 0;
4685
4808
  const opacity = this._intensity * (baseOpacity + (1 - wave) * 0.05 + syncBonus);
@@ -4710,6 +4833,7 @@ class PageBreather {
4710
4833
  update(accuracy, isMoving) {
4711
4834
  if (!this._active) return;
4712
4835
  this._isUserMoving = isMoving;
4836
+ this._accuracy = accuracy;
4713
4837
  const raw = Math.max(0, Math.min(1, (accuracy - 0.05) / 0.5));
4714
4838
  this._targetIntensity = 0.5 + raw * 0.5;
4715
4839
  }
@@ -4727,6 +4851,7 @@ class PageBreather {
4727
4851
  this._intensity = 0;
4728
4852
  this._targetIntensity = 0;
4729
4853
  this._inSync = false;
4854
+ this._accuracy = 0;
4730
4855
  }
4731
4856
  destroy() {
4732
4857
  this.stop();
@@ -4825,11 +4950,13 @@ class GameController {
4825
4950
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.addTrail(data.x, data.y, data.velocity);
4826
4951
  });
4827
4952
  this.bus.on("rhythm:breathe", (data) => {
4828
- var _a3, _b3, _c;
4953
+ var _a3, _b3;
4829
4954
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.setBreathIntensity(data.accuracy);
4830
4955
  (_b3 = this.pageBreather) == null ? void 0 : _b3.update(data.accuracy, data.isMoving);
4831
- if (((_c = this.pageBreather) == null ? void 0 : _c.isInSync()) && this.threeRenderer) {
4956
+ if (data.accuracy > 0 && this.threeRenderer) {
4832
4957
  this.threeRenderer.showProgressParticles(0.6 + data.accuracy * 0.4);
4958
+ } else if (this.threeRenderer) {
4959
+ this.threeRenderer.fadeOutAmbientParticles();
4833
4960
  }
4834
4961
  });
4835
4962
  this.overlay = new OverlayManager(this.config);
@@ -5147,6 +5274,7 @@ class GameController {
5147
5274
  }
5148
5275
  const successLines = eggIndex === 0 ? this.script.stage1Success : eggIndex === 1 ? this.script.stage2Success : this.script.stage3Success;
5149
5276
  await ((_c = this.narrative) == null ? void 0 : _c.showSequence(successLines, 4500));
5277
+ await this._wait(3e3);
5150
5278
  (_d = this.narrative) == null ? void 0 : _d.clear();
5151
5279
  await this._wait(800);
5152
5280
  if (this.threeRenderer) {