// --- 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)
{ 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" },
// 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: []
};
let game = {
active: false,
currentFact: { n1: 0, n2: 0, ans: 0 },
currentInput: "",
attempts: 0,
startTime: 0,
timeTurnerCharges: 0
};
let dementor = { interval: null, progress: 0 };
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);
// 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.startTime = Date.now();
document.getElementById('answer-display').innerText = "";
document.getElementById('feedback-msg').innerText = "";
document.getElementById('hint-grid').innerHTML = "";
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;
if (state.focusQueue.length > 0) {
const factId = state.focusQueue.shift();
const parts = factId.split('x').map(Number);
[n1, n2] = parts;
} else {
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];
}
}
game.currentFact = { n1, n2, ans: 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 = 'green';
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 {
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 = '#740001';
feedback.innerText = `Answer is ${game.currentFact.ans}.`;
} else {
feedback.style.color = '#740001';
feedback.innerText = "Count the stars...";
drawHintGrid(game.currentFact.n1, game.currentFact.n2);
}
}
}
// --- 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 = `${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');
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();
closeShop();
}
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);
}
}
// --- VISUALS ---
function applyCosmetics() {
const card = document.getElementById('main-card');
const allowedSkins = ['skin-default','skin-gryffindor','skin-slytherin','skin-ravenclaw','skin-hufflepuff'];
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 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 = `
${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();
}
function startDementor(resume = false) {
clearInterval(dementor.interval);
if (!resume) dementor.progress = 0;
let speed = 250;
const step = 0.02;
if (game.timeTurnerCharges > 0) speed = 600;
dementor.interval = setInterval(() => {
if (!game.active) return;
if (dementor.progress < 1) {
dementor.progress += step; updateDementorVisuals();
}
}, speed);
}
function updateDementorVisuals() {
const overlay = document.getElementById('dementor-overlay');
const p = dementor.progress > 1 ? 1 : dementor.progress;
const opacity = 0.08 + (p * 0.35);
const size = 14 + (p * 22); // start at edges, creep inward
const darkness = 0.28 + (p * 0.4);
const vignette = 0.18 + (p * 0.22);
const blur = 0 + (p * 1.2);
const glow = 0.15 + (p * 0.75);
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);
document.documentElement.style.setProperty('--focus-glow', glow.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.35');
overlay.style.setProperty('--circle-blur', '0px');
overlay.style.setProperty('--vignette-strength', '0.18');
overlay.style.background = 'transparent';
document.documentElement.style.setProperty('--focus-glow', '0');
saveData();
}
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.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
for (let i = 0; i < (rows * cols); i++) {
const dot = document.createElement('div'); dot.className = 'star-dot'; grid.appendChild(dot);
}
}
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 = 20000 + Math.random() * 20000; // 20–40s
snitchTimer = setTimeout(spawnSnitch, delay);
}
function spawnSnitch() {
const layer = document.getElementById('snitch-layer');
if (!layer || document.querySelector('.flying-snitch')) {
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();
}