Refine timer, failures, and visibility; debounce snitch
This commit is contained in:
parent
5ac93f114c
commit
42e735569f
127
app.js
127
app.js
@ -81,10 +81,11 @@
|
|||||||
currentInput: "",
|
currentInput: "",
|
||||||
attempts: 0,
|
attempts: 0,
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
timeTurnerCharges: 0
|
timeTurnerCharges: 0,
|
||||||
|
forcedAnswer: false
|
||||||
};
|
};
|
||||||
|
|
||||||
let dementor = { interval: null, progress: 0 };
|
let dementor = { interval: null, progress: 0, timedOut: false };
|
||||||
let snitchTimer = null;
|
let snitchTimer = null;
|
||||||
let cheatClicks = 0;
|
let cheatClicks = 0;
|
||||||
|
|
||||||
@ -168,6 +169,7 @@
|
|||||||
resetDementor();
|
resetDementor();
|
||||||
game.currentInput = "";
|
game.currentInput = "";
|
||||||
game.attempts = 0;
|
game.attempts = 0;
|
||||||
|
game.forcedAnswer = false;
|
||||||
game.startTime = Date.now();
|
game.startTime = Date.now();
|
||||||
|
|
||||||
document.getElementById('answer-display').innerText = "";
|
document.getElementById('answer-display').innerText = "";
|
||||||
@ -175,6 +177,10 @@
|
|||||||
const hintGrid = document.getElementById('hint-grid');
|
const hintGrid = document.getElementById('hint-grid');
|
||||||
hintGrid.innerHTML = "";
|
hintGrid.innerHTML = "";
|
||||||
hintGrid.classList.remove('has-stars');
|
hintGrid.classList.remove('has-stars');
|
||||||
|
const castBtn = document.querySelector('.key-cast');
|
||||||
|
const answerDisplay = document.getElementById('answer-display');
|
||||||
|
if (castBtn) castBtn.classList.remove('ready-cast');
|
||||||
|
if (answerDisplay) answerDisplay.classList.remove('forced-answer');
|
||||||
|
|
||||||
if (game.timeTurnerCharges > 0) {
|
if (game.timeTurnerCharges > 0) {
|
||||||
game.timeTurnerCharges--;
|
game.timeTurnerCharges--;
|
||||||
@ -284,24 +290,7 @@
|
|||||||
}
|
}
|
||||||
saveData(); updateUI(); setTimeout(generateQuestion, 1000);
|
saveData(); updateUI(); setTimeout(generateQuestion, 1000);
|
||||||
} else {
|
} else {
|
||||||
state.streak = 0;
|
handleWrongAttempt();
|
||||||
game.attempts++;
|
|
||||||
addToTroubleList(game.currentFact.n1, game.currentFact.n2);
|
|
||||||
card.classList.add('apply-shake');
|
|
||||||
setTimeout(() => card.classList.remove('apply-shake'), 500);
|
|
||||||
dementor.progress += 0.2; updateDementorVisuals();
|
|
||||||
saveData(); updateUI();
|
|
||||||
game.currentInput = "";
|
|
||||||
document.getElementById('answer-display').innerText = "";
|
|
||||||
|
|
||||||
if (game.attempts >= 3) {
|
|
||||||
feedback.style.color = '#ffb0b0';
|
|
||||||
feedback.innerText = `Answer is ${game.currentFact.ans}.`;
|
|
||||||
} else {
|
|
||||||
feedback.style.color = '#ffb0b0';
|
|
||||||
feedback.innerText = "Count the stars...";
|
|
||||||
drawHintGrid(game.currentFact.n1, game.currentFact.n2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -697,6 +686,7 @@
|
|||||||
function startDementor(resume = false) {
|
function startDementor(resume = false) {
|
||||||
clearInterval(dementor.interval);
|
clearInterval(dementor.interval);
|
||||||
if (!resume) dementor.progress = 0;
|
if (!resume) dementor.progress = 0;
|
||||||
|
dementor.timedOut = false;
|
||||||
let speed = 250;
|
let speed = 250;
|
||||||
const step = 0.02;
|
const step = 0.02;
|
||||||
if (game.timeTurnerCharges > 0) speed = 600;
|
if (game.timeTurnerCharges > 0) speed = 600;
|
||||||
@ -706,6 +696,9 @@
|
|||||||
if (!game.active) return;
|
if (!game.active) return;
|
||||||
if (dementor.progress < 1) {
|
if (dementor.progress < 1) {
|
||||||
dementor.progress += step; updateDementorVisuals();
|
dementor.progress += step; updateDementorVisuals();
|
||||||
|
} else if (!dementor.timedOut) {
|
||||||
|
dementor.timedOut = true;
|
||||||
|
handleDementorTimeout();
|
||||||
}
|
}
|
||||||
}, speed);
|
}, speed);
|
||||||
}
|
}
|
||||||
@ -725,9 +718,11 @@
|
|||||||
overlay.style.setProperty('--circle-blur', `${blur}px`);
|
overlay.style.setProperty('--circle-blur', `${blur}px`);
|
||||||
overlay.style.setProperty('--vignette-strength', vignette.toFixed(2));
|
overlay.style.setProperty('--vignette-strength', vignette.toFixed(2));
|
||||||
overlay.style.background = buildDementorBackground(p, size, darkness, vignette);
|
overlay.style.background = buildDementorBackground(p, size, darkness, vignette);
|
||||||
const ring = document.getElementById('timer-ring');
|
const castTimer = document.getElementById('cast-timer-fill');
|
||||||
if (ring) ring.style.setProperty('--timer-progress', p.toFixed(3));
|
if (castTimer) castTimer.style.width = `${Math.max(0, 100 - p * 100)}%`;
|
||||||
document.documentElement.style.setProperty('--focus-glow', glow.toFixed(2));
|
document.documentElement.style.setProperty('--focus-glow', glow.toFixed(2));
|
||||||
|
const uiGlow = Math.min(1.2, 0.15 + (p * 1.05));
|
||||||
|
document.documentElement.style.setProperty('--ui-glow', uiGlow.toFixed(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDementor() {
|
function resetDementor() {
|
||||||
@ -739,10 +734,12 @@
|
|||||||
overlay.style.setProperty('--circle-blur', '0px');
|
overlay.style.setProperty('--circle-blur', '0px');
|
||||||
overlay.style.setProperty('--vignette-strength', '0.18');
|
overlay.style.setProperty('--vignette-strength', '0.18');
|
||||||
overlay.style.background = 'transparent';
|
overlay.style.background = 'transparent';
|
||||||
const ring = document.getElementById('timer-ring');
|
const castTimer = document.getElementById('cast-timer-fill');
|
||||||
if (ring) ring.style.setProperty('--timer-progress', '0');
|
if (castTimer) castTimer.style.width = '100%';
|
||||||
document.documentElement.style.setProperty('--focus-glow', '0');
|
document.documentElement.style.setProperty('--focus-glow', '0');
|
||||||
|
document.documentElement.style.setProperty('--ui-glow', '0');
|
||||||
saveData();
|
saveData();
|
||||||
|
dementor.timedOut = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDementorBackground(progress, size, strength, vignette) {
|
function buildDementorBackground(progress, size, strength, vignette) {
|
||||||
@ -798,6 +795,82 @@
|
|||||||
grid.classList.add('has-stars');
|
grid.classList.add('has-stars');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleWrongAttempt(options = {}) {
|
||||||
|
const { timedOut = false } = options;
|
||||||
|
const feedback = document.getElementById('feedback-msg');
|
||||||
|
const card = document.querySelector('.card-container');
|
||||||
|
state.streak = 0;
|
||||||
|
game.attempts = timedOut ? 3 : game.attempts + 1;
|
||||||
|
if (card) {
|
||||||
|
card.classList.add('apply-shake');
|
||||||
|
setTimeout(() => card.classList.remove('apply-shake'), 500);
|
||||||
|
}
|
||||||
|
if (!timedOut) {
|
||||||
|
dementor.progress += 0.2; updateDementorVisuals();
|
||||||
|
game.currentInput = "";
|
||||||
|
document.getElementById('answer-display').innerText = "";
|
||||||
|
}
|
||||||
|
if (game.attempts >= 3) {
|
||||||
|
revealAnswerAndAdvance(timedOut ? 2000 : 0, timedOut);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game.attempts === 2) {
|
||||||
|
feedback.style.color = '#ffb0b0';
|
||||||
|
feedback.innerText = "Count the stars...";
|
||||||
|
drawHintGrid(game.currentFact.n1, game.currentFact.n2);
|
||||||
|
} else {
|
||||||
|
feedback.style.color = '#ffb0b0';
|
||||||
|
feedback.innerText = "Try again!";
|
||||||
|
}
|
||||||
|
saveData(); updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function revealAnswerAndAdvance(delayMs = 0, timedOut = false) {
|
||||||
|
const feedback = document.getElementById('feedback-msg');
|
||||||
|
const card = document.querySelector('.card-container');
|
||||||
|
const factId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||||||
|
game.active = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
// guard in case question changed
|
||||||
|
const currentId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||||||
|
if (currentId !== factId) return;
|
||||||
|
if (card) {
|
||||||
|
card.classList.add('apply-shake');
|
||||||
|
setTimeout(() => card.classList.remove('apply-shake'), 500);
|
||||||
|
}
|
||||||
|
feedback.style.color = '#ffb0b0';
|
||||||
|
feedback.innerText = `Answer is ${game.currentFact.ans}. Press CAST to continue.`;
|
||||||
|
addToTroubleList(game.currentFact.n1, game.currentFact.n2);
|
||||||
|
game.currentInput = game.currentFact.ans.toString();
|
||||||
|
const answerDisplay = document.getElementById('answer-display');
|
||||||
|
if (answerDisplay) {
|
||||||
|
answerDisplay.innerText = game.currentInput;
|
||||||
|
answerDisplay.classList.add('forced-answer');
|
||||||
|
}
|
||||||
|
const castBtn = document.querySelector('.key-cast');
|
||||||
|
if (castBtn) castBtn.classList.add('ready-cast');
|
||||||
|
game.forcedAnswer = true;
|
||||||
|
resetDementor();
|
||||||
|
saveData(); updateUI();
|
||||||
|
game.active = true;
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDementorTimeout() {
|
||||||
|
const factId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||||||
|
game.active = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||||||
|
if (currentId !== factId) return;
|
||||||
|
const feedback = document.getElementById('feedback-msg');
|
||||||
|
if (feedback) {
|
||||||
|
feedback.style.color = '#ffb0b0';
|
||||||
|
feedback.innerText = "Saved by a Professor...";
|
||||||
|
}
|
||||||
|
handleWrongAttempt({ timedOut: true });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
function applyXPToMastery() {
|
function applyXPToMastery() {
|
||||||
const levelData = CURRICULUM[state.levelIndex];
|
const levelData = CURRICULUM[state.levelIndex];
|
||||||
const requiredFacts = getFactsForLevel(state.levelIndex);
|
const requiredFacts = getFactsForLevel(state.levelIndex);
|
||||||
@ -864,7 +937,7 @@
|
|||||||
// --- GOLDEN SNITCH EVENT ---
|
// --- GOLDEN SNITCH EVENT ---
|
||||||
function scheduleSnitch() {
|
function scheduleSnitch() {
|
||||||
if (snitchTimer) clearTimeout(snitchTimer);
|
if (snitchTimer) clearTimeout(snitchTimer);
|
||||||
const delay = 20000 + Math.random() * 20000; // 20–40s
|
const delay = 45000 + Math.random() * 30000; // 45–75s
|
||||||
snitchTimer = setTimeout(spawnSnitch, delay);
|
snitchTimer = setTimeout(spawnSnitch, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -874,6 +947,10 @@
|
|||||||
scheduleSnitch();
|
scheduleSnitch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (dementor.progress > 0.6) { // scared off by looming dementor
|
||||||
|
scheduleSnitch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const snitch = document.createElement('div');
|
const snitch = document.createElement('div');
|
||||||
snitch.className = 'flying-snitch';
|
snitch.className = 'flying-snitch';
|
||||||
const fromLeft = Math.random() > 0.5;
|
const fromLeft = Math.random() > 0.5;
|
||||||
|
|||||||
@ -64,7 +64,6 @@
|
|||||||
<div id="snitch-layer"></div>
|
<div id="snitch-layer"></div>
|
||||||
|
|
||||||
<div class="card-container skin-default" id="main-card">
|
<div class="card-container skin-default" id="main-card">
|
||||||
<div id="timer-ring"></div>
|
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<button class="shop-btn" onclick="openShop()" title="Student Trunk">🎒</button>
|
<button class="shop-btn" onclick="openShop()" title="Student Trunk">🎒</button>
|
||||||
<div class="currency-badge">
|
<div class="currency-badge">
|
||||||
@ -106,7 +105,10 @@
|
|||||||
<button class="key-btn key-clear" onclick="pressKey('C')">C</button>
|
<button class="key-btn key-clear" onclick="pressKey('C')">C</button>
|
||||||
<button class="key-btn" onclick="pressKey(0)">0</button>
|
<button class="key-btn" onclick="pressKey(0)">0</button>
|
||||||
<button class="key-btn" onclick="pressKey('BS')">⌫</button>
|
<button class="key-btn" onclick="pressKey('BS')">⌫</button>
|
||||||
<button class="key-btn key-cast" onclick="checkAnswer()">CAST SPELL</button>
|
<button class="key-btn key-cast" onclick="checkAnswer()">
|
||||||
|
<span class="cast-label">CAST SPELL</span>
|
||||||
|
<div class="cast-timer"><div class="cast-timer-fill" id="cast-timer-fill"></div></div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="feedback" id="feedback-msg"></div>
|
<div class="feedback" id="feedback-msg"></div>
|
||||||
|
|||||||
55
style.css
55
style.css
@ -5,6 +5,7 @@
|
|||||||
--burgundy: #740001;
|
--burgundy: #740001;
|
||||||
--bg-dark: #121212;
|
--bg-dark: #121212;
|
||||||
--focus-glow: 0;
|
--focus-glow: 0;
|
||||||
|
--ui-glow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; }
|
* { box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; }
|
||||||
@ -238,21 +239,6 @@
|
|||||||
display: flex; flex-direction: column; transition: background 0.3s, border-color 0.3s;
|
display: flex; flex-direction: column; transition: background 0.3s, border-color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#timer-ring {
|
|
||||||
position: absolute;
|
|
||||||
inset: -6px;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 14px;
|
|
||||||
z-index: 90;
|
|
||||||
--timer-progress: 0;
|
|
||||||
--timer-fill: #d4af37;
|
|
||||||
background: conic-gradient(var(--timer-fill) calc(var(--timer-progress) * 1turn), rgba(255,255,255,0.08) 0);
|
|
||||||
mask: radial-gradient(circle at 50% 50%, transparent calc(100% - 8px), #000 calc(100% - 6px));
|
|
||||||
-webkit-mask: radial-gradient(circle at 50% 50%, transparent calc(100% - 8px), #000 calc(100% - 6px));
|
|
||||||
opacity: 0.9;
|
|
||||||
box-shadow: 0 0 12px rgba(212, 175, 55, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-family: 'Cinzel', serif; color: var(--burgundy); font-size: 0.9rem; font-weight: bold; }
|
.header-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-family: 'Cinzel', serif; color: var(--burgundy); font-size: 0.9rem; font-weight: bold; }
|
||||||
#xp-text {
|
#xp-text {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -264,6 +250,11 @@
|
|||||||
text-shadow: 0 1px 4px rgba(0,0,0,0.6);
|
text-shadow: 0 1px 4px rgba(0,0,0,0.6);
|
||||||
box-shadow: 0 0 6px rgba(0,0,0,0.25), inset 0 0 8px rgba(255, 225, 140, 0.15);
|
box-shadow: 0 0 6px rgba(0,0,0,0.25), inset 0 0 8px rgba(255, 225, 140, 0.15);
|
||||||
}
|
}
|
||||||
|
.header-info, #xp-text, #mastery-display, .currency-badge, .shop-btn, .help-btn {
|
||||||
|
text-shadow:
|
||||||
|
0 0 calc(4px + 10px * var(--ui-glow)) rgba(255,255,240,calc(0.25 + 0.35 * var(--ui-glow))),
|
||||||
|
0 0 calc(6px + 12px * var(--ui-glow)) rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.currency-badge {
|
.currency-badge {
|
||||||
background: var(--burgundy); color: var(--gold); padding: 5px 10px;
|
background: var(--burgundy); color: var(--gold); padding: 5px 10px;
|
||||||
@ -298,6 +289,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
#answer-display.forced-answer {
|
||||||
|
animation: pulseAnswer 1s ease-in-out infinite alternate;
|
||||||
|
border-bottom-color: #ffd966;
|
||||||
|
}
|
||||||
|
@keyframes pulseAnswer {
|
||||||
|
from { box-shadow: 0 0 12px rgba(255,217,102,0.6); }
|
||||||
|
to { box-shadow: 0 0 18px rgba(255,217,102,0.95); }
|
||||||
|
}
|
||||||
|
|
||||||
#hint-grid {
|
#hint-grid {
|
||||||
display: grid; justify-content: center; gap: 6px; margin: 5px auto; max-width: 220px;
|
display: grid; justify-content: center; gap: 6px; margin: 5px auto; max-width: 220px;
|
||||||
@ -320,7 +319,23 @@
|
|||||||
-webkit-text-stroke: 1px rgba(0,0,0,0.55);
|
-webkit-text-stroke: 1px rgba(0,0,0,0.55);
|
||||||
}
|
}
|
||||||
.key-btn:active { background: var(--gold); }
|
.key-btn:active { background: var(--gold); }
|
||||||
.key-cast { grid-column: span 3; background: var(--burgundy); color: var(--gold); font-weight: bold; }
|
.key-cast { grid-column: span 3; background: var(--burgundy); color: var(--gold); font-weight: bold; position: relative; overflow: hidden; }
|
||||||
|
.cast-label { position: relative; z-index: 2; }
|
||||||
|
.cast-timer {
|
||||||
|
position: absolute; left: 8px; right: 8px; bottom: 6px;
|
||||||
|
height: 6px; background: rgba(0,0,0,0.3); border-radius: 4px;
|
||||||
|
overflow: hidden; z-index: 1;
|
||||||
|
box-shadow: inset 0 0 4px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.cast-timer-fill {
|
||||||
|
height: 100%; width: 100%; border-radius: 4px;
|
||||||
|
background: linear-gradient(90deg, #ffd966, #d4af37);
|
||||||
|
box-shadow: 0 0 8px rgba(255, 217, 102, 0.7);
|
||||||
|
transition: width 0.2s ease-out;
|
||||||
|
}
|
||||||
|
.key-cast.ready-cast {
|
||||||
|
box-shadow: 0 0 14px rgba(255,217,102,0.9), inset 0 0 8px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
.card-container .key-btn.key-clear {
|
.card-container .key-btn.key-clear {
|
||||||
background: #ffd966;
|
background: #ffd966;
|
||||||
color: #2c2c2c;
|
color: #2c2c2c;
|
||||||
@ -331,11 +346,11 @@
|
|||||||
|
|
||||||
.feedback {
|
.feedback {
|
||||||
height: 20px; margin: 5px 0; font-weight: bold; font-size: 1rem;
|
height: 20px; margin: 5px 0; font-weight: bold; font-size: 1rem;
|
||||||
color: #fffdf2;
|
color: #fff8f8;
|
||||||
text-shadow:
|
text-shadow:
|
||||||
0 0 calc(4px + 10px * var(--focus-glow)) rgba(255, 245, 230, calc(0.4 + 0.45 * var(--focus-glow))),
|
0 0 calc(5px + 12px * var(--focus-glow)) rgba(255, 240, 240, calc(0.5 + 0.45 * var(--focus-glow))),
|
||||||
0 0 calc(8px + 16px * var(--focus-glow)) rgba(0, 0, 0, 0.55);
|
0 0 calc(9px + 18px * var(--focus-glow)) rgba(0, 0, 0, 0.75);
|
||||||
-webkit-text-stroke: 0.5px rgba(0,0,0,0.35);
|
-webkit-text-stroke: 0.6px rgba(0,0,0,0.55);
|
||||||
}
|
}
|
||||||
.quick-consumables {
|
.quick-consumables {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user