Refine timer, failures, and visibility; debounce snitch

This commit is contained in:
chris 2025-11-20 10:32:19 -05:00
parent 5ac93f114c
commit 42e735569f
3 changed files with 141 additions and 47 deletions

127
app.js
View File

@ -81,10 +81,11 @@
currentInput: "",
attempts: 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 cheatClicks = 0;
@ -168,6 +169,7 @@
resetDementor();
game.currentInput = "";
game.attempts = 0;
game.forcedAnswer = false;
game.startTime = Date.now();
document.getElementById('answer-display').innerText = "";
@ -175,6 +177,10 @@
const hintGrid = document.getElementById('hint-grid');
hintGrid.innerHTML = "";
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) {
game.timeTurnerCharges--;
@ -284,24 +290,7 @@
}
saveData(); updateUI(); setTimeout(generateQuestion, 1000);
} else {
state.streak = 0;
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);
}
handleWrongAttempt();
}
}
@ -697,6 +686,7 @@
function startDementor(resume = false) {
clearInterval(dementor.interval);
if (!resume) dementor.progress = 0;
dementor.timedOut = false;
let speed = 250;
const step = 0.02;
if (game.timeTurnerCharges > 0) speed = 600;
@ -706,6 +696,9 @@
if (!game.active) return;
if (dementor.progress < 1) {
dementor.progress += step; updateDementorVisuals();
} else if (!dementor.timedOut) {
dementor.timedOut = true;
handleDementorTimeout();
}
}, speed);
}
@ -725,9 +718,11 @@
overlay.style.setProperty('--circle-blur', `${blur}px`);
overlay.style.setProperty('--vignette-strength', vignette.toFixed(2));
overlay.style.background = buildDementorBackground(p, size, darkness, vignette);
const ring = document.getElementById('timer-ring');
if (ring) ring.style.setProperty('--timer-progress', p.toFixed(3));
const castTimer = document.getElementById('cast-timer-fill');
if (castTimer) castTimer.style.width = `${Math.max(0, 100 - p * 100)}%`;
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() {
@ -739,10 +734,12 @@
overlay.style.setProperty('--circle-blur', '0px');
overlay.style.setProperty('--vignette-strength', '0.18');
overlay.style.background = 'transparent';
const ring = document.getElementById('timer-ring');
if (ring) ring.style.setProperty('--timer-progress', '0');
const castTimer = document.getElementById('cast-timer-fill');
if (castTimer) castTimer.style.width = '100%';
document.documentElement.style.setProperty('--focus-glow', '0');
document.documentElement.style.setProperty('--ui-glow', '0');
saveData();
dementor.timedOut = false;
}
function buildDementorBackground(progress, size, strength, vignette) {
@ -798,6 +795,82 @@
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() {
const levelData = CURRICULUM[state.levelIndex];
const requiredFacts = getFactsForLevel(state.levelIndex);
@ -864,7 +937,7 @@
// --- GOLDEN SNITCH EVENT ---
function scheduleSnitch() {
if (snitchTimer) clearTimeout(snitchTimer);
const delay = 20000 + Math.random() * 20000; // 2040s
const delay = 45000 + Math.random() * 30000; // 4575s
snitchTimer = setTimeout(spawnSnitch, delay);
}
@ -874,6 +947,10 @@
scheduleSnitch();
return;
}
if (dementor.progress > 0.6) { // scared off by looming dementor
scheduleSnitch();
return;
}
const snitch = document.createElement('div');
snitch.className = 'flying-snitch';
const fromLeft = Math.random() > 0.5;

View File

@ -64,7 +64,6 @@
<div id="snitch-layer"></div>
<div class="card-container skin-default" id="main-card">
<div id="timer-ring"></div>
<div class="header-info">
<button class="shop-btn" onclick="openShop()" title="Student Trunk">🎒</button>
<div class="currency-badge">
@ -106,7 +105,10 @@
<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('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 class="feedback" id="feedback-msg"></div>

View File

@ -5,6 +5,7 @@
--burgundy: #740001;
--bg-dark: #121212;
--focus-glow: 0;
--ui-glow: 0;
}
* { 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;
}
#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; }
#xp-text {
display: inline-block;
@ -264,6 +250,11 @@
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);
}
.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 {
background: var(--burgundy); color: var(--gold); padding: 5px 10px;
@ -298,6 +289,14 @@
position: relative;
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 {
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);
}
.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 {
background: #ffd966;
color: #2c2c2c;
@ -331,11 +346,11 @@
.feedback {
height: 20px; margin: 5px 0; font-weight: bold; font-size: 1rem;
color: #fffdf2;
color: #fff8f8;
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(8px + 16px * var(--focus-glow)) rgba(0, 0, 0, 0.55);
-webkit-text-stroke: 0.5px rgba(0,0,0,0.35);
0 0 calc(5px + 12px * var(--focus-glow)) rgba(255, 240, 240, calc(0.5 + 0.45 * var(--focus-glow))),
0 0 calc(9px + 18px * var(--focus-glow)) rgba(0, 0, 0, 0.75);
-webkit-text-stroke: 0.6px rgba(0,0,0,0.55);
}
.quick-consumables {
display: grid;