Komplettes AdivmentSystem überarbeitet!
This commit is contained in:
712
lib/achievementSystem.js
Normal file
712
lib/achievementSystem.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user