flashCards/app.js

833 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- 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();
window.addEventListener('resize', positionDementorOverlay);
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', `<p>We await your owl, ${state.studentName}.</p>`);
} 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', `<p>We await your owl, ${state.studentName}.</p>`);
});
}
} else {
updateLetterGreeting();
document.querySelector('.letter .signature').insertAdjacentHTML('beforebegin', `<p>We await your owl, ${state.studentName}.</p>`);
}
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 = `<div class="shop-actions">`;
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 += `<button class="buy-btn unequip-btn" onclick="unequipItem('${item.id}')">Unequip</button>`;
} else {
actionHtml += `<button class="buy-btn equip-btn" onclick="equipItem('${item.id}')">Equip</button>`;
}
} else {
actionHtml += `<button class="buy-btn" onclick="buyItem('${item.id}')" ${state.gold < item.price ? 'disabled' : ''}>${item.price}G</button>`;
}
} else {
if (qty > 0) {
actionHtml += `<div class="qty-badge">Owned: ${qty}</div>`;
actionHtml += `<button class="buy-btn use-btn" onclick="useConsumable('${item.id}')">Use</button>`;
actionHtml += `<button class="buy-btn" onclick="buyItem('${item.id}')" ${state.gold < item.price ? 'disabled' : ''} style="margin-top:5px; font-size:0.7rem;">Buy (+1)</button>`;
} else {
actionHtml += `<button class="buy-btn" onclick="buyItem('${item.id}')" ${state.gold < item.price ? 'disabled' : ''}>${item.price}G</button>`;
}
}
actionHtml += `</div>`;
div.innerHTML = `<div class="item-details"><h4>${item.name}</h4><p>${item.desc}</p></div>${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 => `<span class="trouble-chip">${fact.n1} × ${fact.n2}</span>`).join('');
statsList.innerHTML = `
<div class="report-card">
<div class="report-heading">
<div class="report-title-block">
<div class="report-title-row">
<span class="report-icon" aria-hidden="true">📜</span>
<div class="report-title">Report Card</div>
</div>
<div class="report-subtitle">${state.studentName || 'Student'}${levelData.name}</div>
</div>
<div class="report-actions">
<div class="grade-badge">Grade ${grade}</div>
<button class="report-btn" onclick="changeStudentName()">Change Name</button>
</div>
</div>
<div class="grade-grid">
<div class="grade-row">
<div class="grade-label">Accuracy</div>
<div class="grade-value">${accuracy.toFixed(1)}%</div>
<div class="grade-meter"><span style="width:${Math.min(accuracy, 100)}%"></span></div>
</div>
<div class="grade-row">
<div class="grade-label">Mastery</div>
<div class="grade-value">${masteredFactsInLevel.length} / ${requiredFacts.length}</div>
<div class="grade-meter"><span style="width:${masteryPct}%"></span></div>
</div>
<div class="grade-row">
<div class="grade-label">XP This Year</div>
<div class="grade-value">${state.currentLevelXP} / ${targetXP}</div>
<div class="grade-meter"><span style="width:${xpPct}%"></span></div>
</div>
</div>
<div class="metric-cards">
<div class="metric-card">
<div class="metric-label">Total Questions</div>
<div class="metric-value">${state.totalQuestions}</div>
</div>
<div class="metric-card">
<div class="metric-label">Correct Answers</div>
<div class="metric-value">${state.correctAnswers}</div>
</div>
<div class="metric-card">
<div class="metric-label">Fastest Answer</div>
<div class="metric-value">${fastestTime}</div>
</div>
<div class="metric-card">
<div class="metric-label">Current Streak</div>
<div class="metric-value">${state.streak}</div>
</div>
</div>
<div class="report-footer">
<div class="report-subsection-title">Trouble Facts</div>
${
troubleFacts
? `<div class="trouble-chips">${troubleFacts}</div>`
: `<div class="report-note">Looks clear for now. Keep casting to reveal any weak spots.</div>`
}
</div>
</div>
`;
}
// --- HELPERS ---
function updateLetterGreeting() {
const greetingEl = document.getElementById('student-greeting');
if (!greetingEl) return;
const name = state.studentName && state.studentName.trim() ? state.studentName : 'Student';
greetingEl.innerHTML = `<strong>Dear ${name},</strong>`;
}
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;
positionDementorOverlay();
dementor.interval = setInterval(() => {
if (!game.active) return;
if (dementor.progress < 1) {
dementor.progress += step; updateDementorVisuals();
}
}, speed);
}
function updateDementorVisuals() {
const overlay = document.getElementById('dementor-overlay');
positionDementorOverlay();
const p = dementor.progress > 1 ? 1 : dementor.progress;
const opacity = 0.18 + (p * 0.62);
const size = 14 + (p * 24); // start at edges, creep inward
const darkness = 0.3 + (p * 0.55);
const vignette = 0.35 + (p * 0.35);
const blur = 0 + (p * 2);
const glow = 0.08 + (p * 0.92);
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));
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.25');
overlay.style.setProperty('--circle-blur', '0px');
overlay.style.setProperty('--vignette-strength', '0.35');
document.documentElement.style.setProperty('--focus-glow', '0');
saveData();
}
function positionDementorOverlay() {
const overlay = document.getElementById('dementor-overlay');
const card = document.getElementById('main-card');
if (!overlay || !card) return;
const rect = card.getBoundingClientRect();
overlay.style.setProperty('--overlay-left', `${rect.left}px`);
overlay.style.setProperty('--overlay-top', `${rect.top}px`);
overlay.style.setProperty('--overlay-width', `${rect.width}px`);
overlay.style.setProperty('--overlay-height', `${rect.height}px`);
}
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; // 2040s
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();
}