easter-egg-quest 1.0.21 → 1.0.23

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.
@@ -142,7 +142,7 @@ function resolveConfig(raw = {}) {
142
142
  stageDurations: {
143
143
  stillnessMs: clampDuration((_i = raw.stageDurations) == null ? void 0 : _i.stillnessMs, 2e4),
144
144
  motionMs: clampDuration((_j = raw.stageDurations) == null ? void 0 : _j.motionMs, 2e4),
145
- rhythmCycles: clampInt((_k = raw.stageDurations) == null ? void 0 : _k.rhythmCycles, 6, 1, 50)
145
+ rhythmCycles: clampInt((_k = raw.stageDurations) == null ? void 0 : _k.rhythmCycles, 3, 1, 50)
146
146
  },
147
147
  renderer: raw.renderer ?? "auto",
148
148
  scoring: {
@@ -2178,6 +2178,41 @@ class ThreeRenderer {
2178
2178
  }
2179
2179
  }
2180
2180
  }
2181
+ burstRhythmParticles(progress, cycles) {
2182
+ if (!this.THREE || !this.scene) return;
2183
+ const T = this.THREE;
2184
+ const burstSize = Math.min(14, 6 + Math.floor(progress * 6) + Math.min(3, Math.floor(cycles)));
2185
+ for (let i = 0; i < burstSize; i++) {
2186
+ const angle = Math.PI * 2 * i / burstSize + (Math.random() - 0.5) * 0.35;
2187
+ const radius = 0.45 + Math.random() * 2.15;
2188
+ const x = Math.cos(angle) * radius;
2189
+ const y = -2.95 - Math.random() * 0.75;
2190
+ const size = 0.036 + Math.random() * 0.038;
2191
+ const geo = this._createMiniEggGeo(T, size);
2192
+ const mat = this._createMiniEggMat(T, 0.18 + Math.random() * 0.12);
2193
+ const particle = new T.Mesh(geo, mat);
2194
+ particle.position.set(x, y, -1 + Math.random() * 0.8);
2195
+ particle.rotation.set(
2196
+ Math.random() * Math.PI,
2197
+ Math.random() * Math.PI,
2198
+ Math.random() * Math.PI
2199
+ );
2200
+ this.scene.add(particle);
2201
+ this._ambientParticles.push({
2202
+ mesh: particle,
2203
+ life: 1.7 + Math.random() * 0.45,
2204
+ vy: 0.85 + Math.random() * 0.65,
2205
+ fadeIn: true
2206
+ });
2207
+ }
2208
+ while (this._ambientParticles.length > 120) {
2209
+ const old = this._ambientParticles.shift();
2210
+ if (!old) break;
2211
+ this.scene.remove(old.mesh);
2212
+ old.mesh.geometry.dispose();
2213
+ old.mesh.material.dispose();
2214
+ }
2215
+ }
2181
2216
  /** Mark all ambient particles for rapid fade-out (rhythm lost). */
2182
2217
  fadeOutAmbientParticles() {
2183
2218
  for (const p of this._ambientParticles) {
@@ -4500,6 +4535,7 @@ class ScoringEngine {
4500
4535
  }
4501
4536
  const STORAGE_KEY = "eeq_scores";
4502
4537
  const PROGRESS_KEY = "eeq_progress";
4538
+ const COMPLETION_KEY = "eeq_completed";
4503
4539
  class Persistence {
4504
4540
  constructor(enabled) {
4505
4541
  this._enabled = enabled;
@@ -4572,6 +4608,22 @@ class Persistence {
4572
4608
  } catch {
4573
4609
  }
4574
4610
  }
4611
+ markCompleted() {
4612
+ if (!this._enabled) return;
4613
+ try {
4614
+ localStorage.setItem(COMPLETION_KEY, "1");
4615
+ localStorage.removeItem(PROGRESS_KEY);
4616
+ } catch {
4617
+ }
4618
+ }
4619
+ isCompleted() {
4620
+ if (!this._enabled) return false;
4621
+ try {
4622
+ return localStorage.getItem(COMPLETION_KEY) === "1";
4623
+ } catch {
4624
+ return false;
4625
+ }
4626
+ }
4575
4627
  }
4576
4628
  class StillnessStage {
4577
4629
  constructor(config, script, input, bus) {
@@ -4791,6 +4843,7 @@ class RhythmStage {
4791
4843
  this._lastPhaseCount = 0;
4792
4844
  this._bestAccuracy = 0;
4793
4845
  this._lastGuideTime = 0;
4846
+ this._lastGoodCycleAt = 0;
4794
4847
  this.IDEAL_MOVE_MIN = 900;
4795
4848
  this.IDEAL_MOVE_MAX = 4200;
4796
4849
  this.IDEAL_PAUSE_MIN = 600;
@@ -4839,6 +4892,11 @@ class RhythmStage {
4839
4892
  this._totalCycles++;
4840
4893
  if (isGood) {
4841
4894
  this._goodCycles++;
4895
+ this._lastGoodCycleAt = Date.now();
4896
+ this.bus.emit("rhythm:cycle", {
4897
+ progress: Math.min(1, this._goodCycles / requiredCycles),
4898
+ cycles: this._goodCycles
4899
+ });
4842
4900
  } else {
4843
4901
  this._goodCycles = Math.max(0, this._goodCycles - 0.5);
4844
4902
  this._showReaction();
@@ -4851,6 +4909,9 @@ class RhythmStage {
4851
4909
  const accuracy = this._totalCycles > 0 ? this._goodCycles / this._totalCycles : 0;
4852
4910
  if (accuracy > this._bestAccuracy) this._bestAccuracy = accuracy;
4853
4911
  const progress = Math.min(1, this._goodCycles / requiredCycles);
4912
+ const targetPhase = this._getTargetPhase();
4913
+ const phaseMatch = this.input.snapshot.isMoving === (targetPhase === "move");
4914
+ const cycleSignal = this._lastGoodCycleAt > 0 ? Math.max(0, 1 - (Date.now() - this._lastGoodCycleAt) / 2200) : 0;
4854
4915
  this.bus.emit("stage:progress", progress);
4855
4916
  this.bus.emit("rhythm:breathe", {
4856
4917
  accuracy,
@@ -4858,7 +4919,10 @@ class RhythmStage {
4858
4919
  progress,
4859
4920
  moveDuration: this.input.moveDuration,
4860
4921
  stillDuration: this.input.stillDuration,
4861
- liveConfidence: this._getLiveConfidence()
4922
+ liveConfidence: this._getLiveConfidence(),
4923
+ targetPhase,
4924
+ phaseMatch,
4925
+ cycleSignal
4862
4926
  });
4863
4927
  if (accuracy === 0 && Date.now() - this._lastGuideTime > 12e3) {
4864
4928
  this._lastGuideTime = Date.now();
@@ -4897,6 +4961,12 @@ class RhythmStage {
4897
4961
  }
4898
4962
  return this._scoreDuration(this.input.stillDuration, this.IDEAL_PAUSE_MIN, this.IDEAL_PAUSE_MAX);
4899
4963
  }
4964
+ _getTargetPhase() {
4965
+ if (this.input.snapshot.isMoving) {
4966
+ return this.input.moveDuration >= this.IDEAL_MOVE_MIN ? "still" : "move";
4967
+ }
4968
+ return this.input.stillDuration >= this.IDEAL_PAUSE_MIN ? "move" : "still";
4969
+ }
4900
4970
  _scoreDuration(duration, idealMin, idealMax) {
4901
4971
  if (duration <= 0) return 0;
4902
4972
  if (duration >= idealMin && duration <= idealMax) return 1;
@@ -5024,32 +5094,26 @@ class PageBreather {
5024
5094
  this._targetIntensity = 0;
5025
5095
  this._rafId = 0;
5026
5096
  this._overlay = null;
5027
- this._label = null;
5028
5097
  this._startTime = 0;
5029
5098
  this._isUserMoving = false;
5030
5099
  this._inSync = false;
5031
5100
  this._accuracy = 0;
5032
- this.CYCLE_MS = 6e3;
5101
+ this._targetPhase = "move";
5102
+ this._phaseMatch = false;
5103
+ this._signal = 0;
5033
5104
  this._tick = () => {
5034
5105
  if (!this._active || !this._overlay) return;
5035
5106
  this._rafId = requestAnimationFrame(this._tick);
5036
5107
  this._intensity += (this._targetIntensity - this._intensity) * 0.06;
5037
5108
  const elapsed = Date.now() - this._startTime;
5038
- const phase = elapsed % this.CYCLE_MS / this.CYCLE_MS;
5039
- const wave = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) * 0.5;
5040
- const shouldMove = phase < 0.5;
5041
- this._inSync = this._accuracy > 0 && shouldMove === this._isUserMoving;
5042
- const baseOpacity = wave * 0.85;
5043
- const syncBonus = this._inSync ? 0.22 : 0;
5044
- const opacity = this._intensity * (0.18 + baseOpacity + (1 - wave) * 0.12 + syncBonus);
5109
+ const wave = (Math.sin(elapsed * 4e-3) + 1) * 0.5;
5110
+ const shouldMove = this._targetPhase === "move";
5111
+ this._inSync = this._phaseMatch && this._signal > 0.15;
5112
+ const baseOpacity = shouldMove ? 0.12 + wave * 0.08 : 0.32 + wave * 0.18;
5113
+ const syncBonus = this._inSync ? 0.12 + this._signal * 0.18 : 0;
5114
+ const opacity = this._intensity * (baseOpacity + syncBonus);
5045
5115
  this._overlay.style.opacity = String(Math.min(0.9, opacity));
5046
- this._overlay.style.transform = `scale(${1 + wave * 0.015})`;
5047
- if (this._label) {
5048
- this._label.textContent = shouldMove ? "In" : "Out";
5049
- this._label.style.opacity = shouldMove === this._isUserMoving ? "0.98" : "0.82";
5050
- this._label.style.boxShadow = this._inSync ? "0 0 28px rgba(212,165,80,0.4)" : "0 0 20px rgba(255,255,255,0.08)";
5051
- this._label.style.borderColor = this._inSync ? "rgba(212,165,80,0.55)" : "rgba(255,255,255,0.12)";
5052
- }
5116
+ this._overlay.style.transform = `scale(${1 + wave * (shouldMove ? 0.01 : 0.02)})`;
5053
5117
  };
5054
5118
  }
5055
5119
  start() {
@@ -5071,55 +5135,36 @@ class PageBreather {
5071
5135
  ].join(";");
5072
5136
  document.body.appendChild(el);
5073
5137
  this._overlay = el;
5074
- const label = document.createElement("div");
5075
- label.style.cssText = [
5076
- "position:fixed",
5077
- "left:50%",
5078
- "top:18%",
5079
- "transform:translateX(-50%)",
5080
- "pointer-events:none",
5081
- "z-index:999981",
5082
- "padding:8px 14px",
5083
- "border-radius:999px",
5084
- "font-family:Georgia, Times New Roman, serif",
5085
- "font-size:12px",
5086
- "letter-spacing:0.12em",
5087
- "text-transform:uppercase",
5088
- "color:rgba(255,248,232,0.95)",
5089
- "background:rgba(8,10,18,0.36)",
5090
- "border:1px solid rgba(255,255,255,0.12)",
5091
- "box-shadow:0 0 20px rgba(255,255,255,0.08)",
5092
- "opacity:0.78"
5093
- ].join(";");
5094
- label.textContent = "In";
5095
- document.body.appendChild(label);
5096
- this._label = label;
5097
5138
  this._tick();
5098
5139
  }
5099
- update(accuracy, isMoving) {
5140
+ update(accuracy, isMoving, targetPhase, phaseMatch, signal) {
5100
5141
  if (!this._active) return;
5101
5142
  this._isUserMoving = isMoving;
5102
5143
  this._accuracy = accuracy;
5103
- const raw = Math.max(0, Math.min(1, (accuracy - 0.02) / 0.45));
5104
- this._targetIntensity = 0.68 + raw * 0.32;
5144
+ this._targetPhase = targetPhase;
5145
+ this._phaseMatch = phaseMatch;
5146
+ this._signal = signal;
5147
+ const raw = Math.max(0, Math.min(1, Math.max(accuracy, signal) / 0.8));
5148
+ this._targetIntensity = 0.38 + raw * 0.62;
5105
5149
  }
5106
5150
  /** Whether the user is currently in sync with the breathing rhythm. */
5107
5151
  isInSync() {
5108
5152
  return this._inSync;
5109
5153
  }
5110
5154
  stop() {
5111
- var _a2, _b2;
5155
+ var _a2;
5112
5156
  if (!this._active) return;
5113
5157
  this._active = false;
5114
5158
  cancelAnimationFrame(this._rafId);
5115
5159
  (_a2 = this._overlay) == null ? void 0 : _a2.remove();
5116
5160
  this._overlay = null;
5117
- (_b2 = this._label) == null ? void 0 : _b2.remove();
5118
- this._label = null;
5119
5161
  this._intensity = 0;
5120
5162
  this._targetIntensity = 0;
5121
5163
  this._inSync = false;
5122
5164
  this._accuracy = 0;
5165
+ this._targetPhase = "move";
5166
+ this._phaseMatch = false;
5167
+ this._signal = 0;
5123
5168
  }
5124
5169
  destroy() {
5125
5170
  this.stop();
@@ -5197,6 +5242,10 @@ class GameController {
5197
5242
  // ─── Lifecycle ─────────────────────────────────────────────────────────
5198
5243
  async init() {
5199
5244
  var _a2, _b2;
5245
+ if (this.persistence.isCompleted()) {
5246
+ this._destroyed = true;
5247
+ return;
5248
+ }
5200
5249
  this.fsm.onTransition((from, to) => this._onTransition(from, to));
5201
5250
  this.bus.on("narrative:show", (text) => {
5202
5251
  var _a3;
@@ -5217,14 +5266,21 @@ class GameController {
5217
5266
  var _a3;
5218
5267
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.addTrail(data.x, data.y, data.velocity);
5219
5268
  });
5269
+ this.bus.on("rhythm:cycle", (data) => {
5270
+ if (!this.threeRenderer) return;
5271
+ this.threeRenderer.burstRhythmParticles(data.progress, data.cycles);
5272
+ });
5220
5273
  this.bus.on("rhythm:breathe", (data) => {
5221
5274
  var _a3, _b3;
5222
5275
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.setBreathIntensity(data.accuracy);
5223
- (_b3 = this.pageBreather) == null ? void 0 : _b3.update(data.accuracy, data.isMoving);
5224
- const rhythmSignal = Math.max(data.accuracy, data.progress * 0.75, data.liveConfidence * 0.85);
5225
- if (rhythmSignal > 0.18 && this.threeRenderer) {
5226
- this.threeRenderer.showProgressParticles(0.3 + rhythmSignal * 0.7, "rhythm");
5227
- } else if (this.threeRenderer) {
5276
+ (_b3 = this.pageBreather) == null ? void 0 : _b3.update(
5277
+ data.accuracy,
5278
+ data.isMoving,
5279
+ data.targetPhase,
5280
+ data.phaseMatch,
5281
+ Math.max(data.liveConfidence * 0.55, data.cycleSignal)
5282
+ );
5283
+ if (data.cycleSignal <= 0.12 && this.threeRenderer) {
5228
5284
  this.threeRenderer.fadeOutAmbientParticles();
5229
5285
  }
5230
5286
  });
@@ -5406,6 +5462,7 @@ class GameController {
5406
5462
  // ─── Entry ─────────────────────────────────────────────────────────────
5407
5463
  async _startEntry() {
5408
5464
  var _a2;
5465
+ if (this.persistence.isCompleted()) return;
5409
5466
  try {
5410
5467
  if (localStorage.getItem("eeq_optout") === "1") return;
5411
5468
  } catch {
@@ -5655,7 +5712,9 @@ class GameController {
5655
5712
  this.results.show(
5656
5713
  score,
5657
5714
  () => {
5715
+ this.persistence.markCompleted();
5658
5716
  this.destroy();
5717
+ window.location.reload();
5659
5718
  }
5660
5719
  );
5661
5720
  (_c = (_b2 = this.config.callbacks).onComplete) == null ? void 0 : _c.call(_b2, score);