Spaces:
Sleeping
Sleeping
feat(gameplay): implement progressive difficulty mechanics and peek cooldown
Browse files
app.py
CHANGED
|
@@ -7779,6 +7779,8 @@ class MatchWiseApp {
|
|
| 7779 |
this.previewTimer = null;
|
| 7780 |
this.previewShuffleTimer = null;
|
| 7781 |
this.previewShuffleAnimationMs = 520;
|
|
|
|
|
|
|
| 7782 |
this.previewCount = PREVIEW_SECONDS;
|
| 7783 |
this.pendingTransition = false;
|
| 7784 |
this.levelStartTime = null;
|
|
@@ -7793,6 +7795,8 @@ class MatchWiseApp {
|
|
| 7793 |
this.wrongMatchTimer = null;
|
| 7794 |
this.distractorSparkleTimer = null;
|
| 7795 |
this.distractorSparkleClearTimer = null;
|
|
|
|
|
|
|
| 7796 |
this.challengeStarted = false;
|
| 7797 |
this.challengeSelectionLocked = false;
|
| 7798 |
this.challengeResultIndex = null;
|
|
@@ -7948,12 +7952,77 @@ class MatchWiseApp {
|
|
| 7948 |
z-index: 4;
|
| 7949 |
}
|
| 7950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7951 |
@keyframes distractorSparkleRuntime {
|
| 7952 |
0% { transform: scale(0.35) rotate(-18deg); opacity: 0; }
|
| 7953 |
28% { transform: scale(1.18) rotate(10deg); opacity: 1; }
|
| 7954 |
100% { transform: scale(0.65) rotate(24deg); opacity: 0; }
|
| 7955 |
}
|
| 7956 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7957 |
@keyframes popBounceRuntime {
|
| 7958 |
0% {
|
| 7959 |
transform: translate(-50%, -50%) scale(0) rotate(-15deg);
|
|
@@ -8776,6 +8845,45 @@ class MatchWiseApp {
|
|
| 8776 |
return effectiveMaxHints;
|
| 8777 |
}
|
| 8778 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8779 |
getDistractorSparkleIntervalMs() {
|
| 8780 |
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8781 |
|
|
@@ -8788,7 +8896,15 @@ class MatchWiseApp {
|
|
| 8788 |
return;
|
| 8789 |
}
|
| 8790 |
|
| 8791 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8792 |
if (!candidates.length) return;
|
| 8793 |
|
| 8794 |
const card = candidates[Math.floor(Math.random() * candidates.length)];
|
|
@@ -8858,6 +8974,7 @@ class MatchWiseApp {
|
|
| 8858 |
this.hints <= 0 ||
|
| 8859 |
this.state.status !== 'playing' ||
|
| 8860 |
this.hinting ||
|
|
|
|
| 8861 |
oddFaceUp ||
|
| 8862 |
this.state.game_over ||
|
| 8863 |
this.state.level_complete;
|
|
@@ -9528,6 +9645,99 @@ class MatchWiseApp {
|
|
| 9528 |
});
|
| 9529 |
}
|
| 9530 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9531 |
startPreviewGridShuffle() {
|
| 9532 |
this.stopPreviewGridShuffle();
|
| 9533 |
|
|
@@ -9554,6 +9764,8 @@ class MatchWiseApp {
|
|
| 9554 |
this.previewShuffleTimer = null;
|
| 9555 |
}
|
| 9556 |
|
|
|
|
|
|
|
| 9557 |
if (!this.grid) return;
|
| 9558 |
|
| 9559 |
this.grid.querySelectorAll('.memory-card').forEach((card) => {
|
|
@@ -9609,6 +9821,7 @@ class MatchWiseApp {
|
|
| 9609 |
});
|
| 9610 |
cardEl.style.transform = '';
|
| 9611 |
cardEl.style.zIndex = '';
|
|
|
|
| 9612 |
});
|
| 9613 |
}
|
| 9614 |
|
|
@@ -9660,6 +9873,8 @@ class MatchWiseApp {
|
|
| 9660 |
this.state.status = 'preview';
|
| 9661 |
this.state.level_complete = false;
|
| 9662 |
this.state.game_over = false;
|
|
|
|
|
|
|
| 9663 |
this.syncProgressiveHintLimit(true);
|
| 9664 |
this.applyStateStyle();
|
| 9665 |
this.setStatus(
|
|
@@ -9688,6 +9903,7 @@ class MatchWiseApp {
|
|
| 9688 |
|
| 9689 |
setTimeout(() => {
|
| 9690 |
this.startPreviewGridShuffle();
|
|
|
|
| 9691 |
}, 260);
|
| 9692 |
this.updateHintsDisplay();
|
| 9693 |
|
|
@@ -9721,6 +9937,8 @@ class MatchWiseApp {
|
|
| 9721 |
this.state.status = 'preview';
|
| 9722 |
this.state.level_complete = false;
|
| 9723 |
this.state.game_over = false;
|
|
|
|
|
|
|
| 9724 |
this.syncProgressiveHintLimit(true);
|
| 9725 |
|
| 9726 |
if (this.overlay) {
|
|
@@ -9752,6 +9970,7 @@ class MatchWiseApp {
|
|
| 9752 |
|
| 9753 |
setTimeout(() => {
|
| 9754 |
this.startPreviewGridShuffle();
|
|
|
|
| 9755 |
}, 260);
|
| 9756 |
|
| 9757 |
this.previewTimer = setInterval(() => {
|
|
@@ -9938,6 +10157,7 @@ class MatchWiseApp {
|
|
| 9938 |
|
| 9939 |
card.classList.add('flipped');
|
| 9940 |
this.flipped.push(idx);
|
|
|
|
| 9941 |
|
| 9942 |
if (this.flipped.length === 1) {
|
| 9943 |
this.updateHintsDisplay();
|
|
@@ -9952,6 +10172,7 @@ class MatchWiseApp {
|
|
| 9952 |
this.locked = true;
|
| 9953 |
this.moves += 1;
|
| 9954 |
this.state.moves = this.moves;
|
|
|
|
| 9955 |
this.updateHud();
|
| 9956 |
|
| 9957 |
const [a, b] = this.flipped;
|
|
@@ -10071,6 +10292,7 @@ class MatchWiseApp {
|
|
| 10071 |
this.resetComboFeedback();
|
| 10072 |
this.clearWrongMatchTimer();
|
| 10073 |
this.locked = true;
|
|
|
|
| 10074 |
this.wrongMatchTimer = window.setTimeout(() => {
|
| 10075 |
try {
|
| 10076 |
[a, b].forEach((index) => {
|
|
@@ -10087,7 +10309,7 @@ class MatchWiseApp {
|
|
| 10087 |
this.gameOver();
|
| 10088 |
}
|
| 10089 |
}
|
| 10090 |
-
},
|
| 10091 |
|
| 10092 |
try {
|
| 10093 |
this.showFeedbackOverlay('WRONG MATCH', 'Lost 1 live', 'negative', false);
|
|
@@ -10121,6 +10343,11 @@ class MatchWiseApp {
|
|
| 10121 |
if (this.state.status !== 'playing') return;
|
| 10122 |
this.syncProgressiveHintLimit(false);
|
| 10123 |
if (this.hinting) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10124 |
if (this.hints <= 0) {
|
| 10125 |
this.setStatus('No peeks left!', 'error', '');
|
| 10126 |
this.updateHintsDisplay();
|
|
@@ -10163,6 +10390,8 @@ class MatchWiseApp {
|
|
| 10163 |
|
| 10164 |
this.hinting = false;
|
| 10165 |
this.locked = false;
|
|
|
|
|
|
|
| 10166 |
this.updateHintsDisplay();
|
| 10167 |
await this.syncBackendEvent('hint');
|
| 10168 |
}, 1200);
|
|
|
|
| 7779 |
this.previewTimer = null;
|
| 7780 |
this.previewShuffleTimer = null;
|
| 7781 |
this.previewShuffleAnimationMs = 520;
|
| 7782 |
+
this.previewFogTimers = [];
|
| 7783 |
+
this.previewHideTimers = [];
|
| 7784 |
this.previewCount = PREVIEW_SECONDS;
|
| 7785 |
this.pendingTransition = false;
|
| 7786 |
this.levelStartTime = null;
|
|
|
|
| 7795 |
this.wrongMatchTimer = null;
|
| 7796 |
this.distractorSparkleTimer = null;
|
| 7797 |
this.distractorSparkleClearTimer = null;
|
| 7798 |
+
this.recentlyFlippedCardIndexes = [];
|
| 7799 |
+
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
|
| 7800 |
this.challengeStarted = false;
|
| 7801 |
this.challengeSelectionLocked = false;
|
| 7802 |
this.challengeResultIndex = null;
|
|
|
|
| 7952 |
z-index: 4;
|
| 7953 |
}
|
| 7954 |
|
| 7955 |
+
.memory-card.distractor-sparkle .memory-back::after {
|
| 7956 |
+
content: '✦';
|
| 7957 |
+
position: absolute;
|
| 7958 |
+
left: 9%;
|
| 7959 |
+
top: 8%;
|
| 7960 |
+
width: 28%;
|
| 7961 |
+
height: 28%;
|
| 7962 |
+
display: flex;
|
| 7963 |
+
align-items: center;
|
| 7964 |
+
justify-content: center;
|
| 7965 |
+
border-radius: 999px;
|
| 7966 |
+
color: rgba(255, 255, 255, 0.95);
|
| 7967 |
+
background: radial-gradient(circle, rgba(255,255,255,0.7), rgba(255,255,255,0.1) 58%, transparent 72%);
|
| 7968 |
+
filter: drop-shadow(0 0 10px rgba(255,255,255,0.75));
|
| 7969 |
+
font-size: clamp(14px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.16), 26px);
|
| 7970 |
+
pointer-events: none;
|
| 7971 |
+
animation: distractorSparkleRuntime 680ms ease-out forwards;
|
| 7972 |
+
z-index: 4;
|
| 7973 |
+
}
|
| 7974 |
+
|
| 7975 |
+
.memory-container.preview-fog-blink .memory-card.preview-locked .memory-back,
|
| 7976 |
+
.matchwise-root.preview-fog-blink .memory-card.preview-locked .memory-back {
|
| 7977 |
+
animation: previewFogBlinkRuntime 260ms ease-in-out forwards;
|
| 7978 |
+
}
|
| 7979 |
+
|
| 7980 |
+
.memory-card.preview-hidden-card .memory-back {
|
| 7981 |
+
animation: previewCardHideRuntime 620ms ease-in-out forwards;
|
| 7982 |
+
}
|
| 7983 |
+
|
| 7984 |
+
.memory-card.preview-hidden-card::after {
|
| 7985 |
+
content: '☁️';
|
| 7986 |
+
position: absolute;
|
| 7987 |
+
inset: 10%;
|
| 7988 |
+
display: flex;
|
| 7989 |
+
align-items: center;
|
| 7990 |
+
justify-content: center;
|
| 7991 |
+
border-radius: 24px;
|
| 7992 |
+
font-size: clamp(24px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.28), 46px);
|
| 7993 |
+
background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(255,255,255,0.62) 55%, rgba(255,255,255,0.14));
|
| 7994 |
+
filter: drop-shadow(0 10px 18px rgba(132, 90, 255, 0.2));
|
| 7995 |
+
pointer-events: none;
|
| 7996 |
+
z-index: 8;
|
| 7997 |
+
animation: previewCloudCoverRuntime 620ms ease-in-out forwards;
|
| 7998 |
+
}
|
| 7999 |
+
|
| 8000 |
@keyframes distractorSparkleRuntime {
|
| 8001 |
0% { transform: scale(0.35) rotate(-18deg); opacity: 0; }
|
| 8002 |
28% { transform: scale(1.18) rotate(10deg); opacity: 1; }
|
| 8003 |
100% { transform: scale(0.65) rotate(24deg); opacity: 0; }
|
| 8004 |
}
|
| 8005 |
|
| 8006 |
+
@keyframes previewFogBlinkRuntime {
|
| 8007 |
+
0% { filter: blur(0px) brightness(1); opacity: 1; }
|
| 8008 |
+
45% { filter: blur(5px) brightness(0.94); opacity: 0.72; }
|
| 8009 |
+
100% { filter: blur(0px) brightness(1); opacity: 1; }
|
| 8010 |
+
}
|
| 8011 |
+
|
| 8012 |
+
@keyframes previewCardHideRuntime {
|
| 8013 |
+
0% { filter: blur(0px); opacity: 1; }
|
| 8014 |
+
18% { filter: blur(3px); opacity: 0.18; }
|
| 8015 |
+
82% { filter: blur(3px); opacity: 0.18; }
|
| 8016 |
+
100% { filter: blur(0px); opacity: 1; }
|
| 8017 |
+
}
|
| 8018 |
+
|
| 8019 |
+
@keyframes previewCloudCoverRuntime {
|
| 8020 |
+
0% { transform: scale(0.86); opacity: 0; }
|
| 8021 |
+
18% { transform: scale(1); opacity: 1; }
|
| 8022 |
+
82% { transform: scale(1); opacity: 1; }
|
| 8023 |
+
100% { transform: scale(1.06); opacity: 0; }
|
| 8024 |
+
}
|
| 8025 |
+
|
| 8026 |
@keyframes popBounceRuntime {
|
| 8027 |
0% {
|
| 8028 |
transform: translate(-50%, -50%) scale(0) rotate(-15deg);
|
|
|
|
| 8845 |
return effectiveMaxHints;
|
| 8846 |
}
|
| 8847 |
|
| 8848 |
+
getHintCooldownLengthMoves() {
|
| 8849 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8850 |
+
|
| 8851 |
+
// Early levels stay friendly. Later levels make Peek a small decision.
|
| 8852 |
+
if (level < 7) return 0;
|
| 8853 |
+
if (level < 14) return 1;
|
| 8854 |
+
return 2;
|
| 8855 |
+
}
|
| 8856 |
+
|
| 8857 |
+
decrementPeekCooldownAfterMove() {
|
| 8858 |
+
if (this.peekCooldownMoves > 0) {
|
| 8859 |
+
this.peekCooldownMoves = Math.max(0, this.peekCooldownMoves - 1);
|
| 8860 |
+
this.state.peek_cooldown_moves = this.peekCooldownMoves;
|
| 8861 |
+
this.updateHintsDisplay();
|
| 8862 |
+
}
|
| 8863 |
+
}
|
| 8864 |
+
|
| 8865 |
+
getWrongFlipBackDelayMs() {
|
| 8866 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8867 |
+
|
| 8868 |
+
// Level 1-5: 900ms, Level 6-12: 700ms, Level 13+: 550ms.
|
| 8869 |
+
if (level <= 5) return 900;
|
| 8870 |
+
if (level <= 12) return 700;
|
| 8871 |
+
return 550;
|
| 8872 |
+
}
|
| 8873 |
+
|
| 8874 |
+
getCompletedChallengeLevels() {
|
| 8875 |
+
return Math.max(0, Number(this.state.completed_challenge_levels || 0));
|
| 8876 |
+
}
|
| 8877 |
+
|
| 8878 |
+
rememberRecentlyFlipped(index) {
|
| 8879 |
+
const value = Number(index);
|
| 8880 |
+
if (!Number.isFinite(value)) return;
|
| 8881 |
+
this.recentlyFlippedCardIndexes = [
|
| 8882 |
+
value,
|
| 8883 |
+
...this.recentlyFlippedCardIndexes.filter((item) => item !== value),
|
| 8884 |
+
].slice(0, 6);
|
| 8885 |
+
}
|
| 8886 |
+
|
| 8887 |
getDistractorSparkleIntervalMs() {
|
| 8888 |
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8889 |
|
|
|
|
| 8896 |
return;
|
| 8897 |
}
|
| 8898 |
|
| 8899 |
+
const faceDownCandidates = Array.from(this.grid.querySelectorAll('.memory-card:not(.matched):not(.flipped):not(.preview-locked)'));
|
| 8900 |
+
const recentlyFlippedCandidates = this.recentlyFlippedCardIndexes
|
| 8901 |
+
.map((index) => this.grid.querySelector(`[data-index="${index}"]`))
|
| 8902 |
+
.filter((card) => card && !card.classList.contains('matched') && !card.classList.contains('preview-locked'));
|
| 8903 |
+
|
| 8904 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8905 |
+
const canUseRecentDecoy = level >= 8 && recentlyFlippedCandidates.length > 0;
|
| 8906 |
+
const useRecentDecoy = canUseRecentDecoy && Math.random() < 0.34;
|
| 8907 |
+
const candidates = useRecentDecoy ? recentlyFlippedCandidates : faceDownCandidates;
|
| 8908 |
if (!candidates.length) return;
|
| 8909 |
|
| 8910 |
const card = candidates[Math.floor(Math.random() * candidates.length)];
|
|
|
|
| 8974 |
this.hints <= 0 ||
|
| 8975 |
this.state.status !== 'playing' ||
|
| 8976 |
this.hinting ||
|
| 8977 |
+
this.peekCooldownMoves > 0 ||
|
| 8978 |
oddFaceUp ||
|
| 8979 |
this.state.game_over ||
|
| 8980 |
this.state.level_complete;
|
|
|
|
| 9645 |
});
|
| 9646 |
}
|
| 9647 |
|
| 9648 |
+
clearPreviewChallengeEffects() {
|
| 9649 |
+
this.previewFogTimers.forEach((timer) => clearTimeout(timer));
|
| 9650 |
+
this.previewFogTimers = [];
|
| 9651 |
+
this.previewHideTimers.forEach((timer) => clearTimeout(timer));
|
| 9652 |
+
this.previewHideTimers = [];
|
| 9653 |
+
|
| 9654 |
+
const effectRoots = [this.memoryContainer, this.root].filter(Boolean);
|
| 9655 |
+
effectRoots.forEach((root) => root.classList.remove('preview-fog-blink'));
|
| 9656 |
+
|
| 9657 |
+
if (this.grid) {
|
| 9658 |
+
this.grid.querySelectorAll('.memory-card.preview-hidden-card').forEach((card) => {
|
| 9659 |
+
card.classList.remove('preview-hidden-card');
|
| 9660 |
+
});
|
| 9661 |
+
}
|
| 9662 |
+
}
|
| 9663 |
+
|
| 9664 |
+
triggerPreviewFogBlinkOnce() {
|
| 9665 |
+
const effectRoot = this.memoryContainer || this.root;
|
| 9666 |
+
if (!effectRoot || this.state.status !== 'preview') return;
|
| 9667 |
+
|
| 9668 |
+
effectRoot.classList.remove('preview-fog-blink');
|
| 9669 |
+
void effectRoot.offsetWidth;
|
| 9670 |
+
effectRoot.classList.add('preview-fog-blink');
|
| 9671 |
+
|
| 9672 |
+
const timer = setTimeout(() => {
|
| 9673 |
+
effectRoot.classList.remove('preview-fog-blink');
|
| 9674 |
+
}, 280);
|
| 9675 |
+
this.previewFogTimers.push(timer);
|
| 9676 |
+
}
|
| 9677 |
+
|
| 9678 |
+
startPreviewFogChallenge() {
|
| 9679 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 9680 |
+
if (level < 10) return;
|
| 9681 |
+
|
| 9682 |
+
const blinkCount = level >= 18 ? 2 : 1;
|
| 9683 |
+
const delays = blinkCount >= 2 ? [760, 1760] : [1260];
|
| 9684 |
+
delays.slice(0, blinkCount).forEach((delay) => {
|
| 9685 |
+
const timer = setTimeout(() => {
|
| 9686 |
+
if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
|
| 9687 |
+
this.triggerPreviewFogBlinkOnce();
|
| 9688 |
+
}
|
| 9689 |
+
}, delay);
|
| 9690 |
+
this.previewFogTimers.push(timer);
|
| 9691 |
+
});
|
| 9692 |
+
}
|
| 9693 |
+
|
| 9694 |
+
getPreviewHideCardCount() {
|
| 9695 |
+
const completedChallengeLevels = this.getCompletedChallengeLevels();
|
| 9696 |
+
if (completedChallengeLevels < 1) return 0;
|
| 9697 |
+
return completedChallengeLevels >= 3 ? 2 : 1;
|
| 9698 |
+
}
|
| 9699 |
+
|
| 9700 |
+
hideRandomPreviewCardsOnce() {
|
| 9701 |
+
if (!this.grid || this.state.status !== 'preview') return;
|
| 9702 |
+
|
| 9703 |
+
const hideCount = this.getPreviewHideCardCount();
|
| 9704 |
+
if (hideCount <= 0) return;
|
| 9705 |
+
|
| 9706 |
+
const cards = Array.from(this.grid.querySelectorAll('.memory-card.preview-locked:not(.matched):not(.preview-hidden-card)'));
|
| 9707 |
+
if (!cards.length) return;
|
| 9708 |
+
|
| 9709 |
+
const shuffled = this.makeDifferentPreviewOrder(cards).slice(0, Math.min(hideCount, cards.length));
|
| 9710 |
+
shuffled.forEach((card) => {
|
| 9711 |
+
card.classList.add('preview-hidden-card');
|
| 9712 |
+
const timer = setTimeout(() => {
|
| 9713 |
+
card.classList.remove('preview-hidden-card');
|
| 9714 |
+
}, 650);
|
| 9715 |
+
this.previewHideTimers.push(timer);
|
| 9716 |
+
});
|
| 9717 |
+
}
|
| 9718 |
+
|
| 9719 |
+
startPreviewHideChallenge() {
|
| 9720 |
+
if (this.getPreviewHideCardCount() <= 0) return;
|
| 9721 |
+
|
| 9722 |
+
[920, 1880].forEach((delay, index) => {
|
| 9723 |
+
const timer = setTimeout(() => {
|
| 9724 |
+
if (window.__activeMemoryLevelId === this.levelId && this.state.status === 'preview') {
|
| 9725 |
+
// The second pulse only appears once challenge progress is stronger.
|
| 9726 |
+
if (index === 0 || this.getCompletedChallengeLevels() >= 2) {
|
| 9727 |
+
this.hideRandomPreviewCardsOnce();
|
| 9728 |
+
}
|
| 9729 |
+
}
|
| 9730 |
+
}, delay);
|
| 9731 |
+
this.previewHideTimers.push(timer);
|
| 9732 |
+
});
|
| 9733 |
+
}
|
| 9734 |
+
|
| 9735 |
+
startPreviewDifficultyEffects() {
|
| 9736 |
+
this.clearPreviewChallengeEffects();
|
| 9737 |
+
this.startPreviewFogChallenge();
|
| 9738 |
+
this.startPreviewHideChallenge();
|
| 9739 |
+
}
|
| 9740 |
+
|
| 9741 |
startPreviewGridShuffle() {
|
| 9742 |
this.stopPreviewGridShuffle();
|
| 9743 |
|
|
|
|
| 9764 |
this.previewShuffleTimer = null;
|
| 9765 |
}
|
| 9766 |
|
| 9767 |
+
this.clearPreviewChallengeEffects();
|
| 9768 |
+
|
| 9769 |
if (!this.grid) return;
|
| 9770 |
|
| 9771 |
this.grid.querySelectorAll('.memory-card').forEach((card) => {
|
|
|
|
| 9821 |
});
|
| 9822 |
cardEl.style.transform = '';
|
| 9823 |
cardEl.style.zIndex = '';
|
| 9824 |
+
cardEl.classList.remove('preview-hidden-card', 'distractor-sparkle');
|
| 9825 |
});
|
| 9826 |
}
|
| 9827 |
|
|
|
|
| 9873 |
this.state.status = 'preview';
|
| 9874 |
this.state.level_complete = false;
|
| 9875 |
this.state.game_over = false;
|
| 9876 |
+
this.recentlyFlippedCardIndexes = [];
|
| 9877 |
+
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
|
| 9878 |
this.syncProgressiveHintLimit(true);
|
| 9879 |
this.applyStateStyle();
|
| 9880 |
this.setStatus(
|
|
|
|
| 9903 |
|
| 9904 |
setTimeout(() => {
|
| 9905 |
this.startPreviewGridShuffle();
|
| 9906 |
+
this.startPreviewDifficultyEffects();
|
| 9907 |
}, 260);
|
| 9908 |
this.updateHintsDisplay();
|
| 9909 |
|
|
|
|
| 9937 |
this.state.status = 'preview';
|
| 9938 |
this.state.level_complete = false;
|
| 9939 |
this.state.game_over = false;
|
| 9940 |
+
this.recentlyFlippedCardIndexes = [];
|
| 9941 |
+
this.peekCooldownMoves = Number(this.state.peek_cooldown_moves || 0);
|
| 9942 |
this.syncProgressiveHintLimit(true);
|
| 9943 |
|
| 9944 |
if (this.overlay) {
|
|
|
|
| 9970 |
|
| 9971 |
setTimeout(() => {
|
| 9972 |
this.startPreviewGridShuffle();
|
| 9973 |
+
this.startPreviewDifficultyEffects();
|
| 9974 |
}, 260);
|
| 9975 |
|
| 9976 |
this.previewTimer = setInterval(() => {
|
|
|
|
| 10157 |
|
| 10158 |
card.classList.add('flipped');
|
| 10159 |
this.flipped.push(idx);
|
| 10160 |
+
this.rememberRecentlyFlipped(idx);
|
| 10161 |
|
| 10162 |
if (this.flipped.length === 1) {
|
| 10163 |
this.updateHintsDisplay();
|
|
|
|
| 10172 |
this.locked = true;
|
| 10173 |
this.moves += 1;
|
| 10174 |
this.state.moves = this.moves;
|
| 10175 |
+
this.decrementPeekCooldownAfterMove();
|
| 10176 |
this.updateHud();
|
| 10177 |
|
| 10178 |
const [a, b] = this.flipped;
|
|
|
|
| 10292 |
this.resetComboFeedback();
|
| 10293 |
this.clearWrongMatchTimer();
|
| 10294 |
this.locked = true;
|
| 10295 |
+
const wrongFlipBackDelay = this.getWrongFlipBackDelayMs();
|
| 10296 |
this.wrongMatchTimer = window.setTimeout(() => {
|
| 10297 |
try {
|
| 10298 |
[a, b].forEach((index) => {
|
|
|
|
| 10309 |
this.gameOver();
|
| 10310 |
}
|
| 10311 |
}
|
| 10312 |
+
}, wrongFlipBackDelay);
|
| 10313 |
|
| 10314 |
try {
|
| 10315 |
this.showFeedbackOverlay('WRONG MATCH', 'Lost 1 live', 'negative', false);
|
|
|
|
| 10343 |
if (this.state.status !== 'playing') return;
|
| 10344 |
this.syncProgressiveHintLimit(false);
|
| 10345 |
if (this.hinting) return;
|
| 10346 |
+
if (this.peekCooldownMoves > 0) {
|
| 10347 |
+
this.setStatus('Peek is cooling down.', 'error', `${this.peekCooldownMoves} move${this.peekCooldownMoves === 1 ? '' : 's'} left before next peek.`);
|
| 10348 |
+
this.updateHintsDisplay();
|
| 10349 |
+
return;
|
| 10350 |
+
}
|
| 10351 |
if (this.hints <= 0) {
|
| 10352 |
this.setStatus('No peeks left!', 'error', '');
|
| 10353 |
this.updateHintsDisplay();
|
|
|
|
| 10390 |
|
| 10391 |
this.hinting = false;
|
| 10392 |
this.locked = false;
|
| 10393 |
+
this.peekCooldownMoves = this.getHintCooldownLengthMoves();
|
| 10394 |
+
this.state.peek_cooldown_moves = this.peekCooldownMoves;
|
| 10395 |
this.updateHintsDisplay();
|
| 10396 |
await this.syncBackendEvent('hint');
|
| 10397 |
}, 1200);
|