easter-egg-quest 1.0.18 → 1.0.19

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,24 @@ 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",
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",
68
+ "move for a few seconds",
69
+ "then hold still for a moment",
70
+ "don’t rush it",
71
+ "alternate move and pause",
80
72
  "in",
81
73
  "out"
82
74
  ],
83
75
  stage3Success: [
84
76
  "rhythm was the answer",
85
- "you found the third egg",
86
- "nice work"
77
+ "you found the third egg"
87
78
  ],
88
79
  // ── Finale ────────────────────────────────────────────────────────────
89
80
  finale: [
@@ -331,8 +322,10 @@ class InputTracker {
331
322
  this._prevX = 0;
332
323
  this._prevY = 0;
333
324
  this._prevMoveTs = 0;
334
- this.STILL_THRESHOLD_MS = 400;
335
- this.MIN_PHASE_MS = 300;
325
+ this.DEFAULT_STILL_THRESHOLD_MS = 400;
326
+ this._stillThresholdMs = this.DEFAULT_STILL_THRESHOLD_MS;
327
+ this.DEFAULT_MIN_PHASE_MS = 300;
328
+ this._minPhaseMs = this.DEFAULT_MIN_PHASE_MS;
336
329
  this._stillTimer = null;
337
330
  this._handlers = [];
338
331
  this._active = false;
@@ -471,6 +464,18 @@ class InputTracker {
471
464
  this._snapshot.totalDistance = 0;
472
465
  this._snapshot.maxVelocity = 0;
473
466
  }
467
+ configurePhaseDetection(opts) {
468
+ if (typeof opts.stillThresholdMs === "number") {
469
+ this._stillThresholdMs = Math.max(250, opts.stillThresholdMs);
470
+ }
471
+ if (typeof opts.minPhaseMs === "number") {
472
+ this._minPhaseMs = Math.max(150, opts.minPhaseMs);
473
+ }
474
+ }
475
+ resetPhaseDetection() {
476
+ this._stillThresholdMs = this.DEFAULT_STILL_THRESHOLD_MS;
477
+ this._minPhaseMs = this.DEFAULT_MIN_PHASE_MS;
478
+ }
474
479
  // ─── Movement / Stillness transition logic ─────────────────────────────
475
480
  _registerMove(now) {
476
481
  this._snapshot.lastMoveTime = now;
@@ -483,7 +488,7 @@ class InputTracker {
483
488
  if (this._stillTimer) clearTimeout(this._stillTimer);
484
489
  this._stillTimer = setTimeout(() => {
485
490
  this._becomeStill();
486
- }, this.STILL_THRESHOLD_MS);
491
+ }, this._stillThresholdMs);
487
492
  }
488
493
  _becomeStill() {
489
494
  if (!this._snapshot.isMoving) return;
@@ -497,7 +502,7 @@ class InputTracker {
497
502
  }
498
503
  _completePhase(type, endTime) {
499
504
  const duration = endTime - this._currentPhaseStart;
500
- if (duration >= this.MIN_PHASE_MS) {
505
+ if (duration >= this._minPhaseMs) {
501
506
  this._phases.push({
502
507
  type,
503
508
  startTime: this._currentPhaseStart,
@@ -1312,6 +1317,7 @@ class NarrativeRenderer {
1312
1317
  this.container = null;
1313
1318
  this.currentLine = null;
1314
1319
  this.clearTimer = null;
1320
+ this.awaitingConfirm = false;
1315
1321
  this.config = config;
1316
1322
  }
1317
1323
  mount() {
@@ -1335,14 +1341,22 @@ class NarrativeRenderer {
1335
1341
  this.container.className = "eeq-narrative";
1336
1342
  this.shadow.appendChild(this.container);
1337
1343
  }
1344
+ _removeCurrentLine(immediate = false) {
1345
+ if (!this.currentLine) return;
1346
+ const prev = this.currentLine;
1347
+ this.currentLine = null;
1348
+ if (immediate) {
1349
+ prev.remove();
1350
+ return;
1351
+ }
1352
+ prev.classList.add("eeq-narrative-exit");
1353
+ setTimeout(() => prev.remove(), 750);
1354
+ }
1338
1355
  /** Show a single narrative line. Fades in, replaces any previous line. */
1339
1356
  showLine(text, className = "eeq-line") {
1340
1357
  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
- }
1358
+ if (this.awaitingConfirm) return;
1359
+ this._removeCurrentLine(true);
1346
1360
  if (this.clearTimer) {
1347
1361
  clearTimeout(this.clearTimer);
1348
1362
  this.clearTimer = null;
@@ -1371,11 +1385,8 @@ class NarrativeRenderer {
1371
1385
  resolve();
1372
1386
  return;
1373
1387
  }
1374
- if (this.currentLine) {
1375
- const prev = this.currentLine;
1376
- prev.classList.add("eeq-narrative-exit");
1377
- setTimeout(() => prev.remove(), 750);
1378
- }
1388
+ this.awaitingConfirm = true;
1389
+ this._removeCurrentLine(true);
1379
1390
  if (this.clearTimer) {
1380
1391
  clearTimeout(this.clearTimer);
1381
1392
  this.clearTimer = null;
@@ -1387,7 +1398,10 @@ class NarrativeRenderer {
1387
1398
  const btn = document.createElement("button");
1388
1399
  btn.className = "eeq-confirm-btn";
1389
1400
  btn.textContent = "OK";
1390
- btn.addEventListener("click", () => resolve(), { once: true });
1401
+ btn.addEventListener("click", () => {
1402
+ this.awaitingConfirm = false;
1403
+ resolve();
1404
+ }, { once: true });
1391
1405
  line.appendChild(btn);
1392
1406
  requestAnimationFrame(() => {
1393
1407
  requestAnimationFrame(() => {
@@ -1409,22 +1423,19 @@ class NarrativeRenderer {
1409
1423
  this.showLine(text, "eeq-line eeq-celebration");
1410
1424
  }
1411
1425
  /** Clear current narrative text. */
1412
- clear() {
1426
+ clear(force = false) {
1427
+ if (this.awaitingConfirm && !force) return;
1413
1428
  if (this.clearTimer) {
1414
1429
  clearTimeout(this.clearTimer);
1415
1430
  this.clearTimer = null;
1416
1431
  }
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
- }
1432
+ this._removeCurrentLine(force);
1433
+ this.awaitingConfirm = false;
1423
1434
  }
1424
1435
  /** Remove all DOM. */
1425
1436
  destroy() {
1426
1437
  var _a2;
1427
- this.clear();
1438
+ this.clear(true);
1428
1439
  (_a2 = this.host) == null ? void 0 : _a2.remove();
1429
1440
  this.host = null;
1430
1441
  this.shadow = null;
@@ -4569,6 +4580,8 @@ class StillnessStage {
4569
4580
  this._attempts = 1;
4570
4581
  this.input.resetPhases();
4571
4582
  this.input.resetCounts();
4583
+ this.bus.emit("narrative:clear");
4584
+ await this._wait(850);
4572
4585
  this._introPlayed = false;
4573
4586
  const introLines = this.script.stage1Intro;
4574
4587
  for (let i = 0; i < introLines.length; i++) {
@@ -4663,6 +4676,8 @@ class MotionStage {
4663
4676
  this._attempts = 1;
4664
4677
  this.input.resetPhases();
4665
4678
  this.input.resetCounts();
4679
+ this.bus.emit("narrative:clear");
4680
+ await this._wait(850);
4666
4681
  this._introPlayed = false;
4667
4682
  const introLines = this.script.stage2Intro;
4668
4683
  for (let i = 0; i < introLines.length; i++) {
@@ -4762,6 +4777,7 @@ class RhythmStage {
4762
4777
  this._totalCycles = 0;
4763
4778
  this._lastPhaseCount = 0;
4764
4779
  this._bestAccuracy = 0;
4780
+ this._lastGuideTime = 0;
4765
4781
  this.IDEAL_MOVE_MIN = 1800;
4766
4782
  this.IDEAL_MOVE_MAX = 5e3;
4767
4783
  this.IDEAL_PAUSE_MIN = 1200;
@@ -4776,8 +4792,11 @@ class RhythmStage {
4776
4792
  this._attempts = 1;
4777
4793
  this._goodCycles = 0;
4778
4794
  this._totalCycles = 0;
4795
+ this.input.configurePhaseDetection({ stillThresholdMs: 900, minPhaseMs: 500 });
4779
4796
  this.input.resetPhases();
4780
4797
  this.input.resetCounts();
4798
+ this.bus.emit("narrative:clear");
4799
+ await this._wait(850);
4781
4800
  this._introPlayed = false;
4782
4801
  const introLines = this.script.stage3Intro;
4783
4802
  for (let i = 0; i < introLines.length; i++) {
@@ -4793,6 +4812,8 @@ class RhythmStage {
4793
4812
  this.input.resetPhases();
4794
4813
  this._lastPhaseCount = this.input.phases.length;
4795
4814
  this._introPlayed = true;
4815
+ this.bus.emit("narrative:show", "move for a few seconds, then hold still");
4816
+ this._lastGuideTime = Date.now();
4796
4817
  }
4797
4818
  update(_dt) {
4798
4819
  if (this._status === "complete" || !this._introPlayed) return this._status;
@@ -4819,7 +4840,17 @@ class RhythmStage {
4819
4840
  if (accuracy > this._bestAccuracy) this._bestAccuracy = accuracy;
4820
4841
  const progress = Math.min(1, this._goodCycles / requiredCycles);
4821
4842
  this.bus.emit("stage:progress", progress);
4822
- this.bus.emit("rhythm:breathe", { accuracy, isMoving: this.input.snapshot.isMoving });
4843
+ this.bus.emit("rhythm:breathe", {
4844
+ accuracy,
4845
+ isMoving: this.input.snapshot.isMoving,
4846
+ progress,
4847
+ moveDuration: this.input.moveDuration,
4848
+ stillDuration: this.input.stillDuration
4849
+ });
4850
+ if (accuracy === 0 && Date.now() - this._lastGuideTime > 12e3) {
4851
+ this._lastGuideTime = Date.now();
4852
+ this.bus.emit("narrative:show", "try this: move 2-4s, then stay still 1-3s");
4853
+ }
4823
4854
  if (this._goodCycles >= requiredCycles) {
4824
4855
  this._status = "complete";
4825
4856
  this.bus.emit("stage:progress", 1);
@@ -4829,6 +4860,7 @@ class RhythmStage {
4829
4860
  return this._status;
4830
4861
  }
4831
4862
  cleanup() {
4863
+ this.input.resetPhaseDetection();
4832
4864
  }
4833
4865
  getResult() {
4834
4866
  return {
@@ -4848,7 +4880,7 @@ class RhythmStage {
4848
4880
  }
4849
4881
  _showReaction() {
4850
4882
  const now = Date.now();
4851
- if (now - this._lastNarrativeTime > 6e4) {
4883
+ if (now - this._lastNarrativeTime > 12e3) {
4852
4884
  this._lastNarrativeTime = now;
4853
4885
  const reactions = this.script.stage3Reactions;
4854
4886
  const line = reactions[this._narrativeIndex % reactions.length];
@@ -4964,6 +4996,7 @@ class PageBreather {
4964
4996
  this._targetIntensity = 0;
4965
4997
  this._rafId = 0;
4966
4998
  this._overlay = null;
4999
+ this._label = null;
4967
5000
  this._startTime = 0;
4968
5001
  this._isUserMoving = false;
4969
5002
  this._inSync = false;
@@ -4979,9 +5012,16 @@ class PageBreather {
4979
5012
  const shouldMove = phase < 0.5;
4980
5013
  this._inSync = this._accuracy > 0 && shouldMove === this._isUserMoving;
4981
5014
  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));
5015
+ const syncBonus = this._inSync ? 0.22 : 0;
5016
+ const opacity = this._intensity * (0.18 + baseOpacity + (1 - wave) * 0.12 + syncBonus);
5017
+ this._overlay.style.opacity = String(Math.min(0.9, opacity));
5018
+ this._overlay.style.transform = `scale(${1 + wave * 0.015})`;
5019
+ if (this._label) {
5020
+ this._label.textContent = shouldMove ? "Move" : "Hold still";
5021
+ this._label.style.opacity = shouldMove === this._isUserMoving ? "0.98" : "0.82";
5022
+ this._label.style.boxShadow = this._inSync ? "0 0 28px rgba(212,165,80,0.4)" : "0 0 20px rgba(255,255,255,0.08)";
5023
+ this._label.style.borderColor = this._inSync ? "rgba(212,165,80,0.55)" : "rgba(255,255,255,0.12)";
5024
+ }
4985
5025
  };
4986
5026
  }
4987
5027
  start() {
@@ -4998,31 +5038,56 @@ class PageBreather {
4998
5038
  "pointer-events:none",
4999
5039
  "z-index:999980",
5000
5040
  "opacity:0",
5001
- "background:radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.7) 100%)",
5041
+ "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
5042
  "transition:opacity 0.3s ease"
5003
5043
  ].join(";");
5004
5044
  document.body.appendChild(el);
5005
5045
  this._overlay = el;
5046
+ const label = document.createElement("div");
5047
+ label.style.cssText = [
5048
+ "position:fixed",
5049
+ "left:50%",
5050
+ "top:18%",
5051
+ "transform:translateX(-50%)",
5052
+ "pointer-events:none",
5053
+ "z-index:999981",
5054
+ "padding:8px 14px",
5055
+ "border-radius:999px",
5056
+ "font-family:Georgia, Times New Roman, serif",
5057
+ "font-size:12px",
5058
+ "letter-spacing:0.12em",
5059
+ "text-transform:uppercase",
5060
+ "color:rgba(255,248,232,0.95)",
5061
+ "background:rgba(8,10,18,0.36)",
5062
+ "border:1px solid rgba(255,255,255,0.12)",
5063
+ "box-shadow:0 0 20px rgba(255,255,255,0.08)",
5064
+ "opacity:0.78"
5065
+ ].join(";");
5066
+ label.textContent = "Move";
5067
+ document.body.appendChild(label);
5068
+ this._label = label;
5006
5069
  this._tick();
5007
5070
  }
5008
5071
  update(accuracy, isMoving) {
5009
5072
  if (!this._active) return;
5010
5073
  this._isUserMoving = isMoving;
5011
5074
  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;
5075
+ const raw = Math.max(0, Math.min(1, (accuracy - 0.02) / 0.45));
5076
+ this._targetIntensity = 0.68 + raw * 0.32;
5014
5077
  }
5015
5078
  /** Whether the user is currently in sync with the breathing rhythm. */
5016
5079
  isInSync() {
5017
5080
  return this._inSync;
5018
5081
  }
5019
5082
  stop() {
5020
- var _a2;
5083
+ var _a2, _b2;
5021
5084
  if (!this._active) return;
5022
5085
  this._active = false;
5023
5086
  cancelAnimationFrame(this._rafId);
5024
5087
  (_a2 = this._overlay) == null ? void 0 : _a2.remove();
5025
5088
  this._overlay = null;
5089
+ (_b2 = this._label) == null ? void 0 : _b2.remove();
5090
+ this._label = null;
5026
5091
  this._intensity = 0;
5027
5092
  this._targetIntensity = 0;
5028
5093
  this._inSync = false;
@@ -5116,7 +5181,7 @@ class GameController {
5116
5181
  });
5117
5182
  this.bus.on("narrative:clear", () => {
5118
5183
  var _a3;
5119
- return (_a3 = this.narrative) == null ? void 0 : _a3.clear();
5184
+ return (_a3 = this.narrative) == null ? void 0 : _a3.clear(true);
5120
5185
  });
5121
5186
  this.bus.on("egg:reveal", (index) => this._handleEggReveal(index));
5122
5187
  this.bus.on("stage:progress", (p) => this._handleStageProgress(p));
@@ -5128,8 +5193,10 @@ class GameController {
5128
5193
  var _a3, _b3;
5129
5194
  (_a3 = this.eggRenderer) == null ? void 0 : _a3.setBreathIntensity(data.accuracy);
5130
5195
  (_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);
5196
+ const rhythmSignal = Math.max(data.accuracy, data.progress * 0.75);
5197
+ const hasLiveCycleSignal = data.moveDuration > 1200 || data.stillDuration > 900;
5198
+ if ((rhythmSignal > 0.08 || hasLiveCycleSignal) && this.threeRenderer) {
5199
+ this.threeRenderer.showProgressParticles(0.45 + rhythmSignal * 0.55);
5133
5200
  } else if (this.threeRenderer) {
5134
5201
  this.threeRenderer.fadeOutAmbientParticles();
5135
5202
  }
@@ -5493,7 +5560,7 @@ class GameController {
5493
5560
  (_d = this.narrative) == null ? void 0 : _d.showCelebration(congrats[eggIndex] ?? "egg collected");
5494
5561
  await this._wait(3500);
5495
5562
  (_e = this.narrative) == null ? void 0 : _e.clear();
5496
- await this._wait(400);
5563
+ await this._wait(900);
5497
5564
  if (eggIndex < 2) {
5498
5565
  if (eggIndex === 0) {
5499
5566
  this.fsm.transitionTo("stage2-intro");