Spaces:
Running
Running
distractor sparkle and progressive peek limit
Browse files
app.py
CHANGED
|
@@ -7791,6 +7791,8 @@ class MatchWiseApp {
|
|
| 7791 |
this.peekGlowTimer = null;
|
| 7792 |
this.livesGlowTimer = null;
|
| 7793 |
this.wrongMatchTimer = null;
|
|
|
|
|
|
|
| 7794 |
this.challengeStarted = false;
|
| 7795 |
this.challengeSelectionLocked = false;
|
| 7796 |
this.challengeResultIndex = null;
|
|
@@ -7818,6 +7820,7 @@ class MatchWiseApp {
|
|
| 7818 |
this.installViewportHandlers();
|
| 7819 |
this.hideResetConfirm();
|
| 7820 |
this.hideGameOverModal();
|
|
|
|
| 7821 |
this.hideHowToPlayModal();
|
| 7822 |
if (this.pageMode === 'start') {
|
| 7823 |
if (this.startNote) {
|
|
@@ -7925,6 +7928,32 @@ class MatchWiseApp {
|
|
| 7925 |
-webkit-text-fill-color: transparent;
|
| 7926 |
}
|
| 7927 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7928 |
@keyframes popBounceRuntime {
|
| 7929 |
0% {
|
| 7930 |
transform: translate(-50%, -50%) scale(0) rotate(-15deg);
|
|
@@ -8720,9 +8749,100 @@ class MatchWiseApp {
|
|
| 8720 |
}, 850);
|
| 8721 |
}
|
| 8722 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8723 |
updateHintsDisplay() {
|
|
|
|
| 8724 |
if (this.hintCountEl) {
|
| 8725 |
-
this.hintCountEl.textContent = `${this.hints} / ${
|
| 8726 |
}
|
| 8727 |
|
| 8728 |
const currentHints = Number(this.hints || 0);
|
|
@@ -9264,6 +9384,7 @@ class MatchWiseApp {
|
|
| 9264 |
this.challengeCorrectIndex = null;
|
| 9265 |
}
|
| 9266 |
|
|
|
|
| 9267 |
this.renderCards();
|
| 9268 |
this.updateHud();
|
| 9269 |
this.updateCardSize();
|
|
@@ -9512,6 +9633,11 @@ class MatchWiseApp {
|
|
| 9512 |
if (this.nextBtn) {
|
| 9513 |
this.nextBtn.disabled = status !== 'complete';
|
| 9514 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9515 |
this.updateHintsDisplay();
|
| 9516 |
this.syncControlsVisibility();
|
| 9517 |
}
|
|
@@ -9534,6 +9660,7 @@ class MatchWiseApp {
|
|
| 9534 |
this.state.status = 'preview';
|
| 9535 |
this.state.level_complete = false;
|
| 9536 |
this.state.game_over = false;
|
|
|
|
| 9537 |
this.applyStateStyle();
|
| 9538 |
this.setStatus(
|
| 9539 |
'Memorize the cards before they flip!',
|
|
@@ -9594,6 +9721,7 @@ class MatchWiseApp {
|
|
| 9594 |
this.state.status = 'preview';
|
| 9595 |
this.state.level_complete = false;
|
| 9596 |
this.state.game_over = false;
|
|
|
|
| 9597 |
|
| 9598 |
if (this.overlay) {
|
| 9599 |
this.overlay.style.display = 'flex';
|
|
@@ -9871,7 +9999,8 @@ class MatchWiseApp {
|
|
| 9871 |
this.state.performance_meter = 0;
|
| 9872 |
this.state.challenge_due = false;
|
| 9873 |
}
|
| 9874 |
-
const
|
|
|
|
| 9875 |
this.showFeedbackOverlay(
|
| 9876 |
'LEVEL COMPLETE',
|
| 9877 |
isChallengeBoard ? 'Level Complete!\n+1 Life!' : (shouldAwardPeek ? 'Level Complete!\n+1 Peek earned!' : 'Level Complete!'),
|
|
@@ -9896,7 +10025,7 @@ class MatchWiseApp {
|
|
| 9896 |
pairFact || featuredFact || this.state.theme || `Bonus ready for level ${this.state.level}.`
|
| 9897 |
);
|
| 9898 |
if (!isChallengeBoard) {
|
| 9899 |
-
this.hints = Math.min(
|
| 9900 |
this.state.hints = this.hints;
|
| 9901 |
}
|
| 9902 |
this.updateHud();
|
|
@@ -9913,7 +10042,7 @@ class MatchWiseApp {
|
|
| 9913 |
}
|
| 9914 |
const hintMessage = shouldAwardPeek
|
| 9915 |
? '+1 peek earned!'
|
| 9916 |
-
: `Max peeks earned: ${this.hints} / ${
|
| 9917 |
const completionDetails = [featuredFact, hintMessage].filter(Boolean).join('\n');
|
| 9918 |
this.setStatus(
|
| 9919 |
this.state.victory_message || 'Level cleared. Click NEXT LEVEL to continue.',
|
|
@@ -9990,6 +10119,7 @@ class MatchWiseApp {
|
|
| 9990 |
|
| 9991 |
async useHint() {
|
| 9992 |
if (this.state.status !== 'playing') return;
|
|
|
|
| 9993 |
if (this.hinting) return;
|
| 9994 |
if (this.hints <= 0) {
|
| 9995 |
this.setStatus('No peeks left!', 'error', '');
|
|
@@ -10072,6 +10202,7 @@ class MatchWiseApp {
|
|
| 10072 |
: Number(this.state.previous_level_time_seconds || 0);
|
| 10073 |
this.state.win_streak = 0;
|
| 10074 |
this.resetComboFeedback();
|
|
|
|
| 10075 |
this.locked = true;
|
| 10076 |
this.applyStateStyle();
|
| 10077 |
this.setStatus(
|
|
@@ -10242,6 +10373,7 @@ class MatchWiseApp {
|
|
| 10242 |
await this.refreshLeaderboardData();
|
| 10243 |
this.hideResetConfirm();
|
| 10244 |
this.hideGameOverModal();
|
|
|
|
| 10245 |
this.showTransitionOverlay('🤖 Starting New Game', 'Resetting the board and preparing a fresh run...');
|
| 10246 |
if (this.nextBtn) this.nextBtn.disabled = true;
|
| 10247 |
|
|
|
|
| 7791 |
this.peekGlowTimer = null;
|
| 7792 |
this.livesGlowTimer = null;
|
| 7793 |
this.wrongMatchTimer = null;
|
| 7794 |
+
this.distractorSparkleTimer = null;
|
| 7795 |
+
this.distractorSparkleClearTimer = null;
|
| 7796 |
this.challengeStarted = false;
|
| 7797 |
this.challengeSelectionLocked = false;
|
| 7798 |
this.challengeResultIndex = null;
|
|
|
|
| 7820 |
this.installViewportHandlers();
|
| 7821 |
this.hideResetConfirm();
|
| 7822 |
this.hideGameOverModal();
|
| 7823 |
+
this.stopDistractorSparkles();
|
| 7824 |
this.hideHowToPlayModal();
|
| 7825 |
if (this.pageMode === 'start') {
|
| 7826 |
if (this.startNote) {
|
|
|
|
| 7928 |
-webkit-text-fill-color: transparent;
|
| 7929 |
}
|
| 7930 |
|
| 7931 |
+
.memory-card.distractor-sparkle .memory-front::after {
|
| 7932 |
+
content: '✦';
|
| 7933 |
+
position: absolute;
|
| 7934 |
+
right: 9%;
|
| 7935 |
+
top: 8%;
|
| 7936 |
+
width: 28%;
|
| 7937 |
+
height: 28%;
|
| 7938 |
+
display: flex;
|
| 7939 |
+
align-items: center;
|
| 7940 |
+
justify-content: center;
|
| 7941 |
+
border-radius: 999px;
|
| 7942 |
+
color: rgba(255, 255, 255, 0.95);
|
| 7943 |
+
background: radial-gradient(circle, rgba(255,255,255,0.78), rgba(255,255,255,0.12) 58%, transparent 72%);
|
| 7944 |
+
filter: drop-shadow(0 0 10px rgba(255,255,255,0.85));
|
| 7945 |
+
font-size: clamp(14px, calc(var(--card-fit-size, var(--card-size, 150px)) * 0.18), 28px);
|
| 7946 |
+
pointer-events: none;
|
| 7947 |
+
animation: distractorSparkleRuntime 680ms ease-out forwards;
|
| 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);
|
|
|
|
| 8749 |
}, 850);
|
| 8750 |
}
|
| 8751 |
|
| 8752 |
+
getEffectiveMaxHints() {
|
| 8753 |
+
const baseMax = Math.max(1, Number(this.maxHints || this.state.max_hints || 5));
|
| 8754 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8755 |
+
|
| 8756 |
+
// Hint cap gently tightens as levels progress.
|
| 8757 |
+
// L1-5: normal cap, L6-10: -1, L11-16: -2, L17+: -3.
|
| 8758 |
+
const reduction = level <= 5 ? 0 : level <= 10 ? 1 : level <= 16 ? 2 : 3;
|
| 8759 |
+
return Math.max(1, baseMax - reduction);
|
| 8760 |
+
}
|
| 8761 |
+
|
| 8762 |
+
syncProgressiveHintLimit(announce = false) {
|
| 8763 |
+
const effectiveMaxHints = this.getEffectiveMaxHints();
|
| 8764 |
+
const previousHints = Number(this.hints || 0);
|
| 8765 |
+
this.hints = Math.max(0, Math.min(effectiveMaxHints, previousHints));
|
| 8766 |
+
this.state.hints = this.hints;
|
| 8767 |
+
|
| 8768 |
+
if (announce && previousHints > this.hints) {
|
| 8769 |
+
this.setStatus(
|
| 8770 |
+
'Harder level: peeks are rarer now.',
|
| 8771 |
+
'show',
|
| 8772 |
+
`Peek limit for this level: ${this.hints} / ${effectiveMaxHints}`
|
| 8773 |
+
);
|
| 8774 |
+
}
|
| 8775 |
+
|
| 8776 |
+
return effectiveMaxHints;
|
| 8777 |
+
}
|
| 8778 |
+
|
| 8779 |
+
getDistractorSparkleIntervalMs() {
|
| 8780 |
+
const level = Math.max(1, Number(this.levelNumber || this.state.level || 1));
|
| 8781 |
+
|
| 8782 |
+
// Very light distraction: slower early, slightly quicker later.
|
| 8783 |
+
return Math.max(1700, 3900 - Math.min(1700, (level - 1) * 95));
|
| 8784 |
+
}
|
| 8785 |
+
|
| 8786 |
+
triggerDistractorSparkleOnce() {
|
| 8787 |
+
if (!this.grid || this.state.status !== 'playing' || this.locked || this.state.game_over || this.state.level_complete) {
|
| 8788 |
+
return;
|
| 8789 |
+
}
|
| 8790 |
+
|
| 8791 |
+
const candidates = Array.from(this.grid.querySelectorAll('.memory-card:not(.matched):not(.flipped):not(.preview-locked)'));
|
| 8792 |
+
if (!candidates.length) return;
|
| 8793 |
+
|
| 8794 |
+
const card = candidates[Math.floor(Math.random() * candidates.length)];
|
| 8795 |
+
if (!card) return;
|
| 8796 |
+
|
| 8797 |
+
card.classList.remove('distractor-sparkle');
|
| 8798 |
+
void card.offsetWidth;
|
| 8799 |
+
card.classList.add('distractor-sparkle');
|
| 8800 |
+
|
| 8801 |
+
if (this.distractorSparkleClearTimer) {
|
| 8802 |
+
clearTimeout(this.distractorSparkleClearTimer);
|
| 8803 |
+
this.distractorSparkleClearTimer = null;
|
| 8804 |
+
}
|
| 8805 |
+
this.distractorSparkleClearTimer = setTimeout(() => {
|
| 8806 |
+
card.classList.remove('distractor-sparkle');
|
| 8807 |
+
this.distractorSparkleClearTimer = null;
|
| 8808 |
+
}, 740);
|
| 8809 |
+
}
|
| 8810 |
+
|
| 8811 |
+
startDistractorSparkles() {
|
| 8812 |
+
if (this.distractorSparkleTimer || !this.grid || this.state.status !== 'playing') {
|
| 8813 |
+
return;
|
| 8814 |
+
}
|
| 8815 |
+
|
| 8816 |
+
const interval = this.getDistractorSparkleIntervalMs();
|
| 8817 |
+
this.distractorSparkleTimer = setInterval(() => {
|
| 8818 |
+
if (window.__activeMemoryLevelId !== this.levelId || this.state.status !== 'playing') {
|
| 8819 |
+
this.stopDistractorSparkles();
|
| 8820 |
+
return;
|
| 8821 |
+
}
|
| 8822 |
+
this.triggerDistractorSparkleOnce();
|
| 8823 |
+
}, interval);
|
| 8824 |
+
}
|
| 8825 |
+
|
| 8826 |
+
stopDistractorSparkles() {
|
| 8827 |
+
if (this.distractorSparkleTimer) {
|
| 8828 |
+
clearInterval(this.distractorSparkleTimer);
|
| 8829 |
+
this.distractorSparkleTimer = null;
|
| 8830 |
+
}
|
| 8831 |
+
if (this.distractorSparkleClearTimer) {
|
| 8832 |
+
clearTimeout(this.distractorSparkleClearTimer);
|
| 8833 |
+
this.distractorSparkleClearTimer = null;
|
| 8834 |
+
}
|
| 8835 |
+
if (this.grid) {
|
| 8836 |
+
this.grid.querySelectorAll('.memory-card.distractor-sparkle').forEach((card) => {
|
| 8837 |
+
card.classList.remove('distractor-sparkle');
|
| 8838 |
+
});
|
| 8839 |
+
}
|
| 8840 |
+
}
|
| 8841 |
+
|
| 8842 |
updateHintsDisplay() {
|
| 8843 |
+
const effectiveMaxHints = this.syncProgressiveHintLimit(false);
|
| 8844 |
if (this.hintCountEl) {
|
| 8845 |
+
this.hintCountEl.textContent = `${this.hints} / ${effectiveMaxHints}`;
|
| 8846 |
}
|
| 8847 |
|
| 8848 |
const currentHints = Number(this.hints || 0);
|
|
|
|
| 9384 |
this.challengeCorrectIndex = null;
|
| 9385 |
}
|
| 9386 |
|
| 9387 |
+
this.stopDistractorSparkles();
|
| 9388 |
this.renderCards();
|
| 9389 |
this.updateHud();
|
| 9390 |
this.updateCardSize();
|
|
|
|
| 9633 |
if (this.nextBtn) {
|
| 9634 |
this.nextBtn.disabled = status !== 'complete';
|
| 9635 |
}
|
| 9636 |
+
if (status === 'playing') {
|
| 9637 |
+
this.startDistractorSparkles();
|
| 9638 |
+
} else {
|
| 9639 |
+
this.stopDistractorSparkles();
|
| 9640 |
+
}
|
| 9641 |
this.updateHintsDisplay();
|
| 9642 |
this.syncControlsVisibility();
|
| 9643 |
}
|
|
|
|
| 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(
|
| 9666 |
'Memorize the cards before they flip!',
|
|
|
|
| 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) {
|
| 9727 |
this.overlay.style.display = 'flex';
|
|
|
|
| 9999 |
this.state.performance_meter = 0;
|
| 10000 |
this.state.challenge_due = false;
|
| 10001 |
}
|
| 10002 |
+
const effectiveMaxHints = this.getEffectiveMaxHints();
|
| 10003 |
+
const shouldAwardPeek = this.hints < effectiveMaxHints;
|
| 10004 |
this.showFeedbackOverlay(
|
| 10005 |
'LEVEL COMPLETE',
|
| 10006 |
isChallengeBoard ? 'Level Complete!\n+1 Life!' : (shouldAwardPeek ? 'Level Complete!\n+1 Peek earned!' : 'Level Complete!'),
|
|
|
|
| 10025 |
pairFact || featuredFact || this.state.theme || `Bonus ready for level ${this.state.level}.`
|
| 10026 |
);
|
| 10027 |
if (!isChallengeBoard) {
|
| 10028 |
+
this.hints = Math.min(effectiveMaxHints, this.hints + 1);
|
| 10029 |
this.state.hints = this.hints;
|
| 10030 |
}
|
| 10031 |
this.updateHud();
|
|
|
|
| 10042 |
}
|
| 10043 |
const hintMessage = shouldAwardPeek
|
| 10044 |
? '+1 peek earned!'
|
| 10045 |
+
: `Max peeks earned for this level: ${this.hints} / ${effectiveMaxHints}`;
|
| 10046 |
const completionDetails = [featuredFact, hintMessage].filter(Boolean).join('\n');
|
| 10047 |
this.setStatus(
|
| 10048 |
this.state.victory_message || 'Level cleared. Click NEXT LEVEL to continue.',
|
|
|
|
| 10119 |
|
| 10120 |
async useHint() {
|
| 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', '');
|
|
|
|
| 10202 |
: Number(this.state.previous_level_time_seconds || 0);
|
| 10203 |
this.state.win_streak = 0;
|
| 10204 |
this.resetComboFeedback();
|
| 10205 |
+
this.stopDistractorSparkles();
|
| 10206 |
this.locked = true;
|
| 10207 |
this.applyStateStyle();
|
| 10208 |
this.setStatus(
|
|
|
|
| 10373 |
await this.refreshLeaderboardData();
|
| 10374 |
this.hideResetConfirm();
|
| 10375 |
this.hideGameOverModal();
|
| 10376 |
+
this.stopDistractorSparkles();
|
| 10377 |
this.showTransitionOverlay('🤖 Starting New Game', 'Resetting the board and preparing a fresh run...');
|
| 10378 |
if (this.nextBtn) this.nextBtn.disabled = true;
|
| 10379 |
|