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;
|
||||||
@@ -7,7 +7,11 @@
|
|||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon server.js",
|
||||||
"init-db": "node scripts/init-db.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": {
|
"dependencies": {
|
||||||
"@hisma/server-puppeteer": "^0.6.5",
|
"@hisma/server-puppeteer": "^0.6.5",
|
||||||
|
|||||||
@@ -1121,22 +1121,29 @@ function displayAchievements() {
|
|||||||
const isCompleted = achievement.is_completed;
|
const isCompleted = achievement.is_completed;
|
||||||
const progress = achievement.progress || 0;
|
const progress = achievement.progress || 0;
|
||||||
const earnedAt = achievement.earned_at;
|
const earnedAt = achievement.earned_at;
|
||||||
|
const completionCount = achievement.completion_count || 0;
|
||||||
|
|
||||||
// Translate achievement
|
// Translate achievement
|
||||||
const translatedAchievement = translateAchievement(achievement);
|
const translatedAchievement = translateAchievement(achievement);
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
if (achievement.name === 'Tageskönig') {
|
if (achievement.name === 'Tageskönig') {
|
||||||
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt });
|
console.log('Tageskönig Debug:', { isCompleted, progress, earnedAt, completionCount });
|
||||||
}
|
}
|
||||||
|
|
||||||
let progressText = '';
|
let progressText = '';
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on';
|
const achievedText = currentLanguage === 'de' ? 'Erreicht am' : 'Achieved on';
|
||||||
const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed';
|
const completedText = currentLanguage === 'de' ? 'Abgeschlossen' : 'Completed';
|
||||||
progressText = earnedAt ?
|
const timesText = currentLanguage === 'de' ? 'x geschafft' : 'x completed';
|
||||||
`${achievedText} ${new Date(earnedAt).toLocaleDateString(currentLanguage === 'de' ? 'de-DE' : 'en-US')}` :
|
|
||||||
completedText;
|
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) {
|
} else if (progress > 0) {
|
||||||
// Show progress for incomplete achievements
|
// Show progress for incomplete achievements
|
||||||
const conditionValue = getAchievementConditionValue(achievement.name);
|
const conditionValue = getAchievementConditionValue(achievement.name);
|
||||||
@@ -1146,6 +1153,7 @@ function displayAchievements() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points';
|
const pointsText = currentLanguage === 'de' ? 'Punkte' : 'Points';
|
||||||
|
const totalPoints = completionCount > 0 ? achievement.points * completionCount : achievement.points;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
<div class="achievement-card ${isCompleted ? 'completed' : 'incomplete'}"
|
||||||
@@ -1155,7 +1163,7 @@ function displayAchievements() {
|
|||||||
<h4 class="achievement-name">${translatedAchievement.name}</h4>
|
<h4 class="achievement-name">${translatedAchievement.name}</h4>
|
||||||
<p class="achievement-description">${translatedAchievement.description}</p>
|
<p class="achievement-description">${translatedAchievement.description}</p>
|
||||||
<div class="achievement-meta">
|
<div class="achievement-meta">
|
||||||
<span class="achievement-points">+${achievement.points} ${pointsText}</span>
|
<span class="achievement-points">+${totalPoints} ${pointsText}</span>
|
||||||
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
${progressText ? `<span class="achievement-progress">${progressText}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -874,6 +874,23 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
|
|||||||
[rfiduid, location_id, recorded_time, new Date()]
|
[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
|
// WebSocket-Event senden für Live-Updates
|
||||||
const io = req.app.get('io');
|
const io = req.app.get('io');
|
||||||
if (io) {
|
if (io) {
|
||||||
@@ -2503,10 +2520,18 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
|
|||||||
[player_id, location_id, timeInterval]
|
[player_id, location_id, timeInterval]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Achievement-Überprüfung nach Zeit-Eingabe
|
// Achievement-Überprüfung nach Zeit-Eingabe (JavaScript)
|
||||||
try {
|
try {
|
||||||
await pool.query('SELECT check_immediate_achievements($1)', [player_id]);
|
const AchievementSystem = require('../lib/achievementSystem');
|
||||||
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
|
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) {
|
} catch (achievementError) {
|
||||||
console.error('Fehler bei Achievement-Check:', achievementError);
|
console.error('Fehler bei Achievement-Check:', achievementError);
|
||||||
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
|
||||||
@@ -3048,14 +3073,16 @@ router.get('/achievements/player/:playerId', async (req, res) => {
|
|||||||
a.category,
|
a.category,
|
||||||
a.icon,
|
a.icon,
|
||||||
a.points,
|
a.points,
|
||||||
COALESCE(pa.progress, 0) as progress,
|
COALESCE(MAX(pa.progress), 0) as progress,
|
||||||
COALESCE(pa.is_completed, false) as is_completed,
|
COALESCE(COUNT(pa.id) > 0, false) as is_completed,
|
||||||
pa.earned_at
|
MAX(pa.earned_at) as earned_at,
|
||||||
|
COUNT(pa.id) as completion_count
|
||||||
FROM achievements a
|
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
|
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
|
ORDER BY
|
||||||
pa.is_completed DESC,
|
is_completed DESC,
|
||||||
a.category,
|
a.category,
|
||||||
a.points DESC
|
a.points DESC
|
||||||
`, [playerId]);
|
`, [playerId]);
|
||||||
@@ -3092,10 +3119,10 @@ router.get('/achievements/player/:playerId/stats', async (req, res) => {
|
|||||||
WHERE is_active = true
|
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(`
|
const playerResult = await pool.query(`
|
||||||
SELECT
|
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,
|
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
|
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
|
FROM achievements a
|
||||||
|
|||||||
94
scripts/simulate-new-time.js
Normal file
94
scripts/simulate-new-time.js
Normal file
@@ -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();
|
||||||
50
scripts/test-achievements.js
Normal file
50
scripts/test-achievements.js
Normal file
@@ -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();
|
||||||
48
scripts/test-immediate-achievements.js
Normal file
48
scripts/test-immediate-achievements.js
Normal file
@@ -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();
|
||||||
75
scripts/test-multiple-achievements.js
Normal file
75
scripts/test-multiple-achievements.js
Normal file
@@ -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();
|
||||||
53
server.js
53
server.js
@@ -23,11 +23,15 @@ const { createServer } = require('http');
|
|||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const swaggerUi = require('swagger-ui-express');
|
const swaggerUi = require('swagger-ui-express');
|
||||||
const swaggerSpecs = require('./swagger');
|
const swaggerSpecs = require('./swagger');
|
||||||
|
const cron = require('node-cron');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
// Route Imports
|
// Route Imports
|
||||||
const { router: apiRoutes, requireApiKey } = require('./routes/api');
|
const { router: apiRoutes, requireApiKey } = require('./routes/api');
|
||||||
|
|
||||||
|
// Achievement System
|
||||||
|
const AchievementSystem = require('./lib/achievementSystem');
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// SERVER CONFIGURATION
|
// SERVER CONFIGURATION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -279,6 +283,55 @@ server.listen(port, () => {
|
|||||||
console.log(` 👑 Admin: /api/v1/admin/`);
|
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
|
// GRACEFUL SHUTDOWN
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user