1010 lines
46 KiB
JavaScript
1010 lines
46 KiB
JavaScript
// --- CONFIG ---
|
||
const CURRICULUM = [
|
||
{ name: "Year 1", numbers: [1, 2, 5, 10], targetXP: 150 },
|
||
{ name: "Year 2", numbers: [3, 4], targetXP: 150 },
|
||
{ name: "Year 3", numbers: [9, 11], targetXP: 200 },
|
||
{ name: "Year 4", numbers: [6, 7], targetXP: 250 },
|
||
{ name: "Year 5", numbers: [8, 12], targetXP: 300 },
|
||
{ name: "N.E.W.T.s", numbers: [1,2,3,4,5,6,7,8,9,10,11,12], targetXP: 5000 }
|
||
];
|
||
|
||
const SHOP_ITEMS = [
|
||
{ id: 'auto_quill', name: "Auto-Quill", type: 'consumable', category: 'Supplies', price: 15, desc: "Writes the first digit." },
|
||
{ id: 'choc_frog', name: "Chocolate Frog", type: 'consumable', category: 'Supplies', price: 25, desc: "Clears Dementors instantly." },
|
||
{ id: 'liquid_luck', name: "Liquid Luck", type: 'consumable', category: 'Supplies', price: 40, desc: "Solves problem instantly." },
|
||
{ id: 'time_turner', name: "Time Turner", type: 'consumable', category: 'Magic Items', price: 60, desc: "Slows Dementors for 5 spells." },
|
||
{ id: 'study_spell', name: "Study Spell", type: 'consumable', category: 'Daily Specials', price: 30, desc: "+25 XP instantly (Mon/Thu).", availableDays: ['Mon','Thu'] },
|
||
{ id: 'vault_key', name: "Vault Key", type: 'consumable', category: 'Daily Specials', price: 50, desc: "+100 Gold (Sun only).", availableDays: ['Sun'] },
|
||
{ id: 'phoenix_feather', name: "Phoenix Feather", type: 'consumable', category: 'Daily Specials', price: 45, desc: "Boost streak to 5 (Tue/Fri).", availableDays: ['Tue','Fri'] },
|
||
{ id: 'shield_charm', name: "Shield Charm", type: 'consumable', category: 'Daily Specials', price: 35, desc: "Reset & slow Dementors for 2 turns (Wed/Sat).", availableDays: ['Wed','Sat'] },
|
||
{ id: 'astral_map', name: "Astral Map", type: 'consumable', category: 'Daily Specials', price: 40, desc: "Shows a star hint for this question (Sun/Tue/Thu).", availableDays: ['Sun','Tue','Thu'] },
|
||
// PETS
|
||
{ id: 'pet_owl', name: "Snowy Owl", type: 'pet', category: 'Familiars', price: 120, desc: "Faithful post owl companion.", class: 'pet-owl', icon: '🦉' },
|
||
{ id: 'pet_cat', name: "Kneazle Cat", type: 'pet', category: 'Familiars', price: 110, desc: "Clever feline lookout.", class: 'pet-cat', icon: '🐈' },
|
||
{ id: 'pet_rat', name: "Garden Rat", type: 'pet', category: 'Familiars', price: 90, desc: "Surprisingly loyal.", class: 'pet-rat', icon: '🐀' },
|
||
{ id: 'pet_toad', name: "Pad-Pad Toad", type: 'pet', category: 'Familiars', price: 80, desc: "Keeps calm in the classroom.", class: 'pet-toad', icon: '🐸' },
|
||
{ id: 'pet_ferret', name: "Mischief Ferret", type: 'pet', category: 'Familiars', price: 130, desc: "Fast distraction in duels.", class: 'pet-ferret', icon: '🦦' },
|
||
// SKINS (Hogwarts Houses + specials)
|
||
{ id: 'skin_gryffindor', name: "Gryffindor", type: 'skin', category: 'Card Skins', price: 100, desc: "Scarlet and gold bravery motif.", class: "skin-gryffindor" },
|
||
{ id: 'skin_slytherin', name: "Slytherin", type: 'skin', category: 'Card Skins', price: 100, desc: "Emerald and silver ambition motif.", class: "skin-slytherin" },
|
||
{ id: 'skin_ravenclaw', name: "Ravenclaw", type: 'skin', category: 'Card Skins', price: 100, desc: "Bronze and blue wit motif.", class: "skin-ravenclaw" },
|
||
{ id: 'skin_hufflepuff', name: "Hufflepuff", type: 'skin', category: 'Card Skins', price: 100, desc: "Honey and charcoal loyalty motif.", class: "skin-hufflepuff" },
|
||
{ id: 'skin_map', name: "Marauder's Map", type: 'skin', category: 'Card Skins', price: 140, desc: "Footprints on aged parchment.", class: "skin-map" },
|
||
{ id: 'skin_spellbook', name: "Spellbook", type: 'skin', category: 'Card Skins', price: 140, desc: "Worn leather and gold filigree.", class: "skin-spellbook" },
|
||
// TRAILS
|
||
{ id: 'trail_sparks', name: "Sparks Trail", type: 'trail', category: 'Wand Effects', price: 120, desc: "Golden sparks.", class: "trail-sparks" },
|
||
{ id: 'trail_ice', name: "Frost Trail", type: 'trail', category: 'Wand Effects', price: 120, desc: "Icy crystals.", class: "trail-ice" },
|
||
{ id: 'trail_slime', name: "Troll Slime", type: 'trail', category: 'Wand Effects', price: 80, desc: "Sticky green trail.", class: "trail-slime" },
|
||
];
|
||
|
||
function getFactsForLevel(levelIndex) {
|
||
const level = CURRICULUM[levelIndex];
|
||
const facts = new Set();
|
||
for (const num1 of level.numbers) {
|
||
for (let num2 = 1; num2 <= 12; num2++) {
|
||
const fact = [num1, num2].sort((a, b) => a - b);
|
||
facts.add(`${fact[0]}x${fact[1]}`);
|
||
}
|
||
}
|
||
return Array.from(facts);
|
||
}
|
||
|
||
function getLevelTargetXP() {
|
||
const levelData = CURRICULUM[state.levelIndex];
|
||
const requiredCount = getFactsForLevel(state.levelIndex).length;
|
||
const scaled = requiredCount * 12; // scales with how many facts are in the level
|
||
return Math.max(levelData.targetXP, scaled);
|
||
}
|
||
|
||
function allFactsMastered(levelIndex) {
|
||
const requiredFacts = getFactsForLevel(levelIndex);
|
||
const masteredFactsInLevel = state.masteredFacts.filter(fact => requiredFacts.includes(fact));
|
||
return masteredFactsInLevel.length === requiredFacts.length;
|
||
}
|
||
|
||
// --- STATE ---
|
||
let state = {
|
||
levelIndex: 0, currentLevelXP: 0, streak: 0, gold: 0,
|
||
troubleList: [], masteredFacts: [],
|
||
totalQuestions: 0, correctAnswers: 0, fastestTime: null,
|
||
inventory: {},
|
||
activeSkin: 'skin-default', activeTrail: '', activeBackground: 'bg-default',
|
||
activePet: '', activePetIcon: '',
|
||
studentName: '',
|
||
focusQueue: [],
|
||
lastFactId: ''
|
||
};
|
||
|
||
let game = {
|
||
active: false,
|
||
currentFact: { n1: 0, n2: 0, ans: 0 },
|
||
currentInput: "",
|
||
attempts: 0,
|
||
startTime: 0,
|
||
timeTurnerCharges: 0,
|
||
forcedAnswer: false
|
||
};
|
||
|
||
let dementor = { interval: null, progress: 0, timedOut: false };
|
||
let snitchTimer = null;
|
||
let cheatClicks = 0;
|
||
|
||
// --- INIT ---
|
||
function init() {
|
||
loadData();
|
||
const card = document.getElementById('main-card');
|
||
const overlay = document.getElementById('dementor-overlay');
|
||
if (card && overlay && overlay.parentElement !== card) {
|
||
card.prepend(overlay);
|
||
}
|
||
|
||
if (!state.studentName) {
|
||
const hashName = window.location.hash.substring(1);
|
||
if (hashName) {
|
||
state.studentName = hashName.substring(0, 12);
|
||
saveData();
|
||
updateLetterGreeting();
|
||
document.querySelector('.letter .signature').insertAdjacentHTML('beforebegin', `<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);
|
||
document.addEventListener('keydown', handleKeydown);
|
||
|
||
// CHEAT LISTENER (Tap Year X 5 times)
|
||
document.getElementById('level-name').addEventListener('click', () => {
|
||
cheatClicks++;
|
||
if (cheatClicks === 5) {
|
||
state.gold += 500;
|
||
document.getElementById('feedback-msg').style.color = '#d4af37';
|
||
document.getElementById('feedback-msg').innerText = "Gringotts Vault Opened! +500G";
|
||
saveData(); updateUI(); cheatClicks = 0;
|
||
}
|
||
setTimeout(() => cheatClicks = 0, 2000);
|
||
});
|
||
|
||
const skip = localStorage.getItem('arithmancySkipIntro');
|
||
if (skip === 'true') startGame(); else openIntro();
|
||
}
|
||
|
||
// --- CORE LOOP ---
|
||
function startGame() {
|
||
game.active = true;
|
||
generateQuestion();
|
||
}
|
||
|
||
function generateQuestion() {
|
||
resetDementor();
|
||
game.currentInput = "";
|
||
game.attempts = 0;
|
||
game.forcedAnswer = false;
|
||
game.startTime = Date.now();
|
||
|
||
document.getElementById('answer-display').innerText = "";
|
||
document.getElementById('feedback-msg').innerText = "";
|
||
const hintGrid = document.getElementById('hint-grid');
|
||
hintGrid.innerHTML = "";
|
||
hintGrid.classList.remove('has-stars');
|
||
const castBtn = document.querySelector('.key-cast');
|
||
const answerDisplay = document.getElementById('answer-display');
|
||
if (castBtn) castBtn.classList.remove('ready-cast');
|
||
if (answerDisplay) answerDisplay.classList.remove('forced-answer');
|
||
|
||
if (game.timeTurnerCharges > 0) {
|
||
game.timeTurnerCharges--;
|
||
updateBuffDisplay();
|
||
} else {
|
||
updateBuffDisplay();
|
||
}
|
||
|
||
const levelData = CURRICULUM[state.levelIndex];
|
||
const allowedNumbers = levelData.numbers;
|
||
const relevantTrouble = state.troubleList.filter(t => allowedNumbers.includes(t.n1) || allowedNumbers.includes(t.n2));
|
||
|
||
let n1, n2;
|
||
const makeId = (a, b) => {
|
||
const [s, l] = a < b ? [a, b] : [b, a];
|
||
return `${s}x${l}`;
|
||
};
|
||
const lastId = state.lastFactId;
|
||
|
||
if (state.focusQueue.length > 0) {
|
||
const factId = state.focusQueue.shift();
|
||
const parts = factId.split('x').map(Number);
|
||
[n1, n2] = parts;
|
||
} else {
|
||
let attempts = 0;
|
||
while (attempts < 6) {
|
||
if (relevantTrouble.length > 0 && Math.random() < 0.5) {
|
||
const struggle = relevantTrouble[Math.floor(Math.random() * relevantTrouble.length)];
|
||
n1 = struggle.n1; n2 = struggle.n2;
|
||
} else {
|
||
n1 = allowedNumbers[Math.floor(Math.random() * allowedNumbers.length)];
|
||
n2 = Math.floor(Math.random() * 12) + 1;
|
||
if (Math.random() > 0.5) [n1, n2] = [n2, n1];
|
||
}
|
||
const cid = makeId(n1, n2);
|
||
if (cid !== lastId || attempts === 5) break;
|
||
attempts++;
|
||
}
|
||
}
|
||
|
||
game.currentFact = { n1, n2, ans: n1 * n2 };
|
||
state.lastFactId = makeId(n1, n2);
|
||
document.getElementById('equation-display').innerText = `${n1} × ${n2}`;
|
||
startDementor();
|
||
}
|
||
|
||
function checkAnswer() {
|
||
if (!game.active || game.currentInput === "") return;
|
||
|
||
state.totalQuestions++;
|
||
const userAns = parseInt(game.currentInput);
|
||
const feedback = document.getElementById('feedback-msg');
|
||
const card = document.querySelector('.card-container');
|
||
const levelData = CURRICULUM[state.levelIndex];
|
||
|
||
if (userAns === game.currentFact.ans) {
|
||
state.correctAnswers++;
|
||
const answerTime = Date.now() - game.startTime;
|
||
if (state.fastestTime === null || answerTime < state.fastestTime) {
|
||
state.fastestTime = answerTime;
|
||
}
|
||
state.streak++;
|
||
const goldGain = game.attempts === 0 ? 2 : 1;
|
||
state.gold += goldGain;
|
||
const xpGain = game.attempts === 0 ? 10 : 5;
|
||
state.currentLevelXP += xpGain;
|
||
applyXPToMastery();
|
||
|
||
const xpPop = document.createElement('div');
|
||
xpPop.className = 'xp-gain-pop';
|
||
xpPop.innerText = `+${xpGain} XP`;
|
||
card.appendChild(xpPop);
|
||
setTimeout(() => xpPop.remove(), 1000);
|
||
|
||
if (game.attempts === 0) {
|
||
const fact = [game.currentFact.n1, game.currentFact.n2].sort((a, b) => a - b);
|
||
const factId = `${fact[0]}x${fact[1]}`;
|
||
if (!state.masteredFacts.includes(factId)) {
|
||
state.masteredFacts.push(factId);
|
||
}
|
||
removeFromTroubleList(game.currentFact.n1, game.currentFact.n2);
|
||
}
|
||
|
||
castPatronus();
|
||
feedback.style.color = '#7cff9a';
|
||
feedback.innerText = `Expecto Patronum! (+${goldGain} G)`;
|
||
|
||
if (allFactsMastered(state.levelIndex) && state.levelIndex < CURRICULUM.length - 1) {
|
||
state.levelIndex++;
|
||
state.currentLevelXP = 0;
|
||
state.masteredFacts = [];
|
||
state.focusQueue = [];
|
||
|
||
Swal.fire({
|
||
title: 'LEVEL UP!',
|
||
text: `Welcome to ${CURRICULUM[state.levelIndex].name}`,
|
||
icon: 'success',
|
||
customClass: {
|
||
popup: 'hogwarts-popup',
|
||
confirmButton: 'hogwarts-button',
|
||
title: 'hogwarts-title',
|
||
}
|
||
});
|
||
|
||
saveData(); updateUI(); setTimeout(generateQuestion, 2500);
|
||
return;
|
||
}
|
||
saveData(); updateUI(); setTimeout(generateQuestion, 1000);
|
||
} else {
|
||
handleWrongAttempt();
|
||
}
|
||
}
|
||
|
||
// --- SHOP & INVENTORY ---
|
||
function openShop() {
|
||
game.active = false;
|
||
clearInterval(dementor.interval);
|
||
document.getElementById('dementor-overlay').style.display = 'none';
|
||
renderShop();
|
||
document.getElementById('shop-modal').classList.add('visible');
|
||
}
|
||
function closeShop() {
|
||
document.getElementById('shop-modal').classList.remove('visible');
|
||
document.getElementById('dementor-overlay').style.display = 'block';
|
||
game.active = true;
|
||
startDementor(true);
|
||
}
|
||
|
||
function availableShopItems() {
|
||
const dayMap = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||
const today = dayMap[new Date().getDay()];
|
||
return SHOP_ITEMS.filter(item => {
|
||
const owned = (state.inventory[item.id] || 0) > 0;
|
||
if (!item.availableDays) return true;
|
||
if (owned) return true;
|
||
return item.availableDays.includes(today);
|
||
});
|
||
}
|
||
|
||
function renderShop() {
|
||
const list = document.getElementById('shop-list');
|
||
document.getElementById('shop-gold').innerText = state.gold;
|
||
list.innerHTML = "";
|
||
const itemsToShow = availableShopItems();
|
||
const categories = [...new Set(itemsToShow.map(i => i.category))];
|
||
|
||
categories.forEach(cat => {
|
||
const catHeader = document.createElement('div');
|
||
catHeader.className = 'shop-category'; catHeader.innerText = cat; list.appendChild(catHeader);
|
||
itemsToShow.filter(i => i.category === cat).forEach(item => {
|
||
|
||
const qty = state.inventory[item.id] || 0;
|
||
const div = document.createElement('div');
|
||
div.className = `shop-item ${qty > 0 ? 'purchased' : ''}`;
|
||
|
||
let actionHtml = `<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');
|
||
const fromShop = document.getElementById('shop-modal')?.classList.contains('visible');
|
||
|
||
if (id === 'time_turner') {
|
||
game.timeTurnerCharges = 5;
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "Time Turner Active (5 turns)";
|
||
}
|
||
else if (id === 'liquid_luck') {
|
||
game.currentInput = game.currentFact.ans.toString();
|
||
document.getElementById('answer-display').innerText = game.currentInput;
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "Liquid Luck applied!";
|
||
}
|
||
else if (id === 'choc_frog') {
|
||
resetDementor();
|
||
feedback.style.color = '#740001'; feedback.innerText = "Ate Chocolate Frog. You feel better.";
|
||
}
|
||
else if (id === 'auto_quill') {
|
||
const strAns = game.currentFact.ans.toString();
|
||
game.currentInput = strAns.charAt(0);
|
||
document.getElementById('answer-display').innerText = game.currentInput;
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "The Quill writes...";
|
||
}
|
||
else if (id === 'study_spell') {
|
||
state.currentLevelXP += 25;
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "+25 XP from Study Spell!";
|
||
}
|
||
else if (id === 'vault_key') {
|
||
state.gold += 100;
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "Gringotts vault refilled! +100G";
|
||
}
|
||
else if (id === 'phoenix_feather') {
|
||
state.streak = Math.max(state.streak, 5);
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "Phoenix Feather restored your focus (Streak 5).";
|
||
}
|
||
else if (id === 'shield_charm') {
|
||
resetDementor();
|
||
game.timeTurnerCharges = Math.max(game.timeTurnerCharges, 2);
|
||
updateBuffDisplay();
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "Shield charm up (slow Dementors 2 turns).";
|
||
}
|
||
else if (id === 'astral_map') {
|
||
drawHintGrid(game.currentFact.n1 || 1, game.currentFact.n2 || 1);
|
||
feedback.style.color = '#d4af37'; feedback.innerText = "The stars reveal the path.";
|
||
}
|
||
|
||
state.inventory[id]--;
|
||
saveData();
|
||
renderQuickConsumables();
|
||
if (fromShop) closeShop(); else updateUI();
|
||
}
|
||
|
||
function updateBuffDisplay() {
|
||
const bar = document.getElementById('buff-bar');
|
||
bar.innerHTML = "";
|
||
if (game.timeTurnerCharges > 0) {
|
||
const badge = document.createElement('div');
|
||
badge.className = 'buff-tag';
|
||
badge.innerText = `⏳ ${game.timeTurnerCharges}`;
|
||
bar.appendChild(badge);
|
||
}
|
||
}
|
||
|
||
function renderQuickConsumables() {
|
||
const holder = document.getElementById('quick-consumables');
|
||
if (!holder) return;
|
||
holder.innerHTML = "";
|
||
const consumables = SHOP_ITEMS.filter(i => i.type === 'consumable' && (state.inventory[i.id] || 0) > 0);
|
||
if (consumables.length === 0) {
|
||
holder.innerHTML = `<div style="font-size:0.8rem; color:#555;">No consumables on hand.</div>`;
|
||
return;
|
||
}
|
||
consumables.forEach(item => {
|
||
const qty = state.inventory[item.id] || 0;
|
||
const card = document.createElement('div');
|
||
card.className = 'consumable-card';
|
||
card.innerHTML = `
|
||
<h5>${item.name}</h5>
|
||
<div class="qty">Owned: ${qty}</div>
|
||
<button ${qty <= 0 ? 'disabled' : ''} onclick="useConsumable('${item.id}')">Use</button>
|
||
`;
|
||
holder.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// --- VISUALS ---
|
||
function applyCosmetics() {
|
||
const card = document.getElementById('main-card');
|
||
const allowedSkins = ['skin-default','skin-gryffindor','skin-slytherin','skin-ravenclaw','skin-hufflepuff','skin-map','skin-spellbook'];
|
||
if (!allowedSkins.includes(state.activeSkin)) {
|
||
state.activeSkin = 'skin-default';
|
||
}
|
||
card.className = `card-container ${state.activeSkin}`;
|
||
const allowedBackgrounds = ['bg-default'];
|
||
if (!allowedBackgrounds.includes(state.activeBackground)) {
|
||
state.activeBackground = 'bg-default';
|
||
}
|
||
document.body.className = state.activeBackground;
|
||
renderPet();
|
||
}
|
||
function handleTouchTrail(e) {
|
||
if (!state.activeTrail) return;
|
||
const touch = e.touches[0]; createParticle(touch.clientX, touch.clientY);
|
||
}
|
||
function handleMouseTrail(e) {
|
||
if (!state.activeTrail || e.buttons === 0) return;
|
||
createParticle(e.clientX, e.clientY);
|
||
}
|
||
function createParticle(x, y) {
|
||
const p = document.createElement('div');
|
||
p.className = `particle ${state.activeTrail}`;
|
||
p.style.left = `${x}px`; p.style.top = `${y}px`;
|
||
document.body.appendChild(p); setTimeout(() => p.remove(), 800);
|
||
}
|
||
|
||
function handleKeydown(e) {
|
||
const activeEl = document.activeElement;
|
||
if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA')) return;
|
||
if (document.querySelector('.modal-overlay.visible')) return;
|
||
if (!game.active && !game.forcedAnswer) return;
|
||
const k = e.key;
|
||
if (k >= '0' && k <= '9') {
|
||
pressKey(Number(k));
|
||
} else if (k === 'Backspace') {
|
||
e.preventDefault();
|
||
pressKey('BS');
|
||
} else if (k === 'Enter') {
|
||
checkAnswer();
|
||
} else if (k === 'c' || k === 'C' || k === 'Escape') {
|
||
pressKey('C');
|
||
}
|
||
}
|
||
|
||
function triggerShake(card, vibrate = true) {
|
||
if (card) {
|
||
card.classList.add('apply-shake');
|
||
setTimeout(() => card.classList.remove('apply-shake'), 500);
|
||
}
|
||
if (vibrate && navigator.vibrate) navigator.vibrate([90, 40, 90]);
|
||
}
|
||
|
||
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();
|
||
renderQuickConsumables();
|
||
}
|
||
|
||
function startDementor(resume = false) {
|
||
clearInterval(dementor.interval);
|
||
if (!resume) dementor.progress = 0;
|
||
dementor.timedOut = false;
|
||
const difficulty = Math.max(game.currentFact.n1 || 1, game.currentFact.n2 || 1);
|
||
let totalMs = 10000 + difficulty * 500; // base + more time for harder facts
|
||
let tickMs = 200;
|
||
if (game.timeTurnerCharges > 0) {
|
||
tickMs = 400;
|
||
totalMs *= 1.6;
|
||
}
|
||
const step = tickMs / totalMs;
|
||
|
||
updateDementorVisuals();
|
||
dementor.interval = setInterval(() => {
|
||
if (!game.active) return;
|
||
if (dementor.progress < 1) {
|
||
dementor.progress += step; updateDementorVisuals();
|
||
} else if (!dementor.timedOut) {
|
||
dementor.timedOut = true;
|
||
handleDementorTimeout();
|
||
}
|
||
}, tickMs);
|
||
}
|
||
|
||
function updateDementorVisuals() {
|
||
const overlay = document.getElementById('dementor-overlay');
|
||
const p = dementor.progress > 1 ? 1 : dementor.progress;
|
||
const opacity = 0.12 + (p * 0.6); // darker as time passes
|
||
const size = 18 + (p * 82); // spread further toward center over time, nearly full cover
|
||
const darkness = 0.4 + (p * 0.55);
|
||
const vignette = 0.2 + (p * 0.32);
|
||
const blur = 0 + (p * 0.8);
|
||
const glow = Math.min(2.0, 0.5 + (p * 1.5)); // stronger glow as it darkens
|
||
overlay.style.opacity = opacity;
|
||
overlay.style.setProperty('--circle-size', `${size}%`);
|
||
overlay.style.setProperty('--circle-strength', darkness.toFixed(2));
|
||
overlay.style.setProperty('--circle-blur', `${blur}px`);
|
||
overlay.style.setProperty('--vignette-strength', vignette.toFixed(2));
|
||
overlay.style.background = buildDementorBackground(p, size, darkness, vignette);
|
||
const castTimer = document.getElementById('cast-timer-fill');
|
||
if (castTimer) castTimer.style.width = `${Math.max(0, 100 - p * 100)}%`;
|
||
document.documentElement.style.setProperty('--focus-glow', glow.toFixed(2));
|
||
const uiGlow = Math.min(1.2, 0.15 + (p * 1.05));
|
||
document.documentElement.style.setProperty('--ui-glow', uiGlow.toFixed(2));
|
||
}
|
||
|
||
function resetDementor() {
|
||
clearInterval(dementor.interval); dementor.progress = 0;
|
||
const overlay = document.getElementById('dementor-overlay');
|
||
overlay.style.opacity = 0;
|
||
overlay.style.setProperty('--circle-size', '12%');
|
||
overlay.style.setProperty('--circle-strength', '0.42');
|
||
overlay.style.setProperty('--circle-blur', '0px');
|
||
overlay.style.setProperty('--vignette-strength', '0.18');
|
||
overlay.style.background = 'transparent';
|
||
const castTimer = document.getElementById('cast-timer-fill');
|
||
if (castTimer) castTimer.style.width = '100%';
|
||
document.documentElement.style.setProperty('--focus-glow', '0');
|
||
document.documentElement.style.setProperty('--ui-glow', '0');
|
||
saveData();
|
||
dementor.timedOut = false;
|
||
}
|
||
|
||
function buildDementorBackground(progress, size, strength, vignette) {
|
||
const circles = [
|
||
{ pos: '0% 12%', s: 1.0 },
|
||
{ pos: '100% 12%', s: 1.0 },
|
||
{ pos: '0% 88%', s: 1.0 },
|
||
{ pos: '100% 88%', s: 1.0 },
|
||
{ pos: '0% 50%', s: 1.1 },
|
||
{ pos: '100% 50%', s: 1.1 },
|
||
{ pos: '50% 0%', s: 1.05 },
|
||
{ pos: '50% 100%', s: 1.05 }
|
||
];
|
||
const activeCount = Math.max(1, Math.min(circles.length, Math.floor(progress * circles.length) + 1));
|
||
const gradients = [];
|
||
for (let i = 0; i < activeCount; i++) {
|
||
const c = circles[i];
|
||
gradients.push(
|
||
`radial-gradient(circle at ${c.pos}, rgba(0,0,0,${(strength * c.s).toFixed(2)}) 0%, rgba(0,0,0,${(strength * c.s).toFixed(2)}) ${Math.max(size * 0.55, 6)}%, transparent ${size}%)`
|
||
);
|
||
}
|
||
gradients.push(`radial-gradient(circle at 50% 50%, transparent 60%, rgba(0,0,0,${(vignette * 0.35).toFixed(2)}) 78%, rgba(0,0,0,${vignette.toFixed(2)}) 92%, rgba(0,0,0,${(vignette * 1.05).toFixed(2)}) 100%)`);
|
||
return gradients.join(',');
|
||
}
|
||
|
||
function castPatronus() {
|
||
clearInterval(dementor.interval);
|
||
const overlay = document.getElementById('dementor-overlay');
|
||
overlay.classList.add('patronus-flash');
|
||
setTimeout(() => { resetDementor(); overlay.classList.remove('patronus-flash'); }, 500);
|
||
}
|
||
|
||
function addToTroubleList(n1, n2) {
|
||
const [s, b] = n1 < n2 ? [n1, n2] : [n2, n1];
|
||
const id = `${s}x${b}`;
|
||
if (!state.troubleList.some(t => t.id === id)) state.troubleList.push({ id, n1: s, n2: b });
|
||
}
|
||
function removeFromTroubleList(n1, n2) {
|
||
const [s, b] = n1 < n2 ? [n1, n2] : [n2, n1];
|
||
const id = `${s}x${b}`;
|
||
state.troubleList = state.troubleList.filter(t => t.id !== id);
|
||
}
|
||
|
||
function drawHintGrid(rows, cols) {
|
||
const grid = document.getElementById('hint-grid');
|
||
grid.innerHTML = "";
|
||
grid.classList.toggle('has-stars', false);
|
||
if (!rows || !cols) return;
|
||
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; // layout vertically taller than wide
|
||
for (let i = 0; i < (rows * cols); i++) {
|
||
const dot = document.createElement('div'); dot.className = 'star-dot'; grid.appendChild(dot);
|
||
}
|
||
grid.classList.add('has-stars');
|
||
}
|
||
|
||
function applyPenalty(amount) {
|
||
if (!amount) return;
|
||
state.currentLevelXP = Math.max(0, state.currentLevelXP - amount);
|
||
}
|
||
|
||
function handleWrongAttempt(options = {}) {
|
||
const { timedOut = false } = options;
|
||
const feedback = document.getElementById('feedback-msg');
|
||
const card = document.querySelector('.card-container');
|
||
state.streak = 0;
|
||
game.attempts = timedOut ? 3 : game.attempts + 1;
|
||
const penalty = game.attempts >= 3 || timedOut ? 10 : (game.attempts === 2 ? 5 : 0);
|
||
triggerShake(card, !timedOut);
|
||
if (!timedOut) {
|
||
dementor.progress += 0.2; updateDementorVisuals();
|
||
game.currentInput = "";
|
||
document.getElementById('answer-display').innerText = "";
|
||
}
|
||
if (penalty > 0) applyPenalty(penalty);
|
||
if (game.attempts >= 3) {
|
||
revealAnswerAndAdvance(timedOut ? 2000 : 0, timedOut, penalty);
|
||
return;
|
||
}
|
||
if (game.attempts === 2) {
|
||
feedback.style.color = '#ffb0b0';
|
||
feedback.innerText = `Count the stars... (-${penalty} XP)`;
|
||
drawHintGrid(game.currentFact.n1, game.currentFact.n2);
|
||
} else {
|
||
feedback.style.color = '#ffb0b0';
|
||
feedback.innerText = "Try again!";
|
||
}
|
||
saveData(); updateUI();
|
||
}
|
||
|
||
function revealAnswerAndAdvance(delayMs = 0, timedOut = false, penalty = 0) {
|
||
const feedback = document.getElementById('feedback-msg');
|
||
const card = document.querySelector('.card-container');
|
||
const factId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||
game.active = false;
|
||
setTimeout(() => {
|
||
// guard in case question changed
|
||
const currentId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||
if (currentId !== factId) return;
|
||
triggerShake(card, false); // triggered from timeout; not a direct gesture
|
||
feedback.style.color = '#ffb0b0';
|
||
feedback.innerText = `Answer is ${game.currentFact.ans}.${penalty ? ` (-${penalty} XP)` : ''} Press CAST to continue.`;
|
||
addToTroubleList(game.currentFact.n1, game.currentFact.n2);
|
||
game.currentInput = game.currentFact.ans.toString();
|
||
const answerDisplay = document.getElementById('answer-display');
|
||
if (answerDisplay) {
|
||
answerDisplay.innerText = game.currentInput;
|
||
answerDisplay.classList.add('forced-answer');
|
||
}
|
||
const castBtn = document.querySelector('.key-cast');
|
||
if (castBtn) castBtn.classList.add('ready-cast');
|
||
game.forcedAnswer = true;
|
||
resetDementor();
|
||
saveData(); updateUI();
|
||
game.active = true;
|
||
}, delayMs);
|
||
}
|
||
|
||
function handleDementorTimeout() {
|
||
const factId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||
game.active = false;
|
||
setTimeout(() => {
|
||
const currentId = `${Math.min(game.currentFact.n1, game.currentFact.n2)}x${Math.max(game.currentFact.n1, game.currentFact.n2)}`;
|
||
if (currentId !== factId) return;
|
||
const feedback = document.getElementById('feedback-msg');
|
||
if (feedback) {
|
||
feedback.style.color = '#ffb0b0';
|
||
feedback.innerText = "Saved by a Professor...";
|
||
}
|
||
handleWrongAttempt({ timedOut: true });
|
||
}, 2000);
|
||
}
|
||
|
||
function applyXPToMastery() {
|
||
const levelData = CURRICULUM[state.levelIndex];
|
||
const requiredFacts = getFactsForLevel(state.levelIndex);
|
||
const targetXP = levelData.targetXP;
|
||
const queued = new Set(state.focusQueue);
|
||
const unmastered = () => requiredFacts.filter(f => !state.masteredFacts.includes(f));
|
||
|
||
while (state.currentLevelXP >= targetXP && unmastered().length > 0) {
|
||
state.currentLevelXP -= targetXP;
|
||
const available = unmastered().filter(f => !queued.has(f));
|
||
if (available.length === 0) break;
|
||
|
||
const troubleIds = state.troubleList.map(t => `${Math.min(t.n1, t.n2)}x${Math.max(t.n1, t.n2)}`);
|
||
const troubleCandidate = available.find(id => troubleIds.includes(id));
|
||
const pickedId = troubleCandidate || available[Math.floor(Math.random() * available.length)];
|
||
|
||
state.focusQueue.push(pickedId);
|
||
queued.add(pickedId);
|
||
}
|
||
}
|
||
|
||
function renderPet() {
|
||
const perch = document.getElementById('pet-perch');
|
||
if (!perch) return;
|
||
perch.innerHTML = "";
|
||
if (!state.activePet) return;
|
||
const pet = document.createElement('div');
|
||
pet.className = `pet-icon ${state.activePet}`;
|
||
pet.innerText = state.activePetIcon || '🐾';
|
||
perch.appendChild(pet);
|
||
}
|
||
|
||
function saveData() { localStorage.setItem('arithmancyDataV17', JSON.stringify(state)); }
|
||
function loadData() {
|
||
const saved = localStorage.getItem('arithmancyDataV17');
|
||
if (saved) {
|
||
const parsed = JSON.parse(saved);
|
||
if (parsed.masteredFacts && Array.isArray(parsed.masteredFacts)) {
|
||
state = { ...state, ...parsed };
|
||
}
|
||
}
|
||
}
|
||
|
||
function openIntro() {
|
||
document.getElementById('intro-modal').classList.add('visible');
|
||
document.getElementById('dont-show-again').checked = (localStorage.getItem('arithmancySkipIntro') === 'true');
|
||
game.active = false; clearInterval(dementor.interval);
|
||
}
|
||
|
||
function closeIntro() {
|
||
document.getElementById('intro-modal').classList.remove('visible');
|
||
const checkbox = document.getElementById('dont-show-again');
|
||
if(checkbox.checked) localStorage.setItem('arithmancySkipIntro', 'true');
|
||
else localStorage.removeItem('arithmancySkipIntro');
|
||
if(game.currentFact.ans === 0) startGame(); else { game.active = true; startDementor(true); }
|
||
}
|
||
window.addEventListener('beforeunload', function (e) {
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
|
||
// --- GOLDEN SNITCH EVENT ---
|
||
function scheduleSnitch() {
|
||
if (snitchTimer) clearTimeout(snitchTimer);
|
||
const delay = 45000 + Math.random() * 30000; // 45–75s
|
||
snitchTimer = setTimeout(spawnSnitch, delay);
|
||
}
|
||
|
||
function spawnSnitch() {
|
||
const layer = document.getElementById('snitch-layer');
|
||
if (!layer || document.querySelector('.flying-snitch')) {
|
||
scheduleSnitch();
|
||
return;
|
||
}
|
||
if (dementor.progress > 0.6) { // scared off by looming dementor
|
||
scheduleSnitch();
|
||
return;
|
||
}
|
||
const snitch = document.createElement('div');
|
||
snitch.className = 'flying-snitch';
|
||
const fromLeft = Math.random() > 0.5;
|
||
snitch.classList.toggle('from-right', !fromLeft);
|
||
snitch.style.top = `${10 + Math.random() * 70}%`;
|
||
snitch.addEventListener('click', () => claimSnitch(snitch));
|
||
layer.appendChild(snitch);
|
||
setTimeout(() => {
|
||
snitch.remove();
|
||
scheduleSnitch();
|
||
}, 8000);
|
||
}
|
||
|
||
function claimSnitch(el) {
|
||
el.remove();
|
||
const reward = 50;
|
||
state.gold += reward;
|
||
saveData(); updateUI();
|
||
const feedback = document.getElementById('feedback-msg');
|
||
feedback.style.color = '#d4af37';
|
||
feedback.innerText = `Caught the Snitch! +${reward}G`;
|
||
scheduleSnitch();
|
||
}
|