diff --git a/www/rhetorik_trainer_quiz.html b/www/rhetorik_trainer_quiz.html index 9eca6be..f98693a 100644 --- a/www/rhetorik_trainer_quiz.html +++ b/www/rhetorik_trainer_quiz.html @@ -16,6 +16,18 @@ RhetorikMaster - Stilmittel Trainer, Quiz & Klausur-Spickzettel + + + + + + + + + + + + @@ -167,7 +179,20 @@ App installieren - + + + + + + + + +
+ + Gamification & Motivation + +
+ +
+

🏛️ Rang & Rhetorik-Erfolge

+

Sammle XP durch Karteikarten, Quiz und Prüfungen, um alle 9 Trophäen freizuschalten.

+
+ + +
+
+
+
Dein aktueller Rang
+
Rang 1: Novize
+
+
+ +
+
+ +
+
+ 0 XP gesammelt + 500 XP bis Textanalytiker +
+
+
+
+
+
+ + +
+
+

Trophäen-Kabinett (0 von 9 freigeschaltet)

+ Lokal gespeichert +
+
+ +
+
+ +
+ + +
+ + +
@@ -1955,6 +2053,7 @@ if (isMastered) { setItemLevel(item.id, currentLvl + 1); playTone('success'); + addXP(20, 'Karte gemeistert'); cardEl.classList.add('swipe-right'); showToast(`„${item.term}“ aufgestiegen in Kasten ${Math.min(5, currentLvl + 1)}!`, 'fa-circle-up'); } else { @@ -2049,6 +2148,10 @@ utterance.rate = 0.95; utterance.pitch = 1.0; window.speechSynthesis.speak(utterance); + + audioListenCount++; + localStorage.setItem('rhetorik_audio_count', audioListenCount.toString()); + addXP(5); } catch (err) { console.warn('SpeechSynthesis error:', err); } @@ -2572,6 +2675,13 @@ if (isCorrect) { quizScore += 100 + (quizStreak * 20); quizStreak++; + if (currentQuizInputType === 'text') { + freitextCorrectCount++; + localStorage.setItem('rhetorik_freitext_count', freitextCorrectCount.toString()); + addXP(25, 'Freitext richtig'); + } else { + addXP(15, 'Quiz richtig'); + } if (quizStreak > quizMaxStreak) quizMaxStreak = quizStreak; playTone('success'); @@ -2712,6 +2822,15 @@ const gradeInfo = calculateExamGrade(correctCount, quizQuestions.length); playTone(gradeInfo.grade <= 3 ? 'win' : 'error'); + // Grant Exam XP & Badges + if (gradeInfo.grade === 1) addXP(250, 'Note 1'); + else if (gradeInfo.grade === 2) addXP(180, 'Note 2'); + else if (gradeInfo.grade === 3) addXP(100, 'Note 3'); + else if (gradeInfo.grade === 4) addXP(50, 'Note 4'); + + if (gradeInfo.grade <= 2) unlockBadge('exam_ace'); + if (elapsedSec <= 300 && gradeInfo.grade <= 3) unlockBadge('speed_demon'); + // Populate Exam Screen document.getElementById('quizPlayArea').classList.add('hidden'); document.getElementById('examTimerBar').classList.add('hidden'); @@ -3297,6 +3416,7 @@ const payload = JSON.parse(evt.target.result); applySyncData(payload); closeSyncModal(); + closeAchievementsModal(); showToast('Lernstand aus JSON-Datei importiert!', 'fa-circle-check'); } catch (err) { alert('Ungültiges Backup-Format in der ausgewählten JSON-Datei.'); @@ -3316,6 +3436,7 @@ } applySyncData(payload); closeSyncModal(); + closeAchievementsModal(); document.getElementById('syncImportText').value = ''; showToast('Lernstand erfolgreich aktualisiert!', 'fa-circle-check'); } @@ -3375,6 +3496,319 @@ } } + + // ========================================== + // GAMIFICATION, XP, BADGES & SOCIAL SHARING + // ========================================== + const ACHIEVEMENTS_DEF = [ + { + id: "first_step", + title: "Erster Schritt", + desc: "Erstes Quiz absolviert oder 5 Karteikarten gelernt.", + icon: "fa-seedling", + color: "emerald", + xpReward: 50 + }, + { + id: "streak_10", + title: "Feuer & Flamme", + desc: "Einen 10er Streak im Quiz-Modus erreicht.", + icon: "fa-fire", + color: "amber", + xpReward: 100 + }, + { + id: "freitext_pro", + title: "Freitext-Virtuose", + desc: "5 Fragen ohne Multiple Choice per Freitext getippt.", + icon: "fa-keyboard", + color: "indigo", + xpReward: 150 + }, + { + id: "exam_ace", + title: "Klausur-Ass", + desc: "Eine Klausursimulation mit Note 1 oder 2 bestanden.", + icon: "fa-crown", + color: "amber", + xpReward: 200 + }, + { + id: "speed_demon", + title: "Speed-Denker", + desc: "Eine 15-Fragen-Prüfung in unter 5 Minuten absolviert.", + icon: "fa-bolt", + color: "violet", + xpReward: 120 + }, + { + id: "audio_listener", + title: "Hörversteher", + desc: "Mindestens 3 Zitate über die Sprachausgabe angehört.", + icon: "fa-headphones", + color: "blue", + xpReward: 60 + }, + { + id: "weakness_slayer", + title: "Fehlerjäger", + desc: "Alle offenen Schwachstellen im Training abgebaut.", + icon: "fa-shield-halved", + color: "rose", + xpReward: 150 + }, + { + id: "sound_master", + title: "Klangmeister", + desc: "Alle Klangfiguren in Leitner-Kasten 5 gemeistert.", + icon: "fa-music", + color: "cyan", + xpReward: 180 + }, + { + id: "grandmaster", + title: "Cicero der Neuzeit", + desc: "Alle 23 rhetorischen Stilmittel in Kasten 5 gemeistert.", + icon: "fa-trophy", + color: "amber", + xpReward: 500 + } + ]; + + const USER_RANKS = [ + { minXp: 0, name: "Novize", level: 1, icon: "fa-seedling" }, + { minXp: 500, name: "Textanalytiker", level: 2, icon: "fa-magnifying-glass" }, + { minXp: 1500, name: "Wortfechter", level: 3, icon: "fa-feather-pointed" }, + { minXp: 3000, name: "Meister der Rhetorik", level: 4, icon: "fa-award" }, + { minXp: 6000, name: "Cicero / Großmeister", level: 5, icon: "fa-crown" } + ]; + + let userXP = parseInt(localStorage.getItem('rhetorik_user_xp') || '0', 10); + let unlockedBadges = JSON.parse(localStorage.getItem('rhetorik_unlocked_badges') || '[]'); + let freitextCorrectCount = parseInt(localStorage.getItem('rhetorik_freitext_count') || '0', 10); + let audioListenCount = parseInt(localStorage.getItem('rhetorik_audio_count') || '0', 10); + + function addXP(amount, reason = '') { + if (amount <= 0) return; + const prevRank = getCurrentRank(userXP); + userXP += amount; + localStorage.setItem('rhetorik_user_xp', userXP.toString()); + updateGamificationHeader(); + + const newRank = getCurrentRank(userXP); + if (newRank.level > prevRank.level) { + playTone('win'); + showToast(`🎉 RANG-AUFSTIEG! Du bist jetzt: ${newRank.name}!`, 'fa-crown'); + } + + checkAchievements(); + } + + function getCurrentRank(xp) { + let current = USER_RANKS[0]; + for (let i = 0; i < USER_RANKS.length; i++) { + if (xp >= USER_RANKS[i].minXp) { + current = USER_RANKS[i]; + } else { + break; + } + } + return current; + } + + function getNextRank(xp) { + for (let i = 0; i < USER_RANKS.length; i++) { + if (xp < USER_RANKS[i].minXp) { + return USER_RANKS[i]; + } + } + return null; + } + + function updateGamificationHeader() { + const rank = getCurrentRank(userXP); + const titleEl = document.getElementById('userRankTitle'); + if (titleEl) titleEl.innerText = rank.name; + + const pillEl = document.getElementById('badgeCountPill'); + if (pillEl) pillEl.innerText = `${unlockedBadges.length}/9`; + } + + function unlockBadge(badgeId) { + if (unlockedBadges.includes(badgeId)) return; + const bDef = ACHIEVEMENTS_DEF.find(b => b.id === badgeId); + if (!bDef) return; + + unlockedBadges.push(badgeId); + localStorage.setItem('rhetorik_unlocked_badges', JSON.stringify(unlockedBadges)); + updateGamificationHeader(); + + playTone('win'); + showToast(`🏆 Trophäe freigeschaltet: ${bDef.title}! (+${bDef.xpReward} XP)`, 'fa-trophy'); + addXP(bDef.xpReward); + } + + function checkAchievements() { + const masteredCount = STILMITTEL_DATA.filter(item => (cardLevels[item.id] || 1) >= 5).length; + const klangMastered = STILMITTEL_DATA.filter(item => item.category === 'klang' && (cardLevels[item.id] || 1) >= 5).length; + const totalKlang = STILMITTEL_DATA.filter(item => item.category === 'klang').length; + + // 1. Erster Schritt + if (masteredCount >= 5 || userXP >= 50) { + unlockBadge('first_step'); + } + + // 2. Feuer & Flamme + if (quizMaxStreak >= 10) { + unlockBadge('streak_10'); + } + + // 3. Freitext-Virtuose + if (freitextCorrectCount >= 5) { + unlockBadge('freitext_pro'); + } + + // 6. Hörversteher + if (audioListenCount >= 3) { + unlockBadge('audio_listener'); + } + + // 7. Fehlerjäger + if (quizMistakes.length === 0 && userXP >= 300) { + unlockBadge('weakness_slayer'); + } + + // 8. Klangmeister + if (klangMastered === totalKlang && totalKlang > 0) { + unlockBadge('sound_master'); + } + + // 9. Großmeister + if (masteredCount >= STILMITTEL_DATA.length) { + unlockBadge('grandmaster'); + } + } + + function openAchievementsModal() { + const rank = getCurrentRank(userXP); + const nextRank = getNextRank(userXP); + + document.getElementById('modalUserRankName').innerText = `Rang ${rank.level}: ${rank.name}`; + document.getElementById('modalUserRankIcon').className = `fa-solid ${rank.icon} text-white`; + + const xpTextEl = document.getElementById('modalUserXpText'); + const nextTextEl = document.getElementById('modalNextRankText'); + const barEl = document.getElementById('modalXpProgressBar'); + + if (nextRank) { + xpTextEl.innerText = `${userXP} XP gesammelt`; + const diff = nextRank.minXp - rank.minXp; + const currentProgress = userXP - rank.minXp; + const percent = Math.min(100, Math.round((currentProgress / diff) * 100)); + nextTextEl.innerText = `${nextRank.minXp - userXP} XP bis ${nextRank.name}`; + barEl.style.width = `${percent}%`; + } else { + xpTextEl.innerText = `${userXP} XP (Maximalrang erreicht)`; + nextTextEl.innerText = 'Cicero / Höchster Rang!'; + barEl.style.width = '100%'; + } + + document.getElementById('modalBadgesUnlockedCount').innerText = unlockedBadges.length; + + // Render 9 Badges + const grid = document.getElementById('achievementsGrid'); + grid.innerHTML = ''; + + ACHIEVEMENTS_DEF.forEach(b => { + const isUnlocked = unlockedBadges.includes(b.id); + const card = document.createElement('div'); + card.className = `p-3.5 rounded-2xl border transition flex items-start gap-3 ${ + isUnlocked + ? 'bg-amber-50/70 border-amber-300 shadow-sm text-slate-900' + : 'bg-slate-50 border-slate-200 text-slate-400 opacity-75' + }`; + + card.innerHTML = ` +
+ +
+
+
+ ${b.title} + ${isUnlocked ? 'Frei' : '+' + b.xpReward + ' XP'} +
+

${b.desc}

+
+ `; + grid.appendChild(card); + }); + + playTone('flip'); + document.getElementById('achievementsModal').classList.remove('hidden'); + } + + function closeAchievementsModal() { + document.getElementById('achievementsModal').classList.add('hidden'); + } + + // SOCIAL SHARING FUNCTIONS + function shareApp() { + const shareData = { + title: 'RhetorikMaster - Stilmittel Trainer & Klausur-Spickzettel', + text: '🏛️ Meistere 23 rhetorische Stilmittel der Textanalyse mit Karteikarten, interaktivem Quiz und Klausursimulation!', + url: 'https://rhetorikmaster.pansi.eu/' + }; + + if (navigator.share) { + navigator.share(shareData).catch(() => {}); + } else { + navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => { + showToast('Empfehlungslink in die Zwischenablage kopiert!', 'fa-share-nodes'); + }); + } + } + + function shareExamResult() { + const gradeTitle = document.getElementById('examGradeTitle')?.innerText || 'Note: 1'; + const scoreText = document.getElementById('examScoreDisplay')?.innerText || '14 / 15'; + const percentText = document.getElementById('examPercentDisplay')?.innerText || '93%'; + + const shareData = { + title: 'Mein Klausur-Ergebnis im RhetorikMaster', + text: `🎓 Ich habe gerade die RhetorikMaster Klausursimulation mit ${gradeTitle} (${scoreText}, ${percentText}) bestanden! 🏛️ Teste dein Stilmittel-Wissen für die Textanalyse:`, + url: 'https://rhetorikmaster.pansi.eu/' + }; + + if (navigator.share) { + navigator.share(shareData).catch(() => {}); + } else { + navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => { + showToast('Klausur-Zeugnis zum Teilen kopiert!', 'fa-copy'); + }); + } + } + + function shareQuizResult() { + const scoreText = document.getElementById('resultScore')?.innerText || '0 Pkt'; + const streakText = document.getElementById('resultMaxStreak')?.innerText || '0'; + + const shareData = { + title: 'Mein Highscore im RhetorikMaster', + text: `🏆 Ich habe im RhetorikMaster Quiz ${scoreText} und einen Streak von ${streakText} erreicht! Schlägst du mein Ergebnis?`, + url: 'https://rhetorikmaster.pansi.eu/' + }; + + if (navigator.share) { + navigator.share(shareData).catch(() => {}); + } else { + navigator.clipboard.writeText(`${shareData.text} ${shareData.url}`).then(() => { + showToast('Quiz-Ergebnis zum Teilen kopiert!', 'fa-copy'); + }); + } + } + // Keyboard Shortcuts window.addEventListener('keydown', (e) => { if (document.activeElement && document.activeElement.tagName === 'INPUT') { @@ -3403,6 +3837,7 @@ if (e.key === 'Escape') { closeDetailModal(); closeSyncModal(); + closeAchievementsModal(); cancelIncomingSync(); return; } @@ -3501,6 +3936,8 @@ applyCardFilters(); updateMasteredHeader(); renderPrintCheatsheet(); + updateGamificationHeader(); + checkAchievements(); checkUrlSync(); }; diff --git a/www/sw.js b/www/sw.js index c6565bd..0d79865 100644 --- a/www/sw.js +++ b/www/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'rhetorikmaster-v8'; +const CACHE_NAME = 'rhetorikmaster-v9'; const STATIC_ASSETS = [ '/', '/index.html',