From 6d5dc9eb7a0b9325b525c93295f7e940304296af Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Thu, 11 Sep 2025 17:07:43 +0200 Subject: [PATCH] =?UTF-8?q?Komplettes=20AdivmentSystem=20=C3=BCberarbeitet?= =?UTF-8?q?!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/achievementSystem.js | 712 +++++++++++++++++++++++++ package.json | 6 +- public/js/dashboard.js | 18 +- routes/api.js | 47 +- scripts/simulate-new-time.js | 94 ++++ scripts/test-achievements.js | 50 ++ scripts/test-immediate-achievements.js | 48 ++ scripts/test-multiple-achievements.js | 75 +++ server.js | 53 ++ 9 files changed, 1087 insertions(+), 16 deletions(-) create mode 100644 lib/achievementSystem.js create mode 100644 scripts/simulate-new-time.js create mode 100644 scripts/test-achievements.js create mode 100644 scripts/test-immediate-achievements.js create mode 100644 scripts/test-multiple-achievements.js diff --git a/lib/achievementSystem.js b/lib/achievementSystem.js new file mode 100644 index 0000000..a94fc86 --- /dev/null +++ b/lib/achievementSystem.js @@ -0,0 +1,712 @@ +/** + * NinjaCross Achievement System + * + * JavaScript-basierte Achievement-Logik für das NinjaCross Parkour System. + * Liest Achievement-Definitionen aus der Datenbank und prüft/vergibt Achievements. + * + * @author NinjaCross Team + * @version 1.0.0 + */ + +const { Pool } = require('pg'); +require('dotenv').config(); + +// Database connection +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false +}); + +class AchievementSystem { + constructor() { + this.achievements = new Map(); + this.playerAchievements = new Map(); + } + + /** + * Lädt alle aktiven Achievements aus der Datenbank + */ + async loadAchievements() { + try { + const result = await pool.query(` + SELECT id, name, description, category, condition_type, condition_value, icon, points + FROM achievements + WHERE is_active = true + ORDER BY category, condition_type + `); + + this.achievements.clear(); + result.rows.forEach(achievement => { + this.achievements.set(achievement.id, achievement); + }); + + console.log(`📋 ${this.achievements.size} Achievements geladen`); + return true; + } catch (error) { + console.error('❌ Fehler beim Laden der Achievements:', error); + return false; + } + } + + /** + * Lädt alle Spieler-Achievements für einen Spieler + * Jetzt werden alle Completions geladen (nicht nur die neueste) + */ + async loadPlayerAchievements(playerId) { + try { + const result = await pool.query(` + SELECT pa.achievement_id, pa.progress, pa.is_completed, pa.earned_at + FROM player_achievements pa + WHERE pa.player_id = $1 + ORDER BY pa.earned_at DESC + `, [playerId]); + + // Gruppiere nach achievement_id und zähle Completions + this.playerAchievements.set(playerId, new Map()); + const achievementCounts = new Map(); + + result.rows.forEach(pa => { + if (!achievementCounts.has(pa.achievement_id)) { + achievementCounts.set(pa.achievement_id, { + count: 0, + latest: pa + }); + } + achievementCounts.get(pa.achievement_id).count++; + }); + + // Speichere die neueste Completion und die Anzahl + achievementCounts.forEach((data, achievementId) => { + this.playerAchievements.get(playerId).set(achievementId, { + ...data.latest, + completion_count: data.count + }); + }); + + return true; + } catch (error) { + console.error(`❌ Fehler beim Laden der Spieler-Achievements für ${playerId}:`, error); + return false; + } + } + + /** + * Prüft alle Achievements für einen Spieler + */ + async checkAllAchievements(playerId) { + console.log(`🎯 Prüfe Achievements für Spieler ${playerId}...`); + + // Lade Spieler-Achievements + await this.loadPlayerAchievements(playerId); + + const newAchievements = []; + + // Prüfe alle Achievement-Kategorien + await this.checkConsistencyAchievements(playerId, newAchievements); + await this.checkImprovementAchievements(playerId, newAchievements); + await this.checkSeasonalAchievements(playerId, newAchievements); + await this.checkBestTimeAchievements(playerId, newAchievements); + + console.log(`✅ ${newAchievements.length} neue Achievements für Spieler ${playerId}`); + return newAchievements; + } + + /** + * Prüft sofortige Achievements für einen Spieler (bei neuer Zeit) + * Diese Achievements werden sofort vergeben, nicht nur um 19:00 Uhr + */ + async checkImmediateAchievements(playerId) { + console.log(`⚡ Prüfe sofortige Achievements für Spieler ${playerId}...`); + + // Lade Spieler-Achievements + await this.loadPlayerAchievements(playerId); + + const newAchievements = []; + + // Prüfe nur sofortige Achievement-Kategorien + await this.checkImmediateConsistencyAchievements(playerId, newAchievements); + await this.checkImprovementAchievements(playerId, newAchievements); + await this.checkSeasonalAchievements(playerId, newAchievements); + // Best-Time Achievements werden NICHT sofort geprüft (nur um 19:00 Uhr) + + if (newAchievements.length > 0) { + console.log(`🏆 ${newAchievements.length} sofortige Achievements für Spieler ${playerId}:`); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } + + return newAchievements; + } + + /** + * Prüft Konsistenz-basierte Achievements + */ + async checkConsistencyAchievements(playerId, newAchievements) { + // Erste Schritte + await this.checkFirstTimeAchievement(playerId, newAchievements); + + // Versuche pro Tag + await this.checkAttemptsPerDayAchievements(playerId, newAchievements); + + // Einzigartige Tage + await this.checkUniqueDaysAchievements(playerId, newAchievements); + } + + /** + * Prüft nur sofortige Konsistenz-basierte Achievements + */ + async checkImmediateConsistencyAchievements(playerId, newAchievements) { + // Erste Schritte + await this.checkFirstTimeAchievement(playerId, newAchievements); + + // Versuche pro Tag + await this.checkAttemptsPerDayAchievements(playerId, newAchievements); + + // Einzigartige Tage werden NICHT sofort geprüft (nur um 19:00 Uhr) + } + + /** + * Prüft "Erste Schritte" Achievement + */ + async checkFirstTimeAchievement(playerId, newAchievements) { + const achievement = Array.from(this.achievements.values()) + .find(a => a.category === 'consistency' && a.condition_type === 'first_time'); + + if (!achievement) return; + + // Prüfe ob bereits erreicht + if (this.isAchievementCompleted(playerId, achievement.id)) return; + + // Zähle Gesamtversuche + const result = await pool.query(` + SELECT COUNT(*) as count + FROM times t + WHERE t.player_id = $1 + `, [playerId]); + + const totalAttempts = parseInt(result.rows[0].count); + + if (totalAttempts >= achievement.condition_value) { + await this.awardAchievement(playerId, achievement, totalAttempts, newAchievements); + } + } + + /** + * Prüft "Versuche pro Tag" Achievements + */ + async checkAttemptsPerDayAchievements(playerId, newAchievements) { + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'consistency' && a.condition_type === 'attempts_per_day'); + + // Zähle Versuche heute + const result = await pool.query(` + SELECT COUNT(*) as count + FROM times t + WHERE t.player_id = $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + `, [playerId]); + + const attemptsToday = parseInt(result.rows[0].count); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + if (attemptsToday >= achievement.condition_value) { + await this.awardAchievement(playerId, achievement, attemptsToday, newAchievements); + } + } + } + + /** + * Prüft "Einzigartige Tage" Achievements + */ + async checkUniqueDaysAchievements(playerId, newAchievements) { + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'consistency' && a.condition_type === 'unique_days'); + + // Zähle einzigartige Tage + const result = await pool.query(` + SELECT COUNT(DISTINCT DATE(t.created_at AT TIME ZONE 'Europe/Berlin')) as count + FROM times t + WHERE t.player_id = $1 + `, [playerId]); + + const uniqueDays = parseInt(result.rows[0].count); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + if (uniqueDays >= achievement.condition_value) { + await this.awardAchievement(playerId, achievement, uniqueDays, newAchievements); + } + } + } + + /** + * Prüft Verbesserungs-basierte Achievements + */ + async checkImprovementAchievements(playerId, newAchievements) { + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'improvement' && a.condition_type === 'time_improvement'); + + // Hole beste und zweitbeste Zeit + const result = await pool.query(` + SELECT + MIN(recorded_time) as best_time, + (SELECT MIN(recorded_time) + FROM times t2 + WHERE t2.player_id = $1 + AND t2.recorded_time > (SELECT MIN(recorded_time) FROM times t3 WHERE t3.player_id = $1) + ) as second_best_time + FROM times t + WHERE t.player_id = $1 + `, [playerId]); + + const { best_time, second_best_time } = result.rows[0]; + + if (!best_time || !second_best_time) return; + + // Berechne Verbesserung in Sekunden + const improvementSeconds = Math.floor((new Date(second_best_time) - new Date(best_time)) / 1000); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + if (improvementSeconds >= achievement.condition_value) { + await this.awardAchievement(playerId, achievement, improvementSeconds, newAchievements); + } + } + } + + /** + * Prüft Saisonale Achievements + */ + async checkSeasonalAchievements(playerId, newAchievements) { + const now = new Date(); + const currentHour = now.getHours(); + const currentMonth = now.getMonth() + 1; // JavaScript months are 0-based + const currentDayOfWeek = now.getDay(); // 0 = Sunday + + // Zeit-basierte Achievements + await this.checkTimeBasedAchievements(playerId, currentHour, currentDayOfWeek, newAchievements); + + // Monatliche Achievements + await this.checkMonthlyAchievements(playerId, currentMonth, newAchievements); + + // Jahreszeiten-Achievements + await this.checkSeasonalTimeAchievements(playerId, currentMonth, newAchievements); + } + + /** + * Prüft zeit-basierte Achievements (Wochenende, Morgen, etc.) + */ + async checkTimeBasedAchievements(playerId, currentHour, currentDayOfWeek, newAchievements) { + const timeAchievements = [ + { condition: 'weekend_play', check: () => currentDayOfWeek === 0 || currentDayOfWeek === 6 }, + { condition: 'morning_play', check: () => currentHour < 10 }, + { condition: 'afternoon_play', check: () => currentHour >= 14 && currentHour < 18 }, + { condition: 'evening_play', check: () => currentHour >= 18 } + ]; + + for (const timeAchievement of timeAchievements) { + if (!timeAchievement.check()) continue; + + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'seasonal' && a.condition_type === timeAchievement.condition); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + // Prüfe ob Spieler zu dieser Zeit gespielt hat + const hasPlayed = await this.checkTimeBasedPlay(playerId, timeAchievement.condition); + + if (hasPlayed) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + } + } + + /** + * Prüft ob Spieler zu einer bestimmten Zeit gespielt hat + */ + async checkTimeBasedPlay(playerId, conditionType) { + let query = ''; + + switch (conditionType) { + case 'weekend_play': + query = ` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(DOW FROM t.created_at AT TIME ZONE 'Europe/Berlin') IN (0, 6) + ) as has_played + `; + break; + case 'morning_play': + query = ` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(HOUR FROM t.created_at AT TIME ZONE 'Europe/Berlin') < 10 + ) as has_played + `; + break; + case 'afternoon_play': + query = ` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(HOUR FROM t.created_at AT TIME ZONE 'Europe/Berlin') BETWEEN 14 AND 17 + ) as has_played + `; + break; + case 'evening_play': + query = ` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(HOUR FROM t.created_at AT TIME ZONE 'Europe/Berlin') >= 18 + ) as has_played + `; + break; + } + + if (!query) return false; + + const result = await pool.query(query, [playerId]); + return result.rows[0].has_played; + } + + /** + * Prüft monatliche Achievements + */ + async checkMonthlyAchievements(playerId, currentMonth, newAchievements) { + const monthNames = [ + 'january', 'february', 'march', 'april', 'may', 'june', + 'july', 'august', 'september', 'october', 'november', 'december' + ]; + + const currentMonthName = monthNames[currentMonth - 1]; + + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'monthly' && a.condition_type === currentMonthName); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + // Prüfe ob Spieler in diesem Monat gespielt hat + const result = await pool.query(` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(MONTH FROM t.created_at AT TIME ZONE 'Europe/Berlin') = $2 + ) as has_played + `, [playerId, currentMonth]); + + if (result.rows[0].has_played) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + } + + /** + * Prüft Jahreszeiten-Achievements + */ + async checkSeasonalTimeAchievements(playerId, currentMonth, newAchievements) { + const season = this.getSeason(currentMonth); + + const achievements = Array.from(this.achievements.values()) + .filter(a => a.category === 'seasonal' && a.condition_type === season); + + for (const achievement of achievements) { + if (this.isAchievementCompleted(playerId, achievement.id)) continue; + + // Prüfe ob Spieler in dieser Jahreszeit gespielt hat + const monthRanges = { + spring: [3, 4, 5], + summer: [6, 7, 8], + autumn: [9, 10, 11], + winter: [12, 1, 2] + }; + + const months = monthRanges[season]; + const result = await pool.query(` + SELECT EXISTS( + SELECT 1 FROM times t + WHERE t.player_id = $1 + AND EXTRACT(MONTH FROM t.created_at AT TIME ZONE 'Europe/Berlin') = ANY($2) + ) as has_played + `, [playerId, months]); + + if (result.rows[0].has_played) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + } + + /** + * Prüft Best-Time Achievements (nur um 19:00 Uhr) + */ + async checkBestTimeAchievements(playerId, newAchievements) { + const now = new Date(); + const currentHour = now.getHours(); + const currentDayOfWeek = now.getDay(); + const currentDate = now.toISOString().split('T')[0]; + const isLastDayOfMonth = this.isLastDayOfMonth(now); + + // Nur um 19:00 Uhr prüfen + if (currentHour !== 19) return; + + // Tageskönig (jeden Tag um 19:00) + await this.checkDailyBest(playerId, currentDate, newAchievements); + + // Wochenchampion (nur Sonntag um 19:00) + if (currentDayOfWeek === 0) { + await this.checkWeeklyBest(playerId, currentDate, newAchievements); + } + + // Monatsmeister (nur am letzten Tag des Monats um 19:00) + if (isLastDayOfMonth) { + await this.checkMonthlyBest(playerId, currentDate, newAchievements); + } + } + + /** + * Prüft Tageskönig Achievement + */ + async checkDailyBest(playerId, currentDate, newAchievements) { + const achievement = Array.from(this.achievements.values()) + .find(a => a.category === 'best_time' && a.condition_type === 'daily_best'); + + if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; + + // Hole beste Zeit des Spielers heute + const playerResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE t.player_id = $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $2 + `, [playerId, currentDate]); + + // Hole beste Zeit des Tages + const dailyResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = $1 + `, [currentDate]); + + const playerBest = playerResult.rows[0].best_time; + const dailyBest = dailyResult.rows[0].best_time; + + if (playerBest && dailyBest && playerBest === dailyBest) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + + /** + * Prüft Wochenchampion Achievement + */ + async checkWeeklyBest(playerId, currentDate, newAchievements) { + const achievement = Array.from(this.achievements.values()) + .find(a => a.category === 'best_time' && a.condition_type === 'weekly_best'); + + if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; + + // Berechne Wochenstart (Montag) + const currentDateObj = new Date(currentDate); + const dayOfWeek = currentDateObj.getDay(); + const weekStart = new Date(currentDateObj); + weekStart.setDate(currentDateObj.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1)); + const weekStartStr = weekStart.toISOString().split('T')[0]; + + // Hole beste Zeit des Spielers diese Woche + const playerResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE t.player_id = $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3 + `, [playerId, weekStartStr, currentDate]); + + // Hole beste Zeit der Woche + const weeklyResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2 + `, [weekStartStr, currentDate]); + + const playerBest = playerResult.rows[0].best_time; + const weeklyBest = weeklyResult.rows[0].best_time; + + if (playerBest && weeklyBest && playerBest === weeklyBest) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + + /** + * Prüft Monatsmeister Achievement + */ + async checkMonthlyBest(playerId, currentDate, newAchievements) { + const achievement = Array.from(this.achievements.values()) + .find(a => a.category === 'best_time' && a.condition_type === 'monthly_best'); + + if (!achievement || this.isAchievementCompleted(playerId, achievement.id)) return; + + // Berechne Monatsstart + const currentDateObj = new Date(currentDate); + const monthStart = new Date(currentDateObj.getFullYear(), currentDateObj.getMonth(), 1); + const monthStartStr = monthStart.toISOString().split('T')[0]; + + // Hole beste Zeit des Spielers diesen Monat + const playerResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE t.player_id = $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $2 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $3 + `, [playerId, monthStartStr, currentDate]); + + // Hole beste Zeit des Monats + const monthlyResult = await pool.query(` + SELECT MIN(recorded_time) as best_time + FROM times t + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') >= $1 + AND DATE(t.created_at AT TIME ZONE 'Europe/Berlin') <= $2 + `, [monthStartStr, currentDate]); + + const playerBest = playerResult.rows[0].best_time; + const monthlyBest = monthlyResult.rows[0].best_time; + + if (playerBest && monthlyBest && playerBest === monthlyBest) { + await this.awardAchievement(playerId, achievement, 1, newAchievements); + } + } + + /** + * Vergibt ein Achievement an einen Spieler + * Erstellt immer einen neuen Eintrag (keine Updates mehr) + */ + async awardAchievement(playerId, achievement, progress, newAchievements) { + try { + await pool.query(` + INSERT INTO player_achievements (player_id, achievement_id, progress, is_completed, earned_at) + VALUES ($1, $2, $3, true, NOW()) + `, [playerId, achievement.id, progress]); + + newAchievements.push({ + id: achievement.id, + name: achievement.name, + description: achievement.description, + icon: achievement.icon, + points: achievement.points, + progress: progress + }); + + console.log(`🏆 Achievement vergeben: ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + } catch (error) { + console.error(`❌ Fehler beim Vergeben des Achievements ${achievement.name}:`, error); + } + } + + /** + * Prüft ob ein Achievement bereits erreicht wurde + * Jetzt können Achievements mehrmals erreicht werden, daher immer false + */ + isAchievementCompleted(playerId, achievementId) { + // Achievements können jetzt mehrmals erreicht werden + // Daher prüfen wir nicht mehr, ob sie bereits erreicht wurden + return false; + } + + /** + * Hilfsfunktionen + */ + getSeason(month) { + if (month >= 3 && month <= 5) return 'spring'; + if (month >= 6 && month <= 8) return 'summer'; + if (month >= 9 && month <= 11) return 'autumn'; + return 'winter'; + } + + isLastDayOfMonth(date) { + const tomorrow = new Date(date); + tomorrow.setDate(date.getDate() + 1); + return tomorrow.getMonth() !== date.getMonth(); + } + + /** + * Berechnet die Gesamtpunkte eines Spielers (inklusive aller Completions) + */ + async getPlayerTotalPoints(playerId) { + try { + const result = await pool.query(` + SELECT + SUM(a.points) as total_points, + COUNT(pa.id) as total_completions + FROM player_achievements pa + INNER JOIN achievements a ON pa.achievement_id = a.id + WHERE pa.player_id = $1 AND pa.is_completed = true + `, [playerId]); + + return { + totalPoints: parseInt(result.rows[0].total_points) || 0, + totalCompletions: parseInt(result.rows[0].total_completions) || 0 + }; + } catch (error) { + console.error(`❌ Fehler beim Berechnen der Gesamtpunkte für ${playerId}:`, error); + return { totalPoints: 0, totalCompletions: 0 }; + } + } + + /** + * Führt tägliche Achievement-Prüfung für alle Spieler durch + */ + async runDailyAchievementCheck() { + console.log('🎯 Starte tägliche Achievement-Prüfung...'); + + // Lade Achievements + await this.loadAchievements(); + + // Hole alle Spieler, die heute gespielt haben + const playersResult = await pool.query(` + SELECT DISTINCT p.id, p.firstname, p.lastname + FROM players p + INNER JOIN times t ON p.id = t.player_id + WHERE DATE(t.created_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE + `); + + let totalNewAchievements = 0; + const allNewAchievements = []; + + for (const player of playersResult.rows) { + console.log(`🔍 Prüfe Achievements für ${player.firstname} ${player.lastname}...`); + + const newAchievements = await this.checkAllAchievements(player.id); + totalNewAchievements += newAchievements.length; + + if (newAchievements.length > 0) { + allNewAchievements.push({ + player: `${player.firstname} ${player.lastname}`, + achievements: newAchievements + }); + } + } + + console.log(`🎉 Tägliche Achievement-Prüfung abgeschlossen!`); + console.log(`📊 ${totalNewAchievements} neue Achievements vergeben`); + + return { + totalNewAchievements, + playerAchievements: allNewAchievements + }; + } +} + +module.exports = AchievementSystem; diff --git a/package.json b/package.json index ec0fcc4..334d6ae 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "start": "node server.js", "dev": "nodemon server.js", "init-db": "node scripts/init-db.js", - "create-user": "node scripts/create-user.js" + "create-user": "node scripts/create-user.js", + "test-achievements": "node scripts/test-achievements.js", + "test-immediate-achievements": "node scripts/test-immediate-achievements.js", + "simulate-new-time": "node scripts/simulate-new-time.js", + "test-multiple-achievements": "node scripts/test-multiple-achievements.js" }, "dependencies": { "@hisma/server-puppeteer": "^0.6.5", diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 446e290..463e539 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -1121,22 +1121,29 @@ function displayAchievements() { const isCompleted = achievement.is_completed; const progress = achievement.progress || 0; const earnedAt = achievement.earned_at; + const completionCount = achievement.completion_count || 0; // Translate achievement const translatedAchievement = translateAchievement(achievement); // Debug logging if (achievement.name === 'Tageskönig') { - console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt }); + console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt, completionCount }); } let progressText = ''; if (isCompleted) { const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on'; const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed'; - progressText = earnedAt ? - `${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` : - completedText; + const timesText = currentLanguage === 'de' ? 'x geschafft' : 'x completed'; + + if (completionCount > 1) { + progressText = `${completionCount}${timesText}`; + } else { + progressText = earnedAt ? + `${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` : + completedText; + } } else if (progress > 0) { // Show progress for incomplete achievements const conditionValue = getAchievementConditionValue(achievement.name); @@ -1146,6 +1153,7 @@ function displayAchievements() { } const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points'; + const totalPoints = completionCount > 0 ? achievement.points * completionCount : achievement.points; return `
${translatedAchievement.name}

