// --- CONFIG --- const CURRICULUM = [ { name: "Year 1", numbers: [1, 2, 5, 10], targetXP: 150 }, { name: "Year 2", numbers: [3, 4], targetXP: 150 }, { name: "Year 3", numbers: [9, 11], targetXP: 200 }, { name: "Year 4", numbers: [6, 7], targetXP: 250 }, { name: "Year 5", numbers: [8, 12], targetXP: 300 }, { name: "N.E.W.T.s", numbers: [1,2,3,4,5,6,7,8,9,10,11,12], targetXP: 5000 } ]; const SHOP_ITEMS = [ { id: 'auto_quill', name: "Auto-Quill", type: 'consumable', category: 'Supplies', price: 15, desc: "Writes the first digit." }, { id: 'choc_frog', name: "Chocolate Frog", type: 'consumable', category: 'Supplies', price: 25, desc: "Clears Dementors instantly." }, { id: 'liquid_luck', name: "Liquid Luck", type: 'consumable', category: 'Supplies', price: 40, desc: "Solves problem instantly." }, { id: 'time_turner', name: "Time Turner", type: 'consumable', category: 'Magic Items', price: 60, desc: "Slows Dementors for 5 spells." }, { id: 'study_spell', name: "Study Spell", type: 'consumable', category: 'Daily Specials', price: 30, desc: "+25 XP instantly (Mon/Thu).", availableDays: ['Mon','Thu'] }, { id: 'vault_key', name: "Vault Key", type: 'consumable', category: 'Daily Specials', price: 50, desc: "+100 Gold (Sun only).", availableDays: ['Sun'] }, { id: 'phoenix_feather', name: "Phoenix Feather", type: 'consumable', category: 'Daily Specials', price: 45, desc: "Boost streak to 5 (Tue/Fri).", availableDays: ['Tue','Fri'] }, { id: 'shield_charm', name: "Shield Charm", type: 'consumable', category: 'Daily Specials', price: 35, desc: "Reset & slow Dementors for 2 turns (Wed/Sat).", availableDays: ['Wed','Sat'] }, { id: 'astral_map', name: "Astral Map", type: 'consumable', category: 'Daily Specials', price: 40, desc: "Shows a star hint for this question (Sun/Tue/Thu).", availableDays: ['Sun','Tue','Thu'] }, // PETS { id: 'pet_owl', name: "Snowy Owl", type: 'pet', category: 'Familiars', price: 120, desc: "Faithful post owl companion.", class: 'pet-owl', icon: '🦉' }, { id: 'pet_cat', name: "Kneazle Cat", type: 'pet', category: 'Familiars', price: 110, desc: "Clever feline lookout.", class: 'pet-cat', icon: '🐈' }, { id: 'pet_rat', name: "Garden Rat", type: 'pet', category: 'Familiars', price: 90, desc: "Surprisingly loyal.", class: 'pet-rat', icon: '🐀' }, { id: 'pet_toad', name: "Pad-Pad Toad", type: 'pet', category: 'Familiars', price: 80, desc: "Keeps calm in the classroom.", class: 'pet-toad', icon: '🐸' }, { id: 'pet_ferret', name: "Mischief Ferret", type: 'pet', category: 'Familiars', price: 130, desc: "Fast distraction in duels.", class: 'pet-ferret', icon: '🦦' }, // SKINS (Hogwarts Houses + specials) { id: 'skin_gryffindor', name: "Gryffindor", type: 'skin', category: 'Card Skins', price: 100, desc: "Scarlet and gold bravery motif.", class: "skin-gryffindor" }, { id: 'skin_slytherin', name: "Slytherin", type: 'skin', category: 'Card Skins', price: 100, desc: "Emerald and silver ambition motif.", class: "skin-slytherin" }, { id: 'skin_ravenclaw', name: "Ravenclaw", type: 'skin', category: 'Card Skins', price: 100, desc: "Bronze and blue wit motif.", class: "skin-ravenclaw" }, { id: 'skin_hufflepuff', name: "Hufflepuff", type: 'skin', category: 'Card Skins', price: 100, desc: "Honey and charcoal loyalty motif.", class: "skin-hufflepuff" }, { id: 'skin_map', name: "Marauder's Map", type: 'skin', category: 'Card Skins', price: 140, desc: "Footprints on aged parchment.", class: "skin-map" }, { id: 'skin_spellbook', name: "Spellbook", type: 'skin', category: 'Card Skins', price: 140, desc: "Worn leather and gold filigree.", class: "skin-spellbook" }, // TRAILS { id: 'trail_sparks', name: "Sparks Trail", type: 'trail', category: 'Wand Effects', price: 120, desc: "Golden sparks.", class: "trail-sparks" }, { id: 'trail_ice', name: "Frost Trail", type: 'trail', category: 'Wand Effects', price: 120, desc: "Icy crystals.", class: "trail-ice" }, { id: 'trail_slime', name: "Troll Slime", type: 'trail', category: 'Wand Effects', price: 80, desc: "Sticky green trail.", class: "trail-slime" }, ]; function getFactsForLevel(levelIndex) { const level = CURRICULUM[levelIndex]; const facts = new Set(); for (const num1 of level.numbers) { for (let num2 = 1; num2 <= 12; num2++) { const fact = [num1, num2].sort((a, b) => a - b); facts.add(`${fact[0]}x${fact[1]}`); } } return Array.from(facts); } function getLevelTargetXP() { const levelData = CURRICULUM[state.levelIndex]; const requiredCount = getFactsForLevel(state.levelIndex).length; const scaled = requiredCount * 12; // scales with how many facts are in the level return Math.max(levelData.targetXP, scaled); } function allFactsMastered(levelIndex) { const requiredFacts = getFactsForLevel(levelIndex); const masteredFactsInLevel = state.masteredFacts.filter(fact => requiredFacts.includes(fact)); return masteredFactsInLevel.length === requiredFacts.length; } // --- STATE --- let state = { levelIndex: 0, currentLevelXP: 0, streak: 0, gold: 0, troubleList: [], masteredFacts: [], totalQuestions: 0, correctAnswers: 0, fastestTime: null, inventory: {}, activeSkin: 'skin-default', activeTrail: '', activeBackground: 'bg-default', activePet: '', activePetIcon: '', studentName: '', focusQueue: [], lastFactId: '' }; let game = { active: false, currentFact: { n1: 0, n2: 0, ans: 0 }, currentInput: "", attempts: 0, startTime: 0, timeTurnerCharges: 0, forcedAnswer: false }; let dementor = { interval: null, progress: 0, timedOut: false }; let snitchTimer = null; let cheatClicks = 0; // --- INIT --- function init() { loadData(); const card = document.getElementById('main-card'); const overlay = document.getElementById('dementor-overlay'); if (card && overlay && overlay.parentElement !== card) { card.prepend(overlay); } if (!state.studentName) { const hashName = window.location.hash.substring(1); if (hashName) { state.studentName = hashName.substring(0, 12); saveData(); updateLetterGreeting(); document.querySelector('.letter .signature').insertAdjacentHTML('beforebegin', `

