commit 00771208802467e0e07e0423bf386f6995e4d76d Author: chris Date: Thu Nov 20 08:24:27 2025 -0500 Initial commit diff --git a/app.js b/app.js new file mode 100644 index 0000000..4c29da9 --- /dev/null +++ b/app.js @@ -0,0 +1,832 @@ +// --- 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', `

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 = `

${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'); + + 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 = ` +
+
+
+
+ +
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(); + } + + 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 = 10 + (p * 10); + 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', '10px'); + 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; // 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(); + } diff --git a/index.html b/index.html new file mode 100644 index 0000000..f0e5ec7 --- /dev/null +++ b/index.html @@ -0,0 +1,117 @@ + + + + + + Hogwarts Arithmancy v15 + + + + + + + + + + + + +
+
+ +
+
+ +
+ 0 G +
+ + +
+ +
+ Year 1 +
+ 0 / 150 XP +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + + + + + + + + + + + + +
+ + +
+
+ + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..3603a49 --- /dev/null +++ b/style.css @@ -0,0 +1,469 @@ +:root { + --parchment: #f8f5e6; + --ink: #2c2c2c; + --gold: #d4af37; + --burgundy: #740001; + --bg-dark: #121212; + --focus-glow: 0; + } + + * { box-sizing: border-box; user-select: none; -webkit-tap-highlight-color: transparent; } + + body { + background-color: var(--bg-dark); + background-image: + radial-gradient(circle at 20% 20%, rgba(212, 175, 55, 0.08), transparent 22%), + radial-gradient(circle at 80% 10%, rgba(116, 0, 1, 0.07), transparent 20%), + radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.06), transparent 25%); + font-family: 'MedievalSharp', cursive; + display: flex; justify-content: center; align-items: center; + min-height: 100vh; margin: 0; color: var(--ink); overflow: hidden; + transition: background 0.5s, background-image 0.5s; + } + + .bg-default { + background-color: #0f0f14; + background-image: + radial-gradient(circle at 20% 20%, rgba(212, 175, 55, 0.08), transparent 22%), + radial-gradient(circle at 80% 10%, rgba(116, 0, 1, 0.07), transparent 20%), + radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.06), transparent 25%); + } + + /* ==================== SKINS ==================== */ + + /* 1. DEFAULT (PARCHMENT) */ + .card-container.skin-default { + background: + linear-gradient(180deg, rgba(212,175,55,0.06), rgba(116,0,1,0.05)), + repeating-linear-gradient(0deg, rgba(0,0,0,0.02), rgba(0,0,0,0.02) 32px, transparent 32px, transparent 64px), + var(--parchment); + } + + /* 2. GRYFFINDOR */ + .card-container.skin-gryffindor { + background: + linear-gradient(135deg, rgba(180, 30, 30, 0.7), rgba(90, 0, 0, 0.6)), + repeating-linear-gradient(45deg, rgba(212, 175, 55, 0.25) 0 8px, transparent 8px 24px), + #2a0101; + color: #ffeccc; border-color: #d4af37; box-shadow: 0 0 38px rgba(180, 30, 30, 0.8); + } + .card-container.skin-gryffindor #equation-display, + .card-container.skin-gryffindor #answer-display { color: #ffeccc; border-color: #d4af37; text-shadow: 0 0 10px rgba(0,0,0,0.5); } + .card-container.skin-gryffindor .header-info, + .card-container.skin-gryffindor .shop-btn, + .card-container.skin-gryffindor .help-btn { color: #ffeccc; border-color: #d4af37; } + .card-container.skin-gryffindor .key-btn { + background: rgba(180, 30, 30, 0.25); border-color: #d4af37; color: #ffeccc; + } + .card-container.skin-gryffindor .key-btn:active { background: #d4af37; color: #3b0505; } + + /* 3. SLYTHERIN */ + .card-container.skin-slytherin { + background: + linear-gradient(160deg, rgba(38, 84, 66, 0.8), rgba(14, 34, 26, 0.95)), + repeating-linear-gradient(135deg, rgba(192, 192, 192, 0.25) 0 10px, transparent 10px 28px), + radial-gradient(circle at 25% 20%, rgba(90, 160, 130, 0.32), transparent 38%), + #0a1712; + color: #d6ffea; border-color: #9ad5b9; box-shadow: 0 0 42px rgba(60, 150, 110, 0.95); + } + .card-container.skin-slytherin #equation-display, + .card-container.skin-slytherin #answer-display { color: #e5fff4; border-color: #9ad5b9; text-shadow: 0 0 10px rgba(0,0,0,0.7); } + .card-container.skin-slytherin .header-info, + .card-container.skin-slytherin .shop-btn, + .card-container.skin-slytherin .help-btn { color: #d6ffea; border-color: #9ad5b9; } + .card-container.skin-slytherin .key-btn { + background: rgba(0, 0, 0, 0.35); border-color: #9ad5b9; color: #e5fff4; box-shadow: inset 0 1px 0 rgba(255,255,255,0.09); + } + .card-container.skin-slytherin .key-btn:active { background: #9ad5b9; color: #072419; } + + /* 4. RAVENCLAW */ + .card-container.skin-ravenclaw { + background: + linear-gradient(150deg, rgba(20, 60, 120, 0.85), rgba(8, 18, 44, 0.95)), + repeating-linear-gradient(45deg, rgba(184, 115, 51, 0.3) 0 10px, transparent 10px 24px), + radial-gradient(circle at 30% 25%, rgba(70, 130, 200, 0.28), transparent 40%), + #0b1630; + color: #e9f2ff; border-color: #c98640; box-shadow: 0 0 42px rgba(70, 120, 200, 0.95); + } + .card-container.skin-ravenclaw #equation-display, + .card-container.skin-ravenclaw #answer-display { color: #f4f8ff; border-color: #c98640; text-shadow: 0 0 10px rgba(0,0,0,0.6); } + .card-container.skin-ravenclaw .header-info, .card-container.skin-ravenclaw .shop-btn { color: #dce9ff; border-color: #c98640; } + .card-container.skin-ravenclaw .key-btn { background: rgba(255,255,255,0.06); border-color: #c98640; color: #f7fbff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.1); } + .card-container.skin-ravenclaw .key-btn:active { background: #c98640; color: #1a0d02; } + + /* 5. HUFFLEPUFF */ + .card-container.skin-hufflepuff { + background: + radial-gradient(circle at 20% 20%, rgba(255, 215, 0, 0.3), transparent 35%), + radial-gradient(circle at 80% 70%, rgba(255, 180, 0, 0.26), transparent 38%), + repeating-linear-gradient(90deg, rgba(0,0,0,0.06) 0 6px, transparent 6px 18px), + #1d1a14; + color: #fff4c2; border-color: #ffcd35; box-shadow: 0 0 40px rgba(255, 195, 0, 0.7); + } + .card-container.skin-hufflepuff #equation-display, + .card-container.skin-hufflepuff #answer-display { color: #fff4c2; border-color: #ffcd35; text-shadow: 0 0 10px rgba(0,0,0,0.4); } + .card-container.skin-hufflepuff .header-info, .card-container.skin-hufflepuff .shop-btn { color: #fff0b0; border-color: #ffcd35; } + .card-container.skin-hufflepuff .key-btn { border-color: #ffcd35; color: #1d1a14; background: rgba(255, 215, 0, 0.25); } + .card-container.skin-hufflepuff .key-btn:active { background: #ffcd35; color: #1d1a14; } + + /* ==================== END SKINS ==================== */ + + + /* --- TOUCH TRAILS --- */ + .particle { position: absolute; pointer-events: none; width: 12px; height: 12px; border-radius: 50%; animation: fadeOut 0.8s forwards; z-index: 9999; } + @keyframes fadeOut { 0% { transform: scale(1); opacity: 1; } 100% { transform: scale(0); opacity: 0; } } + .trail-sparks { background: #ffd700; box-shadow: 0 0 8px #ffd700; } + .trail-ice { background: #e0ffff; box-shadow: 0 0 8px #00ffff; } + .trail-slime { background: #39ff14; box-shadow: 0 0 8px #39ff14; } + + /* --- DEMENTOR --- */ + #dementor-overlay { + position: fixed; + left: var(--overlay-left, 0); + top: var(--overlay-top, 0); + width: var(--overlay-width, 100vw); + height: var(--overlay-height, 100vh); + pointer-events: none; + z-index: 120; + --circle-size: 12%; + --circle-strength: 0.25; + --circle-blur: 10px; + --vignette-strength: 0.35; + background: + radial-gradient(circle at 0% 12%, rgba(0,0,0,var(--circle-strength)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 100% 12%, rgba(0,0,0,var(--circle-strength)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 0% 88%, rgba(0,0,0,var(--circle-strength)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 100% 88%, rgba(0,0,0,var(--circle-strength)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 0% 50%, rgba(0,0,0,calc(var(--circle-strength) * 1.1)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 100% 50%, rgba(0,0,0,calc(var(--circle-strength) * 1.1)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 50% 0%, rgba(0,0,0,calc(var(--circle-strength) * 1.05)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 50% 100%, rgba(0,0,0,calc(var(--circle-strength) * 1.05)) 0%, transparent var(--circle-size)), + radial-gradient(circle at 50% 50%, transparent 35%, rgba(0,0,0,var(--vignette-strength)) 78%, rgba(0,0,0,calc(var(--vignette-strength) * 1.2)) 100%); + opacity: 0; + filter: blur(var(--circle-blur)); + mix-blend-mode: multiply; + transition: opacity 0.4s ease, filter 0.4s ease; + } + .patronus-flash { background: radial-gradient(circle, white 0%, transparent 100%) !important; opacity: 0.8 !important; } + + /* Snitch layer */ + #snitch-layer { + position: fixed; inset: 0; pointer-events: none; z-index: 880; + overflow: hidden; + } + .flying-snitch { + position: absolute; + left: -60px; + width: 32px; height: 20px; + background: radial-gradient(circle at 30% 50%, #fff89a 0%, #ffd700 60%, #c19a00 100%); + border-radius: 50% 50% 50% 50%; + box-shadow: 0 0 12px rgba(255,215,0,0.7), 0 0 24px rgba(255,215,0,0.4); + animation: snitchFlyRight 5s linear forwards, snitchGlow 1.2s ease-in-out infinite alternate; + pointer-events: auto; + } + .flying-snitch.from-right { + left: auto; + right: -60px; + animation: snitchFlyLeft 5s linear forwards, snitchGlow 1.2s ease-in-out infinite alternate; + } + .flying-snitch::before, .flying-snitch::after { + content: ""; + position: absolute; + top: 4px; + width: 18px; height: 12px; + background: linear-gradient(120deg, rgba(255,255,255,0.9), rgba(255,215,0,0.6)); + border-radius: 12px 12px 2px 2px; + box-shadow: 0 0 8px rgba(255,255,255,0.5); + } + .flying-snitch::before { left: -16px; transform: rotate(-12deg); } + .flying-snitch::after { right: -16px; transform: scaleX(-1) rotate(-12deg); } + @keyframes snitchFlyRight { + 0% { transform: translateX(0) translateY(0) rotate(0deg); } + 50% { transform: translateX(60vw) translateY(-6vh) rotate(8deg); } + 100% { transform: translateX(120vw) translateY(4vh) rotate(-6deg); } + } + @keyframes snitchFlyLeft { + 0% { transform: translateX(0) translateY(0) rotate(0deg); } + 50% { transform: translateX(-60vw) translateY(6vh) rotate(-8deg); } + 100% { transform: translateX(-120vw) translateY(-4vh) rotate(6deg); } + } + @keyframes snitchGlow { + from { box-shadow: 0 0 10px rgba(255,215,0,0.5), 0 0 20px rgba(255,215,0,0.3); } + to { box-shadow: 0 0 16px rgba(255,215,0,0.9), 0 0 28px rgba(255,215,0,0.6); } + } + + /* --- UI --- */ + .card-container { + width: 95%; max-width: 450px; padding: 15px; border-radius: 10px; + box-shadow: 0 0 40px var(--gold); text-align: center; + border: 4px double var(--gold); position: relative; z-index: 100; + display: flex; flex-direction: column; transition: background 0.3s, border-color 0.3s; + } + + .header-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-family: 'Cinzel', serif; color: var(--burgundy); font-size: 0.9rem; font-weight: bold; } + #xp-text { + display: inline-block; + padding: 4px 10px; + background: rgba(0,0,0,0.35); + color: #fcefd6; + border: 1px solid rgba(212,175,55,0.6); + border-radius: 12px; + text-shadow: 0 1px 4px rgba(0,0,0,0.6); + box-shadow: 0 0 6px rgba(0,0,0,0.25), inset 0 0 8px rgba(255, 225, 140, 0.15); + } + + .currency-badge { + background: var(--burgundy); color: var(--gold); padding: 5px 10px; + border-radius: 15px; display: flex; align-items: center; gap: 5px; + border: 1px solid var(--gold); font-size: 0.9rem; + } + + .shop-btn, .help-btn { + background: var(--gold); border: 2px solid var(--burgundy); color: var(--burgundy); + width: 40px; height: 40px; border-radius: 50%; font-weight: bold; cursor: pointer; + display: flex; align-items: center; justify-content: center; font-size: 1.5rem; + box-shadow: 0 2px 5px rgba(0,0,0,0.3); + } + .help-btn.active { + background: #1a472a; + color: #f7f7f7; + border-color: #d4af37; + } + + .progress-container { width: 100%; height: 10px; background: rgba(0,0,0,0.1); border: 1px solid var(--burgundy); margin-bottom: 10px; border-radius: 6px; overflow: hidden; } + .progress-fill { height: 100%; background: linear-gradient(90deg, var(--burgundy), var(--gold)); width: 0%; transition: width 0.5s; } + + .equation { font-size: 3.5rem; margin: 5px 0; min-height: 60px; } + #answer-display { font-size: 2.5rem; height: 50px; border-bottom: 3px solid var(--burgundy); width: 80%; margin: 0 auto 10px; color: var(--burgundy); display: flex; align-items: center; justify-content: center; } + + .equation, #answer-display { + text-shadow: + 0 0 calc(3px + 10px * var(--focus-glow)) rgba(255, 235, 180, calc(0.25 + 0.5 * var(--focus-glow))), + 0 0 calc(6px + 14px * var(--focus-glow)) rgba(255, 215, 80, calc(0.15 + 0.35 * var(--focus-glow))); + } + + #hint-grid { display: grid; justify-content: center; gap: 4px; margin: 5px auto; max-width: 200px; } + .star-dot { width: 8px; height: 8px; background: var(--gold); border-radius: 50%; box-shadow: 0 0 5px var(--gold); animation: twinkle 2s infinite; } + @keyframes twinkle { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } + + .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 5px; } + .key-btn { background: rgba(116, 0, 1, 0.05); border: 1px solid var(--gold); color: var(--ink); font-family: 'Cinzel', serif; font-size: 1.5rem; padding: 15px 0; border-radius: 5px; cursor: pointer; + text-shadow: 0 0 calc(2px + 8px * var(--focus-glow)) rgba(255, 225, 140, calc(0.2 + 0.4 * var(--focus-glow))); + box-shadow: 0 0 calc(2px + 8px * var(--focus-glow)) rgba(255, 195, 50, calc(0.12 + 0.3 * var(--focus-glow))); + } + .key-btn:active { background: var(--gold); } + .key-cast { grid-column: span 3; background: var(--burgundy); color: var(--gold); font-weight: bold; } + .card-container .key-btn.key-clear { + background: #ffd966; + color: #2c2c2c; + border-color: #b8860b; + font-weight: bold; + } + .card-container .key-btn.key-clear:active { background: #f1c232; } + + .feedback { height: 20px; margin: 5px 0; font-weight: bold; font-size: 0.9rem; } + #pet-perch { + height: 40px; + display: flex; justify-content: center; align-items: center; + margin-top: 6px; + } + .pet-icon { + font-size: 1.8rem; + filter: drop-shadow(0 0 6px rgba(0,0,0,0.3)); + animation: petFloat 3s ease-in-out infinite; + } + .pet-icon.pet-owl { text-shadow: 0 0 6px rgba(255,255,255,0.5); } + .pet-icon.pet-cat { text-shadow: 0 0 6px rgba(255,182,193,0.5); } + .pet-icon.pet-rat { text-shadow: 0 0 6px rgba(200,200,200,0.4); } + .pet-icon.pet-toad { text-shadow: 0 0 6px rgba(144,238,144,0.6); } + .pet-icon.pet-ferret { text-shadow: 0 0 6px rgba(210,180,140,0.6); } + @keyframes petFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } } + + /* --- MODALS --- */ + .modal-overlay { + position: fixed; top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.9); z-index: 2000; + display: flex; justify-content: center; align-items: center; + visibility: hidden; opacity: 0; transition: 0.3s; + } + .modal-overlay.visible { visibility: visible; opacity: 1; } + + .shop-panel, .letter { + background: var(--parchment); width: 95%; max-width: 500px; height: 85vh; + padding: 20px; border-radius: 5px; box-shadow: 0 0 50px #000; + border: 4px double var(--gold); + display: flex; flex-direction: column; + } + .shop-scroll-area { + overflow-y: auto; + flex-grow: 1; + } + + /* LETTER STYLING */ + .letter { height: auto; max-height: 90vh; transform: rotate(-1deg); font-family: 'MedievalSharp', cursive; position: relative; } + .hogwarts-crest { text-align: center; margin-bottom: 15px; border-bottom: 2px solid #444; padding-bottom: 10px; } + .letter h2 { font-family: 'Cinzel', serif; margin: 0; color: var(--burgundy); font-size: 1.5rem; } + .letter p { font-size: 1rem; line-height: 1.5; margin-bottom: 12px; color: #222; } + .solemn-btn { width: 100%; padding: 15px; margin-top: 15px; background: var(--burgundy); color: var(--gold); border: 2px solid var(--gold); font-family: 'Cinzel'; font-size: 1.1rem; cursor: pointer; } + .checkbox-row { display: flex; align-items: center; justify-content: center; margin-top: 15px; font-size: 0.8rem; gap: 8px; color: #555; } + .checkbox-row input { width: 18px; height: 18px; accent-color: var(--burgundy); } + + /* SHOP STYLING */ + .shop-header { text-align: center; font-family: 'Cinzel'; color: var(--burgundy); border-bottom: 2px solid var(--gold); padding-bottom: 10px; margin-bottom: 10px; } + .shop-category { font-weight: bold; margin-top: 15px; color: #555; text-transform: uppercase; font-size: 0.8rem; border-bottom: 1px solid #ccc; } + .shop-item { display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.05); margin-bottom: 8px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; } + .item-details h4 { margin: 0; color: var(--burgundy); font-family: 'Cinzel'; font-size: 1rem; } + .item-details p { margin: 2px 0 0; font-size: 0.75rem; color: #444; } + + .shop-actions { display: flex; flex-direction: column; gap: 5px; align-items: flex-end; } + + .buy-btn { + background: var(--burgundy); color: var(--gold); border: none; + padding: 8px 12px; cursor: pointer; font-family: 'Cinzel'; + font-size: 0.85rem; min-width: 70px; border-radius: 4px; + } + .buy-btn:disabled { background: #ccc; color: #666; } + + .use-btn { background: #1a472a; color: #fff; } + .unequip-btn { background: #444; color: #fff; } + + .qty-badge { font-size: 0.75rem; color: #555; margin-bottom: 2px; } + + .close-btn { flex-shrink: 0; padding: 15px; background: #333; color: white; border: none; cursor: pointer; font-family: 'Cinzel'; font-size: 1.1rem; } + + /* REPORT CARD */ + .report-card { + background: linear-gradient(135deg, rgba(212, 175, 55, 0.05), rgba(116, 0, 1, 0.05)), + repeating-linear-gradient(#e8debf 0, #e8debf 32px, #f8f5e6 32px, #f8f5e6 34px); + border: 3px double var(--gold); + border-radius: 8px; + padding: 16px; + box-shadow: 0 6px 20px rgba(0,0,0,0.35); + color: #2b2111; + } + .report-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } + .report-title-block { display: flex; flex-direction: column; gap: 4px; } + .report-title-row { display: flex; align-items: center; gap: 8px; } + .report-icon { font-size: 1.3rem; } + .report-title { font-family: 'Cinzel', serif; font-size: 1.3rem; color: var(--burgundy); } + .report-subtitle { font-size: 0.9rem; color: #444; } + .report-actions { display: flex; align-items: center; gap: 8px; } + .grade-badge { + background: var(--burgundy); color: var(--gold); padding: 8px 12px; + border-radius: 20px; border: 2px solid var(--gold); font-weight: bold; font-family: 'Cinzel'; box-shadow: 0 0 12px rgba(0,0,0,0.3); + } + .report-btn { + background: #333; color: #fff; border: 1px solid var(--gold); + padding: 6px 10px; border-radius: 6px; cursor: pointer; + font-family: 'Cinzel', serif; font-size: 0.8rem; + } + .report-btn:hover { background: var(--burgundy); color: var(--gold); } + .grade-grid { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; } + .grade-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: rgba(255,255,255,0.4); + border: 1px solid rgba(0,0,0,0.05); + border-radius: 6px; + } + .grade-label { font-weight: bold; color: #2c2c2c; letter-spacing: 0.5px; } + .grade-value { font-family: 'Cinzel', serif; color: var(--burgundy); } + .grade-meter { + grid-column: 1 / span 2; + height: 8px; background: rgba(0,0,0,0.08); border-radius: 999px; overflow: hidden; + border: 1px solid rgba(0,0,0,0.05); + } + .grade-meter span { + display: block; height: 100%; background: linear-gradient(90deg, var(--burgundy), var(--gold)); + } + .metric-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 8px; + margin-bottom: 12px; + } + .metric-card { + background: rgba(255,255,255,0.5); + border: 1px solid rgba(0,0,0,0.05); + border-radius: 6px; + padding: 10px; + box-shadow: inset 0 1px 0 rgba(255,255,255,0.6); + } + .metric-label { font-size: 0.8rem; color: #555; } + .metric-value { font-size: 1.2rem; font-weight: bold; color: var(--burgundy); font-family: 'Cinzel', serif; } + .report-footer { border-top: 1px dashed rgba(0,0,0,0.2); padding-top: 10px; } + .report-subsection-title { font-weight: bold; color: #2c2c2c; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.8px; font-size: 0.8rem; } + .trouble-chips { display: flex; flex-wrap: wrap; gap: 6px; } + .trouble-chip { + background: rgba(116, 0, 1, 0.1); + color: var(--burgundy); + border: 1px solid rgba(116, 0, 1, 0.2); + padding: 6px 10px; + border-radius: 12px; + font-size: 0.85rem; + } + .report-note { font-size: 0.9rem; color: #444; font-style: italic; } + + /* BUFF INDICATOR */ + #buff-bar { display: flex; justify-content: center; gap: 10px; margin-bottom: 5px; height: 20px; } + .buff-tag { background: #333; color: gold; font-size: 0.75rem; padding: 2px 8px; border-radius: 10px; animation: popIn 0.3s; border: 1px solid gold; } + @keyframes popIn { 0% { transform: scale(0); } 100% { transform: scale(1); } } + + @keyframes pop-out { + 0% { transform: scale(0.8); opacity: 0; } + 50% { transform: scale(1.2); opacity: 1; } + 100% { transform: scale(1.5); opacity: 0; transform: translateY(-80px); } + } + + .xp-gain-pop { + position: absolute; + top: 50px; + right: 20px; + font-size: 1.8rem; + font-weight: bold; + color: var(--gold); + text-shadow: 0 0 10px black; + animation: pop-out 1s ease-out forwards; + pointer-events: none; + } + + @keyframes level-up-flash { + 0%, 100% { transform: scale(1); color: var(--gold); text-shadow: 0 0 10px var(--gold); } + 50% { transform: scale(1.2); color: white; text-shadow: 0 0 20px white; } + } + + .level-up-animation { + animation: level-up-flash 1s ease-in-out; + } + + @keyframes shake { 0% { transform: translate(2px, 1px) rotate(0deg); } 10% { transform: translate(-1px, -2px) rotate(-1deg); } 20% { transform: translate(-3px, 0px) rotate(1deg); } 30% { transform: translate(3px, 2px) rotate(0deg); } 40% { transform: translate(1px, -1px) rotate(1deg); } 50% { transform: translate(-1px, 2px) rotate(-1deg); } 60% { transform: translate(-3px, 1px) rotate(0deg); } 70% { transform: translate(3px, 1px) rotate(-1deg); } 80% { transform: translate(-1px, -1px) rotate(1deg); } 90% { transform: translate(1px, 2px) rotate(0deg); } 100% { transform: translate(1px, -2px) rotate(-1deg); } } + .apply-shake { animation: shake 0.5s; border-color: #740001 !important; box-shadow: 0 0 20px #740001 !important; } + + /* --- SWEETALERT THEME --- */ + .hogwarts-popup { + background: var(--parchment) !important; + border: 4px double var(--gold) !important; + border-radius: 10px !important; + } + + .hogwarts-title { + font-family: 'Cinzel', serif !important; + color: var(--burgundy) !important; + } + + .hogwarts-button { + background-color: var(--burgundy) !important; + color: var(--gold) !important; + border: 2px solid var(--gold) !important; + box-shadow: none !important; + } + .hogwarts-button:focus { + box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.5) !important; + } + /* Ensure dialogs appear above modals */ + .swal2-container { + z-index: 3000 !important; + }