Achivement System

This commit is contained in:
2025-09-05 17:56:23 +02:00
parent a78a8dc3ce
commit 61d5ef2e6f
11 changed files with 2195 additions and 7 deletions

View File

@@ -886,6 +886,15 @@ router.post('/v1/private/create-time', requireApiKey, async (req, res) => {
[player_id, location_id, recorded_time, new Date()]
);
// Achievement-Überprüfung nach Zeit-Eingabe
try {
await pool.query('SELECT check_all_achievements($1)', [player_id]);
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
} catch (achievementError) {
console.error('Fehler bei Achievement-Check:', achievementError);
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
}
// WebSocket-Event senden für Live-Updates
@@ -1202,7 +1211,10 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
ORDER BY t.created_at DESC
`, [player_id]);
res.json(timesResult.rows);
res.json({
success: true,
data: timesResult.rows
});
} catch (error) {
console.error('Fehler beim Abrufen der Benutzerzeiten:', error);
@@ -1213,6 +1225,47 @@ router.get('/v1/public/user-times/:supabase_user_id', async (req, res) => {
}
});
/**
* @swagger
* /api/v1/public/user-player/{supabase_user_id}:
* get:
* summary: Spieler-Info anhand der Supabase User ID abrufen
* description: Ruft Spieler-Informationen für das Dashboard ab
* tags: [Public API]
* parameters:
* - in: path
* name: supabase_user_id
* required: true
* schema:
* type: string
* format: uuid
* description: Supabase User ID
* responses:
* 200:
* description: Spieler-Informationen
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/Player'
* 404:
* description: Spieler nicht gefunden
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get player info by Supabase user ID (no auth required for dashboard)
router.get('/v1/public/user-player/:supabase_user_id', async (req, res) => {
const { supabase_user_id } = req.params;
@@ -1685,6 +1738,75 @@ router.get('/v1/public/locations', async (req, res) => {
}
});
/**
* @swagger
* /api/v1/public/times:
* get:
* summary: Alle Zeiten mit Standort-Informationen abrufen
* description: Ruft alle aufgezeichneten Zeiten mit Standort-Details ab
* tags: [Public API]
* parameters:
* - in: query
* name: location
* schema:
* type: string
* description: Filter nach Standort-ID
* - in: query
* name: limit
* schema:
* type: integer
* default: 100
* description: Maximale Anzahl der Ergebnisse
* responses:
* 200:
* description: Liste aller Zeiten
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* type: object
* properties:
* id:
* type: string
* format: uuid
* player_id:
* type: string
* format: uuid
* location_id:
* type: string
* format: uuid
* recorded_time:
* type: object
* properties:
* seconds:
* type: number
* minutes:
* type: number
* milliseconds:
* type: number
* created_at:
* type: string
* format: date-time
* location_name:
* type: string
* latitude:
* type: number
* longitude:
* type: number
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Public route to get times for location with parameter
router.get('/v1/public/times', async (req, res) => {
const { location } = req.query;
@@ -2022,6 +2144,15 @@ router.post('/v1/admin/runs', requireAdminAuth, async (req, res) => {
[player_id, location_id, timeInterval]
);
// Achievement-Überprüfung nach Zeit-Eingabe
try {
await pool.query('SELECT check_all_achievements($1)', [player_id]);
console.log(`✅ Achievement-Check für Spieler ${player_id} ausgeführt`);
} catch (achievementError) {
console.error('Fehler bei Achievement-Check:', achievementError);
// Achievement-Fehler sollen die Zeit-Eingabe nicht blockieren
}
res.json({
success: true,
message: 'Lauf erfolgreich hinzugefügt',
@@ -2169,4 +2300,277 @@ router.put('/v1/admin/adminusers/:id', requireAdminAuth, async (req, res) => {
}
});
// ==================== ACHIEVEMENT ENDPOINTS ====================
/**
* @swagger
* /api/achievements:
* get:
* summary: Alle verfügbaren Achievements abrufen
* description: Ruft alle aktiven Achievements im System ab
* tags: [Achievements]
* responses:
* 200:
* description: Liste aller Achievements
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* $ref: '#/components/schemas/Achievement'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get all achievements
router.get('/achievements', async (req, res) => {
try {
const result = await pool.query(`
SELECT id, name, description, category, icon, points, is_active
FROM achievements
WHERE is_active = true
ORDER BY category, points DESC
`);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('Error fetching achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Achievements'
});
}
});
/**
* @swagger
* /api/achievements/player/{playerId}:
* get:
* summary: Achievements eines Spielers abrufen
* description: Ruft alle Achievements für einen bestimmten Spieler ab
* tags: [Achievements]
* parameters:
* - in: path
* name: playerId
* required: true
* schema:
* type: string
* format: uuid
* description: Eindeutige Spieler-ID
* responses:
* 200:
* description: Liste der Spieler-Achievements
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: array
* items:
* allOf:
* - $ref: '#/components/schemas/Achievement'
* - $ref: '#/components/schemas/PlayerAchievement'
* 500:
* description: Server-Fehler
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// Get player achievements
router.get('/achievements/player/:playerId', async (req, res) => {
try {
const { playerId } = req.params;
const result = await pool.query(`
SELECT
a.id,
a.name,
a.description,
a.category,
a.icon,
a.points,
pa.progress,
pa.is_completed,
pa.earned_at
FROM achievements a
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
WHERE a.is_active = true
ORDER BY
pa.is_completed DESC,
a.category,
a.points DESC
`, [playerId]);
res.json({
success: true,
data: result.rows
});
} catch (error) {
console.error('Error fetching player achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Spieler-Achievements'
});
}
});
// Get player achievement statistics
router.get('/achievements/player/:playerId/stats', async (req, res) => {
try {
const { playerId } = req.params;
const result = await pool.query(`
SELECT
COUNT(pa.id) as total_achievements,
COUNT(CASE WHEN pa.is_completed = true THEN 1 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
LEFT JOIN player_achievements pa ON a.id = pa.achievement_id AND pa.player_id = $1
WHERE a.is_active = true
`, [playerId]);
const stats = result.rows[0];
res.json({
success: true,
data: {
total_achievements: parseInt(stats.total_achievements),
completed_achievements: parseInt(stats.completed_achievements),
total_points: parseInt(stats.total_points) || 0,
achievements_today: parseInt(stats.achievements_today),
completion_percentage: stats.total_achievements > 0 ?
Math.round((stats.completed_achievements / stats.total_achievements) * 100) : 0
}
});
} catch (error) {
console.error('Error fetching player achievement stats:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Achievement-Statistiken'
});
}
});
// Check achievements for a specific player
router.post('/achievements/check/:playerId', async (req, res) => {
try {
const { playerId } = req.params;
// Verify player exists
const playerCheck = await pool.query('SELECT id FROM players WHERE id = $1', [playerId]);
if (playerCheck.rows.length === 0) {
return res.status(404).json({
success: false,
message: 'Spieler nicht gefunden'
});
}
// Run achievement check
await pool.query('SELECT check_all_achievements($1)', [playerId]);
// Get newly earned achievements
const newAchievements = await pool.query(`
SELECT a.name, a.description, a.icon, a.points
FROM player_achievements pa
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.player_id = $1
AND pa.is_completed = true
AND DATE(pa.earned_at AT TIME ZONE 'Europe/Berlin') = CURRENT_DATE
ORDER BY pa.earned_at DESC
`, [playerId]);
res.json({
success: true,
message: 'Achievement-Check abgeschlossen',
data: {
new_achievements: newAchievements.rows,
count: newAchievements.rows.length
}
});
} catch (error) {
console.error('Error checking achievements:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Überprüfen der Achievements'
});
}
});
// Run daily achievement check for all players
router.post('/achievements/daily-check', async (req, res) => {
try {
// This endpoint runs the daily achievement check
const { runDailyAchievements } = require('../scripts/daily_achievements');
await runDailyAchievements();
res.json({
success: true,
message: 'Tägliche Achievement-Überprüfung abgeschlossen'
});
} catch (error) {
console.error('Error running daily achievement check:', error);
res.status(500).json({
success: false,
message: 'Fehler bei der täglichen Achievement-Überprüfung'
});
}
});
// Get achievement leaderboard
router.get('/achievements/leaderboard', async (req, res) => {
try {
const { limit = 10 } = req.query;
const result = await pool.query(`
SELECT
p.firstname,
p.lastname,
COUNT(pa.id) as completed_achievements,
SUM(a.points) as total_points
FROM players p
INNER JOIN player_achievements pa ON p.id = pa.player_id
INNER JOIN achievements a ON pa.achievement_id = a.id
WHERE pa.is_completed = true
GROUP BY p.id, p.firstname, p.lastname
ORDER BY total_points DESC, completed_achievements DESC
LIMIT $1
`, [parseInt(limit)]);
res.json({
success: true,
data: result.rows.map((row, index) => ({
rank: index + 1,
name: `${row.firstname} ${row.lastname}`,
completed_achievements: parseInt(row.completed_achievements),
total_points: parseInt(row.total_points)
}))
});
} catch (error) {
console.error('Error fetching achievement leaderboard:', error);
res.status(500).json({
success: false,
message: 'Fehler beim Laden der Bestenliste'
});
}
});
module.exports = { router, requireApiKey };