We await your owl, ${state.studentName}.

`); } else { Swal.fire({ title: 'Welcome, young wizard!', text: 'Please state your name for the Hogwarts registry.', input: 'text', inputPlaceholder: 'Your Name', allowOutsideClick: false, allowEscapeKey: false, customClass: { popup: 'hogwarts-popup', confirmButton: 'hogwarts-button', title: 'hogwarts-title', }, preConfirm: (name) => { if (!name) { Swal.showValidationMessage('You must provide a name to enroll!') } return name } }).then((result) => { state.studentName = result.value.substring(0, 12); saveData(); updateLetterGreeting(); document.querySelector('.letter .signature').insertAdjacentHTML('beforebegin', `

We await your owl, ${state.studentName}.

`); }); } } else { updateLetterGreeting(); document.querySelector('.letter .signature').insertAdjacentHTML('beforebegin', `

We await your owl, ${state.studentName}.

`); } applyCosmetics(); updateUI(); scheduleSnitch(); document.addEventListener('touchmove', handleTouchTrail, {passive: false}); document.addEventListener('mousemove', handleMouseTrail); document.addEventListener('keydown', handleKeydown); // CHEAT LISTENER (Tap Year X 5 times) document.getElementById('level-name').addEventListener('click', () => { cheatClicks++; if (cheatClicks === 5) { state.gold += 500; document.getElementById('feedback-msg').style.color = '#d4af37'; document.getElementById('feedback-msg').innerText = "Gringotts Vault Opened! +500G"; saveData(); updateUI(); cheatClicks = 0; } setTimeout(() => cheatClicks = 0, 2000); }); const skip = localStorage.getItem('arithmancySkipIntro'); if (skip === 'true') startGame(); else openIntro(); } // --- CORE LOOP --- function startGame() { game.active = true; generateQuestion(); } function generateQuestion() { resetDementor(); game.currentInput = ""; game.attempts = 0; game.forcedAnswer = false; game.startTime = Date.now(); document.getElementById('answer-display').innerText = ""; document.getElementById('feedback-msg').innerText = ""; 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--; updateBuffDisplay(); } else { updateBuffDisplay(); } const levelData = CURRICULUM[state.levelIndex]; const allowedNumbers = levelData.numbers; const relevantTrouble = state.troubleList.filter(t => allowedNumbers.includes(t.n1) || allowedNumbers.includes(t.n2)); let n1, n2; const makeId = (a, b) => { const [s, l] = a < b ? [a, b] : [b, a]; return `${s}x${l}`; }; const lastId = state.lastFactId; if (state.focusQueue.length > 0) { const factId = state.focusQueue.shift(); const parts = factId.split('x').map(Number); [n1, n2] = parts; } else { let attempts = 0; while (attempts < 6) { if (relevantTrouble.length > 0 && Math.random() < 0.5) { const struggle = relevantTrouble[Math.floor(Math.random() * relevantTrouble.length)]; n1 = struggle.n1; n2 = struggle.n2; } else { n1 = allowedNumbers[Math.floor(Math.random() * allowedNumbers.length)]; n2 = Math.floor(Math.random() * 12) + 1; if (Math.random() > 0.5) [n1, n2] = [n2, n1]; } const cid = makeId(n1, n2); if (cid !== lastId || attempts === 5) break; attempts++; } } game.currentFact = { n1, n2, ans: n1 * n2 }; state.lastFactId = makeId(n1, n2); document.getElementById('equation-display').innerText = `${n1} × ${n2}`; startDementor(); } function checkAnswer() { if (!game.active || game.currentInput === "") return; state.totalQuestions++; const userAns = parseInt(game.currentInput); const feedback = document.getElementById('feedback-msg'); const card = document.querySelector('.card-container'); const levelData = CURRICULUM[state.levelIndex]; if (userAns === game.currentFact.ans) { state.correctAnswers++; const answerTime = Date.now() - game.startTime; if (state.fastestTime === null || answerTime < state.fastestTime) { state.fastestTime = answerTime; } state.streak++; const goldGain = game.attempts === 0 ? 2 : 1; state.gold += goldGain; const xpGain = game.attempts === 0 ? 10 : 5; state.currentLevelXP += xpGain; applyXPToMastery(); const xpPop = document.createElement('div'); xpPop.className = 'xp-gain-pop'; xpPop.innerText = `+${xpGain} XP`; card.appendChild(xpPop); setTimeout(() => xpPop.remove(), 1000); if (game.attempts === 0) { const fact = [game.currentFact.n1, game.currentFact.n2].sort((a, b) => a - b); const factId = `${fact[0]}x${fact[1]}`; if (!state.masteredFacts.includes(factId)) { state.masteredFacts.push(factId); } removeFromTroubleList(game.currentFact.n1, game.currentFact.n2); } castPatronus(); feedback.style.color = '#7cff9a'; feedback.innerText = `Expecto Patronum! (+${goldGain} G)`; if (allFactsMastered(state.levelIndex) && state.levelIndex < CURRICULUM.length - 1) { state.levelIndex++; state.currentLevelXP = 0; state.masteredFacts = []; state.focusQueue = []; Swal.fire({ title: 'LEVEL UP!', text: `Welcome to ${CURRICULUM[state.levelIndex].name}`, icon: 'success', customClass: { popup: 'hogwarts-popup', confirmButton: 'hogwarts-button', title: 'hogwarts-title', } }); saveData(); updateUI(); setTimeout(generateQuestion, 2500); return; } saveData(); updateUI(); setTimeout(generateQuestion, 1000); } else { handleWrongAttempt(); } } // --- SHOP & INVENTORY --- function openShop() { game.active = false; clearInterval(dementor.interval); document.getElementById('dementor-overlay').style.display = 'none'; renderShop(); document.getElementById('shop-modal').classList.add('visible'); } function closeShop() { document.getElementById('shop-modal').classList.remove('visible'); document.getElementById('dementor-overlay').style.display = 'block'; game.active = true; startDementor(true); } function availableShopItems() { const dayMap = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; const today = dayMap[new Date().getDay()]; return SHOP_ITEMS.filter(item => { const owned = (state.inventory[item.id] || 0) > 0; if (!item.availableDays) return true; if (owned) return true; return item.availableDays.includes(today); }); } function renderShop() { const list = document.getElementById('shop-list'); document.getElementById('shop-gold').innerText = state.gold; list.innerHTML = ""; const itemsToShow = availableShopItems(); const categories = [...new Set(itemsToShow.map(i => i.category))]; categories.forEach(cat => { const catHeader = document.createElement('div'); catHeader.className = 'shop-category'; catHeader.innerText = cat; list.appendChild(catHeader); itemsToShow.filter(i => i.category === cat).forEach(item => { const qty = state.inventory[item.id] || 0; const div = document.createElement('div'); div.className = `shop-item ${qty > 0 ? 'purchased' : ''}`; let actionHtml = `
`; if (item.type === 'skin' || item.type === 'trail' || item.type === 'background' || item.type === 'pet') { if (qty > 0) { const isEquipped = (item.type === 'skin' && state.activeSkin === item.class) || (item.type === 'trail' && state.activeTrail === item.class) || (item.type === 'background' && state.activeBackground === item.class) || (item.type === 'pet' && state.activePet === item.class); if (isEquipped) { actionHtml += ``; } else { actionHtml += ``; } } else { actionHtml += ``; } } else { if (qty > 0) { actionHtml += `
Owned: ${qty}
`; actionHtml += ``; actionHtml += ``; } else { actionHtml += ``; } } actionHtml += `
`; div.innerHTML = `

${item.name}

${item.desc}

${actionHtml}`; list.appendChild(div); }); }); } function buyItem(id) { const item = SHOP_ITEMS.find(i => i.id === id); if (state.gold >= item.price) { state.gold -= item.price; if (!state.inventory[id]) state.inventory[id] = 0; state.inventory[id]++; saveData(); updateUI(); renderShop(); } } function equipItem(id) { const item = SHOP_ITEMS.find(i => i.id === id); if (item.type === 'skin') state.activeSkin = item.class; if (item.type === 'trail') state.activeTrail = item.class; if (item.type === 'background') state.activeBackground = item.class; if (item.type === 'pet') { state.activePet = item.class; state.activePetIcon = item.icon || ''; } applyCosmetics(); saveData(); renderShop(); } function unequipItem(id) { const item = SHOP_ITEMS.find(i => i.id === id); if (item.type === 'skin') state.activeSkin = 'skin-default'; if (item.type === 'trail') state.activeTrail = ''; if (item.type === 'background') state.activeBackground = 'bg-default'; if (item.type === 'pet') { state.activePet = ''; state.activePetIcon = ''; } applyCosmetics(); saveData(); renderShop(); } function useConsumable(id) { if (!state.inventory[id] || state.inventory[id] <= 0) return; const feedback = document.getElementById('feedback-msg'); const fromShop = document.getElementById('shop-modal')?.classList.contains('visible'); if (id === 'time_turner') { game.timeTurnerCharges = 5; feedback.style.color = '#d4af37'; feedback.innerText = "Time Turner Active (5 turns)"; } else if (id === 'liquid_luck') { game.currentInput = game.currentFact.ans.toString(); document.getElementById('answer-display').innerText = game.currentInput; feedback.style.color = '#d4af37'; feedback.innerText = "Liquid Luck applied!"; } else if (id === 'choc_frog') { resetDementor(); feedback.style.color = '#740001'; feedback.innerText = "Ate Chocolate Frog. You feel better."; } else if (id === 'auto_quill') { const strAns = game.currentFact.ans.toString(); game.currentInput = strAns.charAt(0); document.getElementById('answer-display').innerText = game.currentInput; feedback.style.color = '#d4af37'; feedback.innerText = "The Quill writes..."; } else if (id === 'study_spell') { state.currentLevelXP += 25; feedback.style.color = '#d4af37'; feedback.innerText = "+25 XP from Study Spell!"; } else if (id === 'vault_key') { state.gold += 100; feedback.style.color = '#d4af37'; feedback.innerText = "Gringotts vault refilled! +100G"; } else if (id === 'phoenix_feather') { state.streak = Math.max(state.streak, 5); feedback.style.color = '#d4af37'; feedback.innerText = "Phoenix Feather restored your focus (Streak 5)."; } else if (id === 'shield_charm') { resetDementor(); game.timeTurnerCharges = Math.max(game.timeTurnerCharges, 2); updateBuffDisplay(); feedback.style.color = '#d4af37'; feedback.innerText = "Shield charm up (slow Dementors 2 turns)."; } else if (id === 'astral_map') { drawHintGrid(game.currentFact.n1 || 1, game.currentFact.n2 || 1); feedback.style.color = '#d4af37'; feedback.innerText = "The stars reveal the path."; } state.inventory[id]--; saveData(); renderQuickConsumables(); if (fromShop) closeShop(); else updateUI(); } function updateBuffDisplay() { const bar = document.getElementById('buff-bar'); bar.innerHTML = ""; if (game.timeTurnerCharges > 0) { const badge = document.createElement('div'); badge.className = 'buff-tag'; badge.innerText = `⏳ ${game.timeTurnerCharges}`; bar.appendChild(badge); } } function renderQuickConsumables() { const holder = document.getElementById('quick-consumables'); if (!holder) return; holder.innerHTML = ""; const consumables = SHOP_ITEMS.filter(i => i.type === 'consumable' && (state.inventory[i.id] || 0) > 0); if (consumables.length === 0) { holder.innerHTML = `
No consumables on hand.
`; return; } consumables.forEach(item => { const qty = state.inventory[item.id] || 0; const card = document.createElement('div'); card.className = 'consumable-card'; card.innerHTML = `
${item.name}
Owned: ${qty}
`; holder.appendChild(card); }); } // --- VISUALS --- function applyCosmetics() { const card = document.getElementById('main-card'); const allowedSkins = ['skin-default','skin-gryffindor','skin-slytherin','skin-ravenclaw','skin-hufflepuff','skin-map','skin-spellbook']; if (!allowedSkins.includes(state.activeSkin)) { state.activeSkin = 'skin-default'; } card.className = `card-container ${state.activeSkin}`; const allowedBackgrounds = ['bg-default']; if (!allowedBackgrounds.includes(state.activeBackground)) { state.activeBackground = 'bg-default'; } document.body.className = state.activeBackground; renderPet(); } function handleTouchTrail(e) { if (!state.activeTrail) return; const touch = e.touches[0]; createParticle(touch.clientX, touch.clientY); } function handleMouseTrail(e) { if (!state.activeTrail || e.buttons === 0) return; createParticle(e.clientX, e.clientY); } function createParticle(x, y) { const p = document.createElement('div'); p.className = `particle ${state.activeTrail}`; p.style.left = `${x}px`; p.style.top = `${y}px`; document.body.appendChild(p); setTimeout(() => p.remove(), 800); } function handleKeydown(e) { const activeEl = document.activeElement; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) return; if (document.querySelector('.modal-overlay.visible')) return; if (!game.active && !game.forcedAnswer) return; const k = e.key; if (k >= '0' && k <= '9') { pressKey(Number(k)); } else if (k === 'Backspace') { e.preventDefault(); pressKey('BS'); } else if (k === 'Enter') { checkAnswer(); } else if (k === 'c' || k === 'C' || k === 'Escape') { pressKey('C'); } } function openStats() { game.active = false; clearInterval(dementor.interval); document.getElementById('dementor-overlay').style.display = 'none'; renderStats(); document.getElementById('stats-modal').classList.add('visible'); } function closeStats() { document.getElementById('stats-modal').classList.remove('visible'); document.getElementById('dementor-overlay').style.display = 'block'; game.active = true; startDementor(true); } function renderStats() { const statsList = document.getElementById('stats-list'); const levelData = CURRICULUM[state.levelIndex]; const accuracy = state.totalQuestions === 0 ? 0 : (state.correctAnswers / state.totalQuestions) * 100; const fastestTime = state.fastestTime === null ? 'N/A' : `${(state.fastestTime / 1000).toFixed(2)}s`; const requiredFacts = getFactsForLevel(state.levelIndex); const masteredFactsInLevel = state.masteredFacts.filter(fact => requiredFacts.includes(fact)); const masteryPct = requiredFacts.length === 0 ? 0 : (masteredFactsInLevel.length / requiredFacts.length) * 100; const targetXP = getLevelTargetXP(); const xpPct = Math.min((state.currentLevelXP / targetXP) * 100, 100); const grade = accuracy >= 95 ? 'O' : accuracy >= 85 ? 'E' : accuracy >= 75 ? 'A' : accuracy >= 65 ? 'P' : accuracy >= 50 ? 'D' : 'T'; const troubleFacts = state.troubleList.map(fact => `${fact.n1} × ${fact.n2}`).join(''); statsList.innerHTML = `
Report Card
${state.studentName || 'Student'} • ${levelData.name}
Grade ${grade}
Accuracy
${accuracy.toFixed(1)}%
Mastery
${masteredFactsInLevel.length} / ${requiredFacts.length}
XP This Year
${state.currentLevelXP} / ${targetXP}
Total Questions
${state.totalQuestions}
Correct Answers
${state.correctAnswers}
Fastest Answer
${fastestTime}
Current Streak
${state.streak}
`; } // --- HELPERS --- function updateLetterGreeting() { const greetingEl = document.getElementById('student-greeting'); if (!greetingEl) return; const name = state.studentName && state.studentName.trim() ? state.studentName : 'Student'; greetingEl.innerHTML = `Dear ${name},`; } function changeStudentName() { Swal.fire({ title: 'Change your name?', text: 'This updates the Hogwarts registry.', icon: 'question', showCancelButton: true, confirmButtonColor: '#740001', cancelButtonColor: '#3085d6', confirmButtonText: 'Yes, change it' }).then(result => { if (!result.isConfirmed) return; Swal.fire({ title: 'Enter your new name', input: 'text', inputPlaceholder: 'New Name', showCancelButton: true, confirmButtonText: 'Save', preConfirm: (name) => { if (!name || !name.trim()) { Swal.showValidationMessage('Name cannot be empty'); } return name; } }).then(res => { if (!res.isConfirmed) return; state.studentName = res.value.trim().substring(0, 12); saveData(); updateLetterGreeting(); updateUI(); renderStats(); }); }); } function pressKey(key) { if (!game.active) return; if (key === 'C') game.currentInput = ""; else if (key === 'BS') game.currentInput = game.currentInput.slice(0, -1); else if (game.currentInput.length < 4) game.currentInput += key; document.getElementById('answer-display').innerText = game.currentInput; } function updateUI() { const levelData = CURRICULUM[state.levelIndex]; document.getElementById('level-name').innerText = levelData.name; document.getElementById('gold-display').innerText = state.gold; const targetXP = getLevelTargetXP(); document.getElementById('xp-text').innerText = `${state.currentLevelXP} / ${targetXP} XP`; const requiredFacts = getFactsForLevel(state.levelIndex); const masteredFactsInLevel = state.masteredFacts.filter(fact => requiredFacts.includes(fact)); document.getElementById('mastery-display').innerText = `Mastered: ${masteredFactsInLevel.length} / ${requiredFacts.length}`; let pct = (masteredFactsInLevel.length / requiredFacts.length) * 100; if(pct > 100) pct = 100; document.getElementById('progress-bar').style.width = `${pct}%`; updateBuffDisplay(); renderQuickConsumables(); } function startDementor(resume = false) { clearInterval(dementor.interval); if (!resume) dementor.progress = 0; dementor.timedOut = false; const difficulty = Math.max(game.currentFact.n1 || 1, game.currentFact.n2 || 1); let totalMs = 10000 + difficulty * 500; // base + more time for harder facts let tickMs = 200; if (game.timeTurnerCharges > 0) { tickMs = 400; totalMs *= 1.6; } const step = tickMs / totalMs; updateDementorVisuals(); dementor.interval = setInterval(() => { if (!game.active) return; if (dementor.progress < 1) { dementor.progress += step; updateDementorVisuals(); } else if (!dementor.timedOut) { dementor.timedOut = true; handleDementorTimeout(); } }, tickMs); } function updateDementorVisuals() { const overlay = document.getElementById('dementor-overlay'); const p = dementor.progress > 1 ? 1 : dementor.progress; const opacity = 0.12 + (p * 0.6); // darker as time passes const size = 18 + (p * 82); // spread further toward center over time, nearly full cover const darkness = 0.4 + (p * 0.55); const vignette = 0.2 + (p * 0.32); const blur = 0 + (p * 0.8); const glow = Math.min(2.0, 0.5 + (p * 1.5)); // stronger glow as it darkens overlay.style.opacity = opacity; overlay.style.setProperty('--circle-size', `${size}%`); overlay.style.setProperty('--circle-strength', darkness.toFixed(2)); overlay.style.setProperty('--circle-blur', `${blur}px`); overlay.style.setProperty('--vignette-strength', vignette.toFixed(2)); overlay.style.background = buildDementorBackground(p, size, darkness, vignette); 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() { clearInterval(dementor.interval); dementor.progress = 0; const overlay = document.getElementById('dementor-overlay'); overlay.style.opacity = 0; overlay.style.setProperty('--circle-size', '12%'); overlay.style.setProperty('--circle-strength', '0.42'); overlay.style.setProperty('--circle-blur', '0px'); overlay.style.setProperty('--vignette-strength', '0.18'); overlay.style.background = 'transparent'; 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) { const circles = [ { pos: '0% 12%', s: 1.0 }, { pos: '100% 12%', s: 1.0 }, { pos: '0% 88%', s: 1.0 }, { pos: '100% 88%', s: 1.0 }, { pos: '0% 50%', s: 1.1 }, { pos: '100% 50%', s: 1.1 }, { pos: '50% 0%', s: 1.05 }, { pos: '50% 100%', s: 1.05 } ]; const activeCount = Math.max(1, Math.min(circles.length, Math.floor(progress * circles.length) + 1)); const gradients = []; for (let i = 0; i < activeCount; i++) { const c = circles[i]; gradients.push( `radial-gradient(circle at ${c.pos}, rgba(0,0,0,${(strength * c.s).toFixed(2)}) 0%, rgba(0,0,0,${(strength * c.s).toFixed(2)}) ${Math.max(size * 0.55, 6)}%, transparent ${size}%)` ); } gradients.push(`radial-gradient(circle at 50% 50%, transparent 60%, rgba(0,0,0,${(vignette * 0.35).toFixed(2)}) 78%, rgba(0,0,0,${vignette.toFixed(2)}) 92%, rgba(0,0,0,${(vignette * 1.05).toFixed(2)}) 100%)`); return gradients.join(','); } function castPatronus() { clearInterval(dementor.interval); const overlay = document.getElementById('dementor-overlay'); overlay.classList.add('patronus-flash'); setTimeout(() => { resetDementor(); overlay.classList.remove('patronus-flash'); }, 500); } function addToTroubleList(n1, n2) { const [s, b] = n1 < n2 ? [n1, n2] : [n2, n1]; const id = `${s}x${b}`; if (!state.troubleList.some(t => t.id === id)) state.troubleList.push({ id, n1: s, n2: b }); } function removeFromTroubleList(n1, n2) { const [s, b] = n1 < n2 ? [n1, n2] : [n2, n1]; const id = `${s}x${b}`; state.troubleList = state.troubleList.filter(t => t.id !== id); } function drawHintGrid(rows, cols) { const grid = document.getElementById('hint-grid'); grid.innerHTML = ""; grid.classList.toggle('has-stars', false); if (!rows || !cols) return; grid.style.gridTemplateColumns = `repeat(${rows}, 1fr)`; // flip to spread horizontally first for (let i = 0; i < (rows * cols); i++) { const dot = document.createElement('div'); dot.className = 'star-dot'; grid.appendChild(dot); } grid.classList.add('has-stars'); } function applyPenalty(amount) { if (!amount) return; state.currentLevelXP = Math.max(0, state.currentLevelXP - amount); } 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; const penalty = game.attempts >= 3 || timedOut ? 10 : (game.attempts === 2 ? 5 : 0); 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 (penalty > 0) applyPenalty(penalty); if (game.attempts >= 3) { revealAnswerAndAdvance(timedOut ? 2000 : 0, timedOut, penalty); return; } if (game.attempts === 2) { feedback.style.color = '#ffb0b0'; feedback.innerText = `Count the stars... (-${penalty} XP)`; drawHintGrid(game.currentFact.n1, game.currentFact.n2); } else { feedback.style.color = '#ffb0b0'; feedback.innerText = "Try again!"; } saveData(); updateUI(); } function revealAnswerAndAdvance(delayMs = 0, timedOut = false, penalty = 0) { 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}.${penalty ? ` (-${penalty} XP)` : ''} 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); const targetXP = levelData.targetXP; const queued = new Set(state.focusQueue); const unmastered = () => requiredFacts.filter(f => !state.masteredFacts.includes(f)); while (state.currentLevelXP >= targetXP && unmastered().length > 0) { state.currentLevelXP -= targetXP; const available = unmastered().filter(f => !queued.has(f)); if (available.length === 0) break; const troubleIds = state.troubleList.map(t => `${Math.min(t.n1, t.n2)}x${Math.max(t.n1, t.n2)}`); const troubleCandidate = available.find(id => troubleIds.includes(id)); const pickedId = troubleCandidate || available[Math.floor(Math.random() * available.length)]; state.focusQueue.push(pickedId); queued.add(pickedId); } } function renderPet() { const perch = document.getElementById('pet-perch'); if (!perch) return; perch.innerHTML = ""; if (!state.activePet) return; const pet = document.createElement('div'); pet.className = `pet-icon ${state.activePet}`; pet.innerText = state.activePetIcon || '🐾'; perch.appendChild(pet); } function saveData() { localStorage.setItem('arithmancyDataV17', JSON.stringify(state)); } function loadData() { const saved = localStorage.getItem('arithmancyDataV17'); if (saved) { const parsed = JSON.parse(saved); if (parsed.masteredFacts && Array.isArray(parsed.masteredFacts)) { state = { ...state, ...parsed }; } } } function openIntro() { document.getElementById('intro-modal').classList.add('visible'); document.getElementById('dont-show-again').checked = (localStorage.getItem('arithmancySkipIntro') === 'true'); game.active = false; clearInterval(dementor.interval); } function closeIntro() { document.getElementById('intro-modal').classList.remove('visible'); const checkbox = document.getElementById('dont-show-again'); if(checkbox.checked) localStorage.setItem('arithmancySkipIntro', 'true'); else localStorage.removeItem('arithmancySkipIntro'); if(game.currentFact.ans === 0) startGame(); else { game.active = true; startDementor(true); } } window.addEventListener('beforeunload', function (e) { e.preventDefault(); e.returnValue = ''; }); document.addEventListener('DOMContentLoaded', init); // --- GOLDEN SNITCH EVENT --- function scheduleSnitch() { if (snitchTimer) clearTimeout(snitchTimer); const delay = 45000 + Math.random() * 30000; // 45–75s snitchTimer = setTimeout(spawnSnitch, delay); } function spawnSnitch() { const layer = document.getElementById('snitch-layer'); if (!layer || document.querySelector('.flying-snitch')) { 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; snitch.classList.toggle('from-right', !fromLeft); snitch.style.top = `${10 + Math.random() * 70}%`; snitch.addEventListener('click', () => claimSnitch(snitch)); layer.appendChild(snitch); setTimeout(() => { snitch.remove(); scheduleSnitch(); }, 8000); } function claimSnitch(el) { el.remove(); const reward = 50; state.gold += reward; saveData(); updateUI(); const feedback = document.getElementById('feedback-msg'); feedback.style.color = '#d4af37'; feedback.innerText = `Caught the Snitch! +${reward}G`; scheduleSnitch(); }