${translatedAchievement.description}

- +${achievement.points} ${pointsText} + +${totalPoints} ${pointsText} ${progressText ? `${progressText}` : ''}
diff --git a/routes/api.js b/routes/api.js index 0a3a7c6..1085e63 100644 --- a/routes/api.js +++ b/routes/api.js @@ -874,6 +874,23 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => { [rfiduid, location_id, recorded_time, new Date()] ); + // Achievement-Überprüfung nach Zeit-Eingabe (JavaScript) + try { + const AchievementSystem = require('../lib/achievementSystem'); + const achievementSystem = new AchievementSystem(); + const newAchievements = await achievementSystem.checkImmediateAchievements(result.rows[0].player_id); + + if (newAchievements.length > 0) { + console.log(`🏆 ${newAchievements.length} neue Achievements für Spieler ${result.rows[0].player_id}:`); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } + } catch (achievementError) { + console.error('Fehler bei Achievement-Check:', achievementError); + // Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren + } + // WebSocket-Event senden für Live-Updates const io = req.app.get('io'); if (io) { @@ -2503,10 +2520,18 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => { [player_id, location_id, timeInterval] ); - // Achievement-Überprüfung nach Zeit-Eingabe + // Achievement-Überprüfung nach Zeit-Eingabe (JavaScript) try { - await pool.query('SELECT check_immediate_achievements($1)', [player_id]); - console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`); + const AchievementSystem = require('../lib/achievementSystem'); + const achievementSystem = new AchievementSystem(); + const newAchievements = await achievementSystem.checkImmediateAchievements(player_id); + + if (newAchievements.length > 0) { + console.log(`🏆 ${newAchievements.length} neue Achievements für Spieler ${player_id}:`); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } } catch (achievementError) { console.error('Fehler bei Achievement-Check:', achievementError); // Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren @@ -3048,14 +3073,16 @@ router.get('/achievements/player/:playerId', async (req, res) => { a.category, a.icon, a.points, - COALESCE(pa.progress, 0) as progress, - COALESCE(pa.is_completed, false) as is_completed, - pa.earned_at + COALESCE(MAX(pa.progress), 0) as progress, + COALESCE(COUNT(pa.id) > 0, false) as is_completed, + MAX(pa.earned_at) as earned_at, + COUNT(pa.id) as completion_count FROM achievements a - LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1 + LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1 AND pa.is_completed = true WHERE a.is_active = true + GROUP BY a.id, a.name, a.name_en, a.description, a.description_en, a.category, a.icon, a.points ORDER BY - pa.is_completed DESC, + is_completed DESC, a.category, a.points DESC `, [playerId]); @@ -3092,10 +3119,10 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => { WHERE is_active = true `); - // Get player's earned achievements + // Get player's earned achievements (count unique achievements, not total completions) const playerResult = await pool.query(` SELECT - COUNT(CASE WHEN pa.is_completed = true THEN 1 END) as completed_achievements, + COUNT(DISTINCT CASE WHEN pa.is_completed = true THEN pa.achievement_id END) as completed_achievements, SUM(CASE WHEN pa.is_completed = true THEN a.points ELSE 0 END) as total_points, COUNT(CASE WHEN pa.is_completed = true AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE THEN 1 END) as achievements_today FROM achievements a diff --git a/scripts/simulate-new-time.js b/scripts/simulate-new-time.js new file mode 100644 index 0000000..8944e26 --- /dev/null +++ b/scripts/simulate-new-time.js @@ -0,0 +1,94 @@ +/** + * Simuliert das Aufzeichnen einer neuen Zeit und testet sofortige Achievements + * + * Dieses Script simuliert, was passiert, wenn ein Spieler eine neue Zeit aufzeichnet + */ + +const { Pool } = require('pg'); +const AchievementSystem = require('../lib/achievementSystem'); +require('dotenv').config(); + +// Database connection +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false +}); + +async function simulateNewTime() { + console.log('🎮 Simuliere neue Zeit-Aufzeichnung...\n'); + + try { + // Hole einen Test-Spieler + const playerResult = await pool.query(` + SELECT p.id, p.firstname, p.lastname, p.rfiduid + FROM players p + WHERE p.rfiduid IS NOT NULL + LIMIT 1 + `); + + if (playerResult.rows.length === 0) { + console.log('❌ Kein Spieler mit RFID gefunden'); + return; + } + + const player = playerResult.rows[0]; + console.log(`👤 Teste mit Spieler: ${player.firstname} ${player.lastname} (${player.rfiduid})`); + + // Hole eine Test-Location + const locationResult = await pool.query(` + SELECT id, name FROM locations LIMIT 1 + `); + + if (locationResult.rows.length === 0) { + console.log('❌ Keine Location gefunden'); + return; + } + + const location = locationResult.rows[0]; + console.log(`📍 Teste mit Location: ${location.name}`); + + // Simuliere eine neue Zeit (etwas langsamer als die beste Zeit) + const testTime = '00:01:30.500'; // 1:30.500 + console.log(`⏱️ Simuliere Zeit: ${testTime}`); + + // Füge die Zeit zur Datenbank hinzu + const timeResult = await pool.query( + `INSERT INTO times (player_id, location_id, recorded_time, created_at) + VALUES ($1, $2, $3, $4) + RETURNING id, player_id, created_at`, + [player.id, location.id, testTime, new Date()] + ); + + console.log(`✅ Zeit erfolgreich gespeichert (ID: ${timeResult.rows[0].id})`); + + // Teste sofortige Achievements + console.log('\n🏆 Prüfe sofortige Achievements...'); + const achievementSystem = new AchievementSystem(); + await achievementSystem.loadAchievements(); + + const newAchievements = await achievementSystem.checkImmediateAchievements(player.id); + + if (newAchievements.length > 0) { + console.log(`\n🎉 ${newAchievements.length} neue Achievements vergeben:`); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } else { + console.log('\nℹ️ Keine neuen Achievements vergeben'); + } + + console.log('\n✅ Simulation erfolgreich abgeschlossen!'); + + } catch (error) { + console.error('❌ Simulation fehlgeschlagen:', error); + } finally { + await pool.end(); + } +} + +// Führe Simulation aus +simulateNewTime(); diff --git a/scripts/test-achievements.js b/scripts/test-achievements.js new file mode 100644 index 0000000..a815186 --- /dev/null +++ b/scripts/test-achievements.js @@ -0,0 +1,50 @@ +/** + * Test Script für das JavaScript Achievement System + * + * Testet die Achievement-Logik ohne auf 19:00 Uhr zu warten + */ + +const AchievementSystem = require('../lib/achievementSystem'); +require('dotenv').config(); + +async function testAchievementSystem() { + console.log('🧪 Starte Achievement-System Test...\n'); + + try { + const achievementSystem = new AchievementSystem(); + + // Lade Achievements + console.log('📋 Lade Achievements...'); + await achievementSystem.loadAchievements(); + + // Führe tägliche Achievement-Prüfung durch + console.log('\n🎯 Führe tägliche Achievement-Prüfung durch...'); + const result = await achievementSystem.runDailyAchievementCheck(); + + // Zeige Ergebnisse + console.log('\n📊 Test-Ergebnisse:'); + console.log(` 🏆 ${result.totalNewAchievements} neue Achievements vergeben`); + console.log(` 👥 ${result.playerAchievements.length} Spieler haben neue Achievements erhalten`); + + if (result.playerAchievements.length > 0) { + console.log('\n📋 Neue Achievements im Detail:'); + result.playerAchievements.forEach(player => { + console.log(` 👤 ${player.player}:`); + player.achievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + }); + } else { + console.log('\nℹ️ Keine neuen Achievements vergeben'); + } + + console.log('\n✅ Test erfolgreich abgeschlossen!'); + + } catch (error) { + console.error('❌ Test fehlgeschlagen:', error); + process.exit(1); + } +} + +// Führe Test aus +testAchievementSystem(); diff --git a/scripts/test-immediate-achievements.js b/scripts/test-immediate-achievements.js new file mode 100644 index 0000000..48c0681 --- /dev/null +++ b/scripts/test-immediate-achievements.js @@ -0,0 +1,48 @@ +/** + * Test Script für sofortige Achievements + * + * Testet die sofortige Achievement-Logik für einen einzelnen Spieler + */ + +const AchievementSystem = require('../lib/achievementSystem'); +require('dotenv').config(); + +async function testImmediateAchievements() { + console.log('⚡ Starte sofortige Achievement-Test...\n'); + + try { + const achievementSystem = new AchievementSystem(); + + // Lade Achievements + console.log('📋 Lade Achievements...'); + await achievementSystem.loadAchievements(); + + // Teste mit einem spezifischen Spieler (Carsten Graf) + const testPlayerId = '313ceee3-8040-44b4-98d2-e63703579e5d'; + + console.log(`\n🎯 Teste sofortige Achievements für Spieler ${testPlayerId}...`); + const newAchievements = await achievementSystem.checkImmediateAchievements(testPlayerId); + + // Zeige Ergebnisse + console.log('\n📊 Sofortige Achievement-Test Ergebnisse:'); + console.log(` 🏆 ${newAchievements.length} neue sofortige Achievements vergeben`); + + if (newAchievements.length > 0) { + console.log('\n📋 Neue sofortige Achievements:'); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } else { + console.log('\nℹ️ Keine neuen sofortigen Achievements vergeben'); + } + + console.log('\n✅ Sofortige Achievement-Test erfolgreich abgeschlossen!'); + + } catch (error) { + console.error('❌ Test fehlgeschlagen:', error); + process.exit(1); + } +} + +// Führe Test aus +testImmediateAchievements(); diff --git a/scripts/test-multiple-achievements.js b/scripts/test-multiple-achievements.js new file mode 100644 index 0000000..caa6937 --- /dev/null +++ b/scripts/test-multiple-achievements.js @@ -0,0 +1,75 @@ +/** + * Test Script für mehrfache Achievements + * + * Demonstriert, wie Achievements mehrmals erreicht werden können + * und wie die Gesamtpunkte berechnet werden + */ + +const AchievementSystem = require('../lib/achievementSystem'); +require('dotenv').config(); + +async function testMultipleAchievements() { + console.log('🔄 Teste mehrfache Achievements...\n'); + + try { + const achievementSystem = new AchievementSystem(); + + // Lade Achievements + console.log('📋 Lade Achievements...'); + await achievementSystem.loadAchievements(); + + // Teste mit Carsten Graf + const testPlayerId = '313ceee3-8040-44b4-98d2-e63703579e5d'; + + console.log(`\n👤 Teste mit Spieler: ${testPlayerId}`); + + // Zeige aktuelle Punkte + console.log('\n📊 Aktuelle Gesamtpunkte:'); + const currentPoints = await achievementSystem.getPlayerTotalPoints(testPlayerId); + console.log(` 🏆 Gesamtpunkte: ${currentPoints.totalPoints}`); + console.log(` 🔢 Gesamt-Completions: ${currentPoints.totalCompletions}`); + + // Führe Achievement-Check durch + console.log('\n🎯 Führe Achievement-Check durch...'); + const newAchievements = await achievementSystem.checkImmediateAchievements(testPlayerId); + + if (newAchievements.length > 0) { + console.log(`\n🏆 ${newAchievements.length} neue Achievements vergeben:`); + newAchievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + } else { + console.log('\nℹ️ Keine neuen Achievements vergeben'); + } + + // Zeige neue Gesamtpunkte + console.log('\n📊 Neue Gesamtpunkte:'); + const newPoints = await achievementSystem.getPlayerTotalPoints(testPlayerId); + console.log(` 🏆 Gesamtpunkte: ${newPoints.totalPoints} (${newPoints.totalPoints - currentPoints.totalPoints > 0 ? '+' : ''}${newPoints.totalPoints - currentPoints.totalPoints})`); + console.log(` 🔢 Gesamt-Completions: ${newPoints.totalCompletions} (${newPoints.totalCompletions - currentPoints.totalCompletions > 0 ? '+' : ''}${newPoints.totalCompletions - currentPoints.totalCompletions})`); + + // Zeige alle Achievements mit Completions + console.log('\n📋 Alle Achievements mit Completions:'); + await achievementSystem.loadPlayerAchievements(testPlayerId); + const playerAchievements = achievementSystem.playerAchievements.get(testPlayerId); + + if (playerAchievements && playerAchievements.size > 0) { + for (const [achievementId, data] of playerAchievements) { + const achievement = Array.from(achievementSystem.achievements.values()) + .find(a => a.id === achievementId); + if (achievement) { + console.log(` ${achievement.icon} ${achievement.name}: ${data.completion_count}x (${data.completion_count * achievement.points} Punkte)`); + } + } + } + + console.log('\n✅ Mehrfache Achievement-Test erfolgreich abgeschlossen!'); + + } catch (error) { + console.error('❌ Test fehlgeschlagen:', error); + process.exit(1); + } +} + +// Führe Test aus +testMultipleAchievements(); diff --git a/server.js b/server.js index a6271ff..bd5bded 100644 --- a/server.js +++ b/server.js @@ -23,11 +23,15 @@ const { createServer } = require('http'); const { Server } = require('socket.io'); const swaggerUi = require('swagger-ui-express'); const swaggerSpecs = require('./swagger'); +const cron = require('node-cron'); require('dotenv').config(); // Route Imports const { router: apiRoutes, requireApiKey } = require('./routes/api'); +// Achievement System +const AchievementSystem = require('./lib/achievementSystem'); + // ============================================================================ // SERVER CONFIGURATION // ============================================================================ @@ -279,6 +283,55 @@ server.listen(port, () => { console.log(` 👑 Admin: /api/v1/admin/`); }); +// ============================================================================ +// SCHEDULED TASKS +// ============================================================================ + +/** + * Scheduled Function - Runs daily at 7 PM (19:00) + * Führt tägliche Achievement-Prüfung durch + */ +async function scheduledTaskAt7PM() { + const now = new Date(); + console.log(`⏰ Geplante Aufgabe ausgeführt um ${now.toLocaleString('de-DE')}`); + + try { + // Initialisiere Achievement-System + const achievementSystem = new AchievementSystem(); + + // Führe tägliche Achievement-Prüfung durch + const result = await achievementSystem.runDailyAchievementCheck(); + + console.log(`🏆 Achievement-Prüfung abgeschlossen:`); + console.log(` 📊 ${result.totalNewAchievements} neue Achievements vergeben`); + console.log(` 👥 ${result.playerAchievements.length} Spieler haben neue Achievements erhalten`); + + // Zeige Details der neuen Achievements + if (result.playerAchievements.length > 0) { + console.log(`\n📋 Neue Achievements im Detail:`); + result.playerAchievements.forEach(player => { + console.log(` 👤 ${player.player}:`); + player.achievements.forEach(achievement => { + console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); + }); + }); + } + + console.log('✅ Geplante Aufgabe erfolgreich abgeschlossen'); + } catch (error) { + console.error('❌ Fehler bei der geplanten Aufgabe:', error); + } +} + +// Cron Job: Täglich um 19:00 Uhr (7 PM) +// Format: Sekunde Minute Stunde Tag Monat Wochentag +cron.schedule('0 0 19 * * *', scheduledTaskAt7PM, { + scheduled: true, + timezone: "Europe/Berlin" // Deutsche Zeitzone +}); + +console.log('📅 Geplante Aufgabe eingerichtet: Täglich um 19:00 Uhr'); + // ============================================================================ // GRACEFUL SHUTDOWN // ============================================================================