easter-egg-quest 1.0.18 → 1.0.20

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.
@@ -57,33 +57,25 @@ const DEFAULT_SCRIPT = {
57
57
  stage2Success: [
58
58
  "movement was the answer",
59
59
  "the second egg appears",
60
- "for those who never stop",
61
- "not stillness this time",
62
- "but life in motion"
60
+ "hold it to collect"
63
61
  ],
64
62
  // ── Stage 3: Rhythm ───────────────────────────────────────────────────
65
63
  stage3Intro: [
66
- "two truths that shouldn’t exist together",
67
64
  "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."
65
+ "Every living thing knows it. You’ve been doing it your whole life without thinking."
69
66
  ],
70
67
  stage3Reactions: [
71
- "one truth alone isn’t enough",
72
- "the other alone isn’t either",
68
+ "one truth alone is not enough",
69
+ "the other alone is not enough either",
73
70
  "what do all living things share?",
74
- "you already know the answer",
75
- "you’ve been doing it since birth",
71
+ "you have known it since the beginning",
76
72
  "listen to yourself",
77
- "closer...",
78
- "can you feel it?",
79
- "the body remembers",
80
73
  "in",
81
74
  "out"
82
75
  ],
83
76
  stage3Success: [
84
77
  "rhythm was the answer",
85
- "you found the third egg",
86
- "nice work"
78
+ "you found the third egg"
87
79
  ],
88
80
  // ── Finale ────────────────────────────────────────────────────────────
89
81
  finale: [
@@ -331,8 +323,10 @@ class InputTracker {
331
323
  this._prevX = 0;
332
324
  this._prevY = 0;
333
325
  this._prevMoveTs = 0;
334
- this.STILL_THRESHOLD_MS = 400;
335
- this.MIN_PHASE_MS = 300;
326
+ this.DEFAULT_STILL_THRESHOLD_MS = 400;
327
+ this._stillThresholdMs = this.DEFAULT_STILL_THRESHOLD_MS;
328
+ this.DEFAULT_MIN_PHASE_MS = 300;
329
+ this._minPhaseMs = this.DEFAULT_MIN_PHASE_MS;
336
330
  this._stillTimer = null;
337
331
  this._handlers = [];
338
332
  this._active = false;
@@ -471,6 +465,18 @@ class InputTracker {
471
465
  this._snapshot.totalDistance = 0;
472
466
  this._snapshot.maxVelocity = 0;
473
467
  }
468
+ configurePhaseDetection(opts) {
469
+ if (typeof opts.stillThresholdMs === "number") {
470
+ this._stillThresholdMs = Math.max(250, opts.stillThresholdMs);
471
+ }
472
+ if (typeof opts.minPhaseMs === "number") {
473
+ this._minPhaseMs = Math.max(150, opts.minPhaseMs);
474
+ }
475
+ }
476
+ resetPhaseDetection() {
477
+ this._stillThresholdMs = this.DEFAULT_STILL_THRESHOLD_MS;
478
+ this._minPhaseMs = this.DEFAULT_MIN_PHASE_MS;
479
+ }
474
480
  // ─── Movement / Stillness transition logic ─────────────────────────────
475
481
  _registerMove(now) {
476
482
  this._snapshot.lastMoveTime = now;
@@ -483,7 +489,7 @@ class InputTracker {
483
489
  if (this._stillTimer) clearTimeout(this._stillTimer);
484
490
  this._stillTimer = setTimeout(() => {
485
491
  this._becomeStill();
486
- }, this.STILL_THRESHOLD_MS);
492
+ }, this._stillThresholdMs);
487
493
  }
488
494
  _becomeStill() {
489
495
  if (!this._snapshot.isMoving) return;
@@ -497,7 +503,7 @@ class InputTracker {
497
503
  }
498
504
  _completePhase(type, endTime) {
499
505
  const duration = endTime - this._currentPhaseStart;
500
- if (duration >= this.MIN_PHASE_MS) {
506
+ if (duration >= this._minPhaseMs) {
501
507
  this._phases.push({
502
508
  type,
503
509
  startTime: this._currentPhaseStart,
@@ -1312,6 +1318,7 @@ class NarrativeRenderer {
1312
1318
  this.container = null;
1313
1319
  this.currentLine = null;
1314
1320
  this.clearTimer = null;
1321
+ this.awaitingConfirm = false;
1315
1322
  this.config = config;
1316
1323
  }
1317
1324
  mount() {
@@ -1335,14 +1342,22 @@ class NarrativeRenderer {
1335
1342
  this.container.className = "eeq-narrative";
1336
1343
  this.shadow.appendChild(this.container);
1337
1344
  }
1345
+ _removeCurrentLine(immediate = false) {
1346
+ if (!this.currentLine) return;
1347
+ const prev = this.currentLine;
1348
+ this.currentLine = null;
1349
+ if (immediate) {
1350
+ prev.remove();
1351
+ return;
1352
+ }
1353
+ prev.classList.add("eeq-narrative-exit");
1354
+ setTimeout(() => prev.remove(), 750);
1355
+ }
1338
1356
  /** Show a single narrative line. Fades in, replaces any previous line. */
1339
1357
  showLine(text, className = "eeq-line") {
1340
1358
  if (!this.container) return;
1341
- if (this.currentLine) {
1342
- const prev = this.currentLine;
1343
- prev.classList.add("eeq-narrative-exit");
1344
- setTimeout(() => prev.remove(), 750);
1345
- }
1359
+ if (this.awaitingConfirm) return;
1360
+ this._removeCurrentLine(true);
1346
1361
  if (this.clearTimer) {
1347
1362
  clearTimeout(this.clearTimer);
1348
1363
  this.clearTimer = null;
@@ -1371,11 +1386,8 @@ class NarrativeRenderer {
1371
1386
  resolve();
1372
1387
  return;
1373
1388
  }
1374
- if (this.currentLine) {
1375
- const prev = this.currentLine;
1376
- prev.classList.add("eeq-narrative-exit");
1377
- setTimeout(() => prev.remove(), 750);
1378
- }
1389
+ this.awaitingConfirm = true;
1390
+ this._removeCurrentLine(true);
1379
1391
  if (this.clearTimer) {
1380
1392
  clearTimeout(this.clearTimer);
1381
1393
  this.clearTimer = null;
@@ -1387,7 +1399,10 @@ class NarrativeRenderer {
1387
1399
  const btn = document.createElement("button");
1388
1400
  btn.className = "eeq-confirm-btn";
1389
1401
  btn.textContent = "OK";
1390
- btn.addEventListener("click", () => resolve(), { once: true });
1402
+ btn.addEventListener("click", () => {
1403
+ this.awaitingConfirm = false;
1404
+ resolve();
1405
+ }, { once: true });
1391
1406
  line.appendChild(btn);
1392
1407
  requestAnimationFrame(() => {
1393
1408
  requestAnimationFrame(() => {
@@ -1409,22 +1424,19 @@ class NarrativeRenderer {
1409
1424
  this.showLine(text, "eeq-line eeq-celebration");
1410
1425
  }
1411
1426
  /** Clear current narrative text. */
1412
- clear() {
1427
+ clear(force = false) {
1428
+ if (this.awaitingConfirm && !force) return;
1413
1429
  if (this.clearTimer) {
1414
1430
  clearTimeout(this.clearTimer);
1415
1431
  this.clearTimer = null;
1416
1432
  }
1417
- if (this.currentLine) {
1418
- this.currentLine.classList.add("eeq-narrative-exit");
1419
- const ref = this.currentLine;
1420
- setTimeout(() => ref.remove(), 750);
1421
- this.currentLine = null;
1422
- }
1433
+ this._removeCurrentLine(force);
1434
+ this.awaitingConfirm = false;
1423
1435
  }
1424
1436
  /** Remove all DOM. */
1425
1437
  destroy() {
1426
1438
  var _a2;
1427
- this.clear();
1439
+ this.clear(true);
1428
1440
  (_a2 = this.host) == null ? void 0 : _a2.remove();
1429
1441
  this.host = null;
1430
1442
  this.shadow = null;
@@ -1753,6 +1765,7 @@ class ThreeRenderer {
1753
1765
  this._greetingOverlay = null;
1754
1766
  this._trailParticles = [];
1755
1767
  this._ambientParticles = [];
1768
+ this._lastAmbientSpawnAt = 0;
1756
1769
  this._revealParticles = [];
1757
1770
  this._disposed = false;
1758
1771
  this._interactiveEgg = null;
@@ -2119,32 +2132,43 @@ class ThreeRenderer {
2119
2132
  }
2120
2133
  }
2121
2134
  }
2122
- /** Spawn gentle ambient particles to show stage progress. */
2123
- showProgressParticles(progress) {
2135
+ /** Spawn ambient particles to show stage progress. */
2136
+ showProgressParticles(progress, source = "default") {
2124
2137
  if (!this.THREE || !this.scene || progress < 0.05) return;
2125
2138
  const T = this.THREE;
2126
- if (Math.random() > progress * 0.3) return;
2127
- const angle = Math.random() * Math.PI * 2;
2128
- const radius = 2 + Math.random() * 2;
2129
- const x = Math.cos(angle) * radius;
2130
- const y = -2 + Math.random() * 4;
2131
- const size = 0.02 + Math.random() * 0.025;
2132
- const geo = this._createMiniEggGeo(T, size);
2133
- const mat = this._createMiniEggMat(T, 0);
2134
- const p = new T.Mesh(geo, mat);
2135
- p.position.set(x, y, -1 + Math.random() * 0.5);
2136
- p.rotation.set(
2137
- Math.random() * Math.PI,
2138
- Math.random() * Math.PI,
2139
- Math.random() * Math.PI
2140
- );
2141
- this.scene.add(p);
2142
- this._ambientParticles.push({
2143
- mesh: p,
2144
- life: 1,
2145
- vy: 0.2 + Math.random() * 0.3,
2146
- fadeIn: true
2147
- });
2139
+ const now = performance.now();
2140
+ const isRhythm = source === "rhythm";
2141
+ if (isRhythm) {
2142
+ const minGap = Math.max(80, 180 - progress * 120);
2143
+ if (now - this._lastAmbientSpawnAt < minGap) return;
2144
+ } else if (Math.random() > progress * 0.3) {
2145
+ return;
2146
+ }
2147
+ this._lastAmbientSpawnAt = now;
2148
+ const spawnCount = isRhythm && progress > 0.6 ? 2 : 1;
2149
+ for (let i = 0; i < spawnCount; i++) {
2150
+ const angle = Math.random() * Math.PI * 2;
2151
+ const radius = isRhythm ? 1.1 + Math.random() * 1.8 : 2 + Math.random() * 2;
2152
+ const x = Math.cos(angle) * radius;
2153
+ const y = isRhythm ? -2.8 - Math.random() * 0.9 : -2 + Math.random() * 4;
2154
+ const size = isRhythm ? 0.03 + Math.random() * 0.03 : 0.02 + Math.random() * 0.025;
2155
+ const geo = this._createMiniEggGeo(T, size);
2156
+ const mat = this._createMiniEggMat(T, 0);
2157
+ const p = new T.Mesh(geo, mat);
2158
+ p.position.set(x, y, -1 + Math.random() * 0.5);
2159
+ p.rotation.set(
2160
+ Math.random() * Math.PI,
2161
+ Math.random() * Math.PI,
2162
+ Math.random() * Math.PI
2163
+ );
2164
+ this.scene.add(p);
2165
+ this._ambientParticles.push({
2166
+ mesh: p,
2167
+ life: isRhythm ? 1.25 : 1,
2168
+ vy: isRhythm ? 0.45 + Math.random() * 0.35 : 0.2 + Math.random() * 0.3,
2169
+ fadeIn: true
2170
+ });
2171
+ }
2148
2172
  if (this._ambientParticles.length > 60) {
2149
2173
  const old = this._ambientParticles.shift();
2150
2174
  if (old) {
@@ -4569,6 +4593,8 @@ class StillnessStage {
4569
4593
  this._attempts = 1;
4570
4594
  this.input.resetPhases();
4571
4595
  this.input.resetCounts();
4596
+ this.bus.emit("narrative:clear");
4597
+ await this._wait(850);
4572
4598
  this._introPlayed = false;
4573
4599
  const introLines = this.script.stage1Intro;
4574
4600
  for (let i = 0; i < introLines.length; i++) {
@@ -4663,6 +4689,8 @@ class MotionStage {
4663
4689
  this._attempts = 1;
4664
4690
  this.input.resetPhases();
4665
4691
  this.input.resetCounts();
4692
+ this.bus.emit("narrative:clear");
4693
+ await this._wait(850);
4666
4694
  this._introPlayed = false;
4667
4695
  const introLines = this.script.stage2Intro;
4668
4696
  for (let i = 0; i < introLines.length; i++) {
@@ -4762,10 +4790,11 @@ class RhythmStage {
4762
4790
  this._totalCycles = 0;
4763
4791
  this._lastPhaseCount = 0;
4764
4792
  this._bestAccuracy = 0;
4765
- this.IDEAL_MOVE_MIN = 1800;
4766
- this.IDEAL_MOVE_MAX = 5e3;
4767
- this.IDEAL_PAUSE_MIN = 1200;
4768
- this.IDEAL_PAUSE_MAX = 4e3;
4793
+ this._lastGuideTime = 0;
4794
+ this.IDEAL_MOVE_MIN = 900;
4795
+ this.IDEAL_MOVE_MAX = 4200;
4796
+ this.IDEAL_PAUSE_MIN = 600;
4797
+ this.IDEAL_PAUSE_MAX = 2800;
4769
4798
  this.config = config;
4770
4799
  this.script = script;
4771
4800
  this.input = input;
@@ -4776,8 +4805,11 @@ class RhythmStage {
4776
4805
  this._attempts = 1;
4777
4806
  this._goodCycles = 0;
4778
4807
  this._totalCycles = 0;
4808
+ this.input.configurePhaseDetection({ stillThresholdMs: 450, minPhaseMs: 350 });
4779
4809
  this.input.resetPhases();
4780
4810
  this.input.resetCounts();
4811
+ this.bus.emit("narrative:clear");
4812
+ await this._wait(850);
4781
4813
  this._introPlayed = false;
4782
4814
  const introLines = this.script.stage3Intro;
4783
4815
  for (let i = 0; i < introLines.length; i++) {
@@ -4793,6 +4825,7 @@ class RhythmStage {
4793
4825
  this.input.resetPhases();
4794
4826
  this._lastPhaseCount = this.input.phases.length;
4795
4827
  this._introPlayed = true;
4828
+ this._lastGuideTime = Date.now();
4796
4829
  }
4797
4830
  update(_dt) {
4798
4831
  if (this._status === "complete" || !this._introPlayed) return this._status;
@@ -4807,7 +4840,7 @@ class RhythmStage {
4807
4840
  if (isGood) {
4808
4841
  this._goodCycles++;
4809
4842
  } else {
4810
- this._goodCycles = Math.max(0, this._goodCycles - 1);
4843
+ this._goodCycles = Math.max(0, this._goodCycles - 0.5);
4811
4844
  this._showReaction();
4812
4845
  }
4813
4846
  this._lastPhaseCount += 2;
@@ -4819,7 +4852,17 @@ class RhythmStage {
4819
4852
  if (accuracy > this._bestAccuracy) this._bestAccuracy = accuracy;
4820
4853
  const progress = Math.min(1, this._goodCycles / requiredCycles);
4821
4854
  this.bus.emit("stage:progress", progress);
4822
- this.bus.emit("rhythm:breathe", { accuracy, isMoving: this.input.snapshot.isMoving });
4855
+ this.bus.emit("rhythm:breathe", {
4856
+ accuracy,
4857
+ isMoving: this.input.snapshot.isMoving,
4858
+ progress,
4859
+ moveDuration: this.input.moveDuration,
4860
+ stillDuration: this.input.stillDuration
4861
+ });
4862
+ if (accuracy === 0 && Date.now() - this._lastGuideTime > 12e3) {
4863
+ this._lastGuideTime = Date.now();
4864
+ this._showReaction();
4865
+ }
4823
4866
  if (this._goodCycles >= requiredCycles) {
4824
4867
  this._status = "complete";
4825
4868
  this.bus.emit("stage:progress", 1);
@@ -4829,6 +4872,7 @@ class RhythmStage {
4829
4872
  return this._status;
4830
4873
  }
4831
4874
  cleanup() {
4875
+ this.input.resetPhaseDetection();
4832
4876
  }
4833
4877
  getResult() {
4834
4878
  return {
@@ -4848,7 +4892,7 @@ class RhythmStage {
4848
4892
  }
4849
4893
  _showReaction() {
4850
4894
  const now = Date.now();
4851
- if (now - this._lastNarrativeTime > 6e4) {
4895
+ if (now - this._lastNarrativeTime > 12e3) {
4852
4896
  this._lastNarrativeTime = now;
4853
4897
  const reactions = this.script.stage3Reactions;
4854
4898
  const line = reactions[this._narrativeIndex % reactions.length];
@@ -4964,6 +5008,7 @@ class PageBreather {
4964
5008
  this._targetIntensity = 0;
4965
5009
  this._rafId = 0;
4966
5010
  this._overlay = null;
5011
+ this._label = null;
4967
5012
  this._startTime = 0;
4968
5013
  this._isUserMoving = false;
4969
5014
  this._inSync = false;
@@ -4979,9 +5024,16 @@ class PageBreather {
4979
5024
  const shouldMove = phase < 0.5;
4980
5025
  this._inSync = this._accuracy > 0 && shouldMove === this._isUserMoving;
4981
5026
  const baseOpacity = wave * 0.85;
4982
- const syncBonus = this._inSync ? 0.15 : 0;
4983
- const opacity = this._intensity * (baseOpacity + (1 - wave) * 0.05 + syncBonus);
4984
- this._overlay.style.opacity = String(Math.min(1, opacity));
5027
+ const syncBonus = this._inSync ? 0.22 : 0;
5028
+ const opacity = this._intensity * (0.18 + baseOpacity + (1 - wave) * 0.12 + syncBonus);
5029
+ this._overlay.style.opacity = String(Math.min(0.9, opacity));
5030
+ this._overlay.style.transform = `scale(${1 + wave * 0.015})`;
5031
+ if (this._label) {
5032
+ this._label.textContent = shouldMove ? "In" : "Out";
5033
+ this._label.style.opacity = shouldMove === this._isUserMoving ? "0.98" : "0.82";
5034
+ this._label.style.boxShadow = this._inSync ? "0 0 28px rgba(212,165,80,0.4)" : "0 0 20px rgba(255,255,255,0.08)";
5035
+ this._label.style.borderColor = this._inSync ? "rgba(212,165,80,0.55)" : "rgba(255,255,255,0.12)";
5036
+ }
4985
5037
  };
4986
5038
  }
4987
5039
  start() {
@@ -4998,31 +5050,56 @@ class PageBreather {
4998
5050
  "pointer-events:none",
4999
5051
  "z-index:999980",
5000
5052
  "opacity:0",
5001
- "background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.7) 100%)",
5053
+ "background:radial-gradient(ellipse at center, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.02) 24%, rgba(8,12,22,0.18) 46%, rgba(4,6,12,0.82) 100%)",
5002
5054
  "transition:opacity 0.3s ease"
5003
5055
  ].join(";");
5004
5056
  document.body.appendChild(el);
5005
5057
  this._overlay = el;
5058
+ const label = document.createElement("div");
5059
+ label.style.cssText = [
5060
+ "position:fixed",
5061
+ "left:50%",
5062
+ "top:18%",
5063
+ "transform:translateX(-50%)",
5064
+ "pointer-events:none",
5065
+ "z-index:999981",
5066
+ "padding:8px 14px",
5067
+ "border-radius:999px",
5068
+ "font-family:Georgia, Times New Roman, serif",
5069
+ "font-size:12px",
5070
+ "letter-spacing:0.12em",
5071
+ "text-transform:uppercase",
5072
+ "color:rgba(255,248,232,0.95)",
5073
+ "background:rgba(8,10,18,0.36)",
5074
+ "border:1px solid rgba(255,255,255,0.12)",
5075
+ "box-shadow:0 0 20px rgba(255,255,255,0.08)",
5076
+ "opacity:0.78"
5077
+ ].join(";");
5078
+ label.textContent = "In";
5079
+ document.body.appendChild(label);
5080
+ this._label = label;
5006
5081
  this._tick();
5007
5082
  }
5008
5083
  update(accuracy, isMoving) {
5009
5084
  if (!this._active) return;
5010
5085
  this._isUserMoving = isMoving;
5011
5086
  this._accuracy = accuracy;
5012
- const raw = Math.max(0, Math.min(1, (accuracy - 0.05) / 0.5));
5013
- this._targetIntensity = 0.5 + raw * 0.5;
5087
+ const raw = Math.max(0, Math.min(1, (accuracy - 0.02) / 0.45));
5088
+ this._targetIntensity = 0.68 + raw * 0.32;
5014
5089
  }
5015
5090
  /** Whether the user is currently in sync with the breathing rhythm. */
5016
5091
  isInSync() {
5017
5092
  return this._inSync;
5018
5093
  }
5019
5094
  stop() {
5020
- var _a2;
5095
+ var _a2, _b2;
5021
5096
  if (!this._active) return;
5022
5097
  this._active = false;
5023
5098
  cancelAnimationFrame(this._rafId);
5024
5099
  (_a2 = this._overlay) == null ? void 0 : _a2.remove();
5025
5100
  this._overlay = null;
5101
+ (_b2 = this._label) == null ? void 0 : _b2.remove();
5102
+ this._label = null;
5026
5103
  this._intensity = 0;
5027
5104
  this._targetIntensity = 0;
5028
5105
  this._inSync = false;
@@ -5116,7 +5193,7 @@ class GameController {
5116
5193
  });
5117
5194
  this.bus.on("narrative:clear", () => {
5118
5195
  var _a3;
5119
- return (_a3 = this.narrative) == null ? void 0 : _a3.clear();
5196
+ return (_a3 = this.narrative) == null ? void 0 : _a3.clear(true);
5120
5197
  });
5121
5198
  this.bus.on("egg:reveal", (index) => this._handleEggReveal(index));
5122
5199
  this.bus.on("stage:progress", (p) => this._handleStageProgress(p));
@@ -5128,8 +5205,11 @@ class GameController {
5128
5205
  var _a3, _b3;
5129
5206
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.setBreathIntensity(data.accuracy);
5130
5207
  (_b3 = this.pageBreather) == null ? void 0 : _b3.update(data.accuracy, data.isMoving);
5131
- if (data.accuracy > 0 && this.threeRenderer) {
5132
- this.threeRenderer.showProgressParticles(0.6 + data.accuracy * 0.4);
5208
+ const rhythmSignal = Math.max(data.accuracy, data.progress * 0.75);
5209
+ const hasLiveCycleSignal = data.moveDuration > 700 || data.stillDuration > 700;
5210
+ if ((rhythmSignal > 0.08 || hasLiveCycleSignal) && this.threeRenderer) {
5211
+ const liveSignal = hasLiveCycleSignal ? 0.2 : 0;
5212
+ this.threeRenderer.showProgressParticles(Math.max(0.35, 0.45 + Math.max(rhythmSignal, liveSignal) * 0.55), "rhythm");
5133
5213
  } else if (this.threeRenderer) {
5134
5214
  this.threeRenderer.fadeOutAmbientParticles();
5135
5215
  }
@@ -5493,7 +5573,7 @@ class GameController {
5493
5573
  (_d = this.narrative) == null ? void 0 : _d.showCelebration(congrats[eggIndex] ?? "egg collected");
5494
5574
  await this._wait(3500);
5495
5575
  (_e = this.narrative) == null ? void 0 : _e.clear();
5496
- await this._wait(400);
5576
+ await this._wait(900);
5497
5577
  if (eggIndex < 2) {
5498
5578
  if (eggIndex === 0) {
5499
5579
  this.fsm.transitionTo("stage2-intro");