diff --git a/backend/app.js b/backend/app.js index 766d8ab..77d5c65 100644 --- a/backend/app.js +++ b/backend/app.js @@ -44,7 +44,7 @@ app.use('/api/admin', adminRouter); app.use('/api/match3', match3Router); app.use('/api/taxi', taxiRouter); app.use('/api/taxi-maps', taxiMapRouter); -app.use('/api/taxi/highscore', taxiHighscoreRouter); +app.use('/api/taxi/highscores', taxiHighscoreRouter); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); app.use('/api/contact', contactRouter); app.use('/api/socialnetwork', socialnetworkRouter); diff --git a/backend/controllers/taxiHighscoreController.js b/backend/controllers/taxiHighscoreController.js index a285ee1..48488fc 100644 --- a/backend/controllers/taxiHighscoreController.js +++ b/backend/controllers/taxiHighscoreController.js @@ -17,7 +17,7 @@ class TaxiHighscoreController { } const highscoreData = { - userId: parseInt(userId), + hashedUserId: userId, // userId ist bereits ein String (Hash) nickname, passengersDelivered: parseInt(passengersDelivered), playtime: parseInt(playtime), @@ -75,18 +75,17 @@ class TaxiHighscoreController { */ async getUserBestScores(req, res) { try { - const { userId } = req.params; - const { mapId } = req.query; + const { userId, mapId } = req.query; if (!userId) { return res.status(400).json({ - success: false, + success: false, message: 'userId ist erforderlich' }); } const bestScores = await taxiHighscoreService.getUserBestScores( - parseInt(userId), + userId, // userId ist bereits ein String (Hash) mapId ? parseInt(mapId) : null ); @@ -109,8 +108,7 @@ class TaxiHighscoreController { */ async getUserHighscores(req, res) { try { - const { userId } = req.params; - const { limit = 20 } = req.query; + const { userId, limit = 20 } = req.query; if (!userId) { return res.status(400).json({ @@ -120,7 +118,7 @@ class TaxiHighscoreController { } const highscores = await taxiHighscoreService.getUserHighscores( - parseInt(userId), + userId, // userId ist bereits ein String (Hash) parseInt(limit) ); @@ -143,8 +141,7 @@ class TaxiHighscoreController { */ async getUserRank(req, res) { try { - const { userId } = req.params; - const { mapId, orderBy = 'points' } = req.query; + const { userId, mapId, orderBy = 'points' } = req.query; if (!userId) { return res.status(400).json({ @@ -154,7 +151,7 @@ class TaxiHighscoreController { } const rank = await taxiHighscoreService.getUserRank( - parseInt(userId), + userId, // userId ist bereits ein String (Hash) mapId ? parseInt(mapId) : null, orderBy ); diff --git a/backend/routers/taxiHighscoreRouter.js b/backend/routers/taxiHighscoreRouter.js index e1375bc..ac74f10 100644 --- a/backend/routers/taxiHighscoreRouter.js +++ b/backend/routers/taxiHighscoreRouter.js @@ -2,11 +2,23 @@ import express from 'express'; import taxiHighscoreController from '../controllers/taxiHighscoreController.js'; const router = express.Router(); + +// POST /api/taxi/highscores - Neuen Highscore erstellen router.post('/', taxiHighscoreController.createHighscore); -router.get('/top', taxiHighscoreController.getTopHighscores); -router.get('/my-best', taxiHighscoreController.getUserBestScores); -router.get('/my-scores', taxiHighscoreController.getUserHighscores); -router.get('/my-rank', taxiHighscoreController.getUserRank); + +// GET /api/taxi/highscores - Top Highscores abrufen +router.get('/', taxiHighscoreController.getTopHighscores); + +// GET /api/taxi/highscores/rank - Rang des Benutzers abrufen +router.get('/rank', taxiHighscoreController.getUserRank); + +// GET /api/taxi/highscores/user/best - Beste Punkte des Benutzers +router.get('/user/best', taxiHighscoreController.getUserBestScores); + +// GET /api/taxi/highscores/user - Alle Highscores des Benutzers +router.get('/user', taxiHighscoreController.getUserHighscores); + +// GET /api/taxi/highscores/stats - Highscore-Statistiken router.get('/stats', taxiHighscoreController.getHighscoreStats); export default router; diff --git a/backend/services/taxiHighscoreService.js b/backend/services/taxiHighscoreService.js index 6739b18..cd2b3e0 100644 --- a/backend/services/taxiHighscoreService.js +++ b/backend/services/taxiHighscoreService.js @@ -13,7 +13,7 @@ class TaxiHighscoreService extends BaseService { * Speichert oder aktualisiert einen Highscore-Eintrag * Jeder User kann nur einen Eintrag pro Map haben (der beste wird gespeichert) * @param {Object} highscoreData - Die Highscore-Daten - * @param {number} highscoreData.userId - ID des Users + * @param {string} highscoreData.hashedUserId - Hash-ID des Users * @param {string} highscoreData.nickname - Nickname des Users * @param {number} highscoreData.passengersDelivered - Anzahl abgelieferter Passagiere * @param {number} highscoreData.playtime - Spielzeit in Sekunden @@ -24,10 +24,14 @@ class TaxiHighscoreService extends BaseService { */ async createHighscore(highscoreData) { try { + // Hash-ID zu echter User-ID konvertieren + const user = await this.getUserByHashedId(highscoreData.hashedUserId); + const userId = user.id; + // Prüfen ob bereits ein Eintrag für diesen User und diese Map existiert const existingHighscore = await this.model.findOne({ where: { - userId: highscoreData.userId, + userId: userId, mapId: highscoreData.mapId } }); @@ -50,7 +54,7 @@ class TaxiHighscoreService extends BaseService { } else { // Kein existierender Eintrag, neuen erstellen const highscore = await this.model.create({ - userId: highscoreData.userId, + userId: userId, nickname: highscoreData.nickname, passengersDelivered: highscoreData.passengersDelivered, playtime: highscoreData.playtime, @@ -107,12 +111,16 @@ class TaxiHighscoreService extends BaseService { /** * Holt die persönlichen Bestleistungen eines Users - * @param {number} userId - ID des Users + * @param {string} hashedUserId - Hash-ID des Users * @param {number} mapId - ID der Map (optional) * @returns {Promise} Bestleistungen des Users */ - async getUserBestScores(userId, mapId = null) { + async getUserBestScores(hashedUserId, mapId = null) { try { + // Hash-ID zu echter User-ID konvertieren + const user = await this.getUserByHashedId(hashedUserId); + const userId = user.id; + const whereClause = { userId }; if (mapId) { whereClause.mapId = mapId; @@ -146,12 +154,16 @@ class TaxiHighscoreService extends BaseService { /** * Holt alle Highscores eines Users - * @param {number} userId - ID des Users + * @param {string} hashedUserId - Hash-ID des Users * @param {number} limit - Anzahl der Einträge (Standard: 20) * @returns {Promise} Array der Highscore-Einträge des Users */ - async getUserHighscores(userId, limit = 20) { + async getUserHighscores(hashedUserId, limit = 20) { try { + // Hash-ID zu echter User-ID konvertieren + const user = await this.getUserByHashedId(hashedUserId); + const userId = user.id; + const highscores = await this.model.findAll({ where: { userId }, include: [ @@ -174,13 +186,17 @@ class TaxiHighscoreService extends BaseService { /** * Holt die Rangliste-Position eines Users für eine bestimmte Map - * @param {number} userId - ID des Users + * @param {string} hashedUserId - Hash-ID des Users * @param {number} mapId - ID der Map (optional) * @param {string} orderBy - Sortierung ('points', 'passengersDelivered', 'playtime') * @returns {Promise} Rangliste-Position (1-basiert) */ - async getUserRank(userId, mapId = null, orderBy = 'points') { + async getUserRank(hashedUserId, mapId = null, orderBy = 'points') { try { + // Hash-ID zu echter User-ID konvertieren + const user = await this.getUserByHashedId(hashedUserId); + const userId = user.id; + const whereClause = mapId ? { mapId } : {}; const userHighscore = await this.model.findOne({ diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index d2a0788..1d40b02 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -185,18 +185,19 @@ const store = createStore({ let retryCount = 0; const maxRetries = 10; const retryConnection = (reconnectFn) => { - console.log(`Reconnect-Versuch ${retryCount + 1}/${maxRetries}`); + console.log(`Backend-Reconnect-Versuch ${retryCount + 1}/${maxRetries}`); if (retryCount >= maxRetries) { // Nach maxRetries alle 5 Sekunden weiter versuchen - console.log('Max Retries erreicht, versuche weiter alle 5 Sekunden...'); + console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...'); setTimeout(() => { + retryCount = 0; // Reset für nächsten Zyklus reconnectFn(); }, 5000); return; } retryCount++; const delay = 5000; // Alle 5 Sekunden versuchen - console.log(`Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); + console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); setTimeout(() => { reconnectFn(); }, delay); @@ -309,6 +310,7 @@ const store = createStore({ // Nach maxRetries alle 5 Sekunden weiter versuchen console.log('Daemon: Max Retries erreicht, versuche weiter alle 5 Sekunden...'); setTimeout(() => { + retryCount = 0; // Reset für nächsten Zyklus reconnectFn(); }, 5000); return; diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 60d49bf..30ddf35 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -91,17 +91,57 @@ - -
- + +
+ +
+ +
+ + +
+
+

🏆 Highscore

+
Top 20 Spieler
+
+
+
+ Lade Highscore... +
+
+ Noch keine Highscores vorhanden +
+
+
+
{{ entry.rank }}
+
{{ entry.nickname }}
+
{{ entry.points }} Pkt
+
+
...
+
+
{{ currentPlayerEntry.rank }}
+
{{ currentPlayerEntry.nickname }}
+
{{ currentPlayerEntry.points }} Pkt
+
+
+
+
@@ -114,6 +154,9 @@ + @@ -312,6 +355,11 @@ export default { ,prevTaxiY: 250 ,skipRedLightOneFrame: false ,gasStations: [] // Tankstellen im Spiel + ,showHighscore: false // Highscore-Anzeige aktiv + ,highscoreList: [] // Liste der Highscore-Einträge + ,loadingHighscore: false // Lade-Status für Highscore + ,currentPlayerEntry: null // Eintrag des aktuellen Spielers + ,showCurrentPlayerBelow: false // Zeige aktuellen Spieler nach Platz 20 } }, computed: { @@ -586,6 +634,7 @@ export default { }, setupEventListeners() { + // Event-Listener auf Document registrieren (Canvas bleibt immer sichtbar) document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keyup', this.handleKeyUp); }, @@ -698,6 +747,9 @@ export default { clearTimeout(this.motorStopTimeout); this.motorStopTimeout = null; } + // Event-Listener von Document entfernen + document.removeEventListener('keydown', this.handleKeyDown); + document.removeEventListener('keyup', this.handleKeyUp); // Cleanup aller Timeouts if (this.passengerGenerationTimeout) { clearTimeout(this.passengerGenerationTimeout); @@ -708,8 +760,6 @@ export default { this.crashDialogTimeout = null; } // AudioContext bleibt global erhalten, nicht schließen - document.removeEventListener('keydown', this.handleKeyDown); - document.removeEventListener('keyup', this.handleKeyUp); if (this.audioUnlockHandler) { document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true }); document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true }); @@ -2865,8 +2915,9 @@ export default { // AudioContext bei erster Benutzerinteraktion initialisieren this.ensureAudioUnlockedInEvent(); - // Bei Beschleunigungs-Key Motor starten (User-Geste garantiert) - if ((event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') && this.motorSound && !this.motorSound.isPlaying) { + + // Motor nur starten wenn Spiel nicht pausiert ist + if (!this.isPaused && (event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') && this.motorSound && !this.motorSound.isPlaying) { this.motorSound.start(); // Direkt Parameter setzen, um hörbares Feedback ohne Verzögerung zu bekommen const speedKmh = Math.max(5, this.taxi.speed * 5); @@ -2917,6 +2968,14 @@ export default { togglePause() { this.isPaused = !this.isPaused; this.showPauseOverlay = this.isPaused; + + // Motorgeräusch stoppen wenn pausiert, starten wenn fortgesetzt + if (this.isPaused) { + if (this.motorSound && this.motorSound.isPlaying) { + this.motorSound.stop(); + } + } + // Wenn fortgesetzt wird, startet der Motor automatisch bei der nächsten Beschleunigung }, restartLevel() { @@ -3432,6 +3491,106 @@ export default { prev = curr; curr = next || null; } } + }, + + // Highscore-Funktionen + async toggleHighscore() { + this.showHighscore = !this.showHighscore; + + if (this.showHighscore) { + // Spiel pausieren wenn Highscore angezeigt wird + if (!this.isPaused) { + this.isPaused = true; + // Motorgeräusch stoppen wenn pausiert + if (this.motorSound && this.motorSound.isPlaying) { + this.motorSound.stop(); + } + } + // Highscore laden + await this.loadHighscore(); + } else { + // Highscore geschlossen - Spiel automatisch fortsetzen + this.isPaused = false; + this.showPauseOverlay = false; + // Motor startet automatisch bei der nächsten Beschleunigung + } + }, + + async loadHighscore() { + this.loadingHighscore = true; + try { + // Lade Top 20 Highscores für die aktuelle Map + const response = await apiClient.get('/api/taxi/highscores', { + params: { + mapId: this.selectedMapId, + limit: 20, + orderBy: 'points' // Sortiere nach Punkten + } + }); + + if (response.data && response.data.success && Array.isArray(response.data.data)) { + this.highscoreList = response.data.data.map((entry, index) => ({ + rank: index + 1, + nickname: entry.nickname || 'Unbekannt', + points: entry.points, + isCurrentPlayer: entry.userId === this.$store.state.user?.id + })); + + // Prüfe ob aktueller Spieler eine Platzierung hat + await this.checkCurrentPlayerRank(); + } + } catch (error) { + console.error('Fehler beim Laden der Highscores:', error); + this.highscoreList = []; + } finally { + this.loadingHighscore = false; + } + }, + + async checkCurrentPlayerRank() { + if (!this.$store.state.user?.id) return; + + try { + // Lade Rang des aktuellen Spielers + const response = await apiClient.get('/api/taxi/highscores/rank', { + params: { + userId: this.$store.state.user.id, + mapId: this.selectedMapId, + orderBy: 'points' + } + }); + + if (response.data && response.data.success && response.data.data && response.data.data.rank) { + const rank = response.data.data.rank; + + // Wenn Spieler Platz 21 oder schlechter hat + if (rank > 20) { + this.showCurrentPlayerBelow = true; + + // Lade beste Punkte des Spielers + const bestScoreResponse = await apiClient.get('/api/taxi/highscores/user/best', { + params: { + userId: this.$store.state.user.id, + mapId: this.selectedMapId + } + }); + + if (bestScoreResponse.data && bestScoreResponse.data.success && bestScoreResponse.data.data) { + this.currentPlayerEntry = { + rank: rank, + nickname: this.$store.state.user.nickname || 'Du', + points: bestScoreResponse.data.data.points, + isCurrentPlayer: true + }; + } + } else { + this.showCurrentPlayerBelow = false; + this.currentPlayerEntry = null; + } + } + } catch (error) { + console.error('Fehler beim Laden des Spieler-Rangs:', error); + } } } } @@ -3473,6 +3632,7 @@ export default { /* Game Canvas Section */ .game-canvas-section { + position: relative; display: flex; flex-direction: column; align-items: center; @@ -3890,6 +4050,7 @@ export default { margin: 0; /* Kein Margin */ text-align: center; width: 500px; /* Feste Breite wie das Tacho-Display */ + height: 500px; /* Feste Höhe beibehalten */ box-sizing: border-box; /* Border wird in die Breite eingerechnet */ } @@ -3970,6 +4131,120 @@ export default { border: 1px solid #7E471B; } +/* Highscore Overlay */ +.highscore-overlay { + position: absolute; + top: 0; + left: 0; + width: 500px; + height: 500px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(5px); + border: 2px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + display: flex; + flex-direction: column; + overflow: hidden; + z-index: 10; +} + +.highscore-header { + background: #F9A22C; + color: #000; + padding: 20px; + text-align: center; + border-bottom: 2px solid #ddd; +} + +.highscore-header h2 { + margin: 0 0 5px 0; + font-size: 1.5rem; + font-weight: 600; +} + +.highscore-subtitle { + margin: 0; + font-size: 0.9rem; + opacity: 0.8; +} + +.highscore-list { + flex: 1; + overflow-y: auto; + padding: 15px; +} + +.loading-message, +.no-highscore { + text-align: center; + padding: 40px 20px; + color: #666; + font-style: italic; +} + +.highscore-table { + display: flex; + flex-direction: column; + gap: 8px; +} + +.highscore-entry { + display: grid; + grid-template-columns: 50px 1fr auto; + gap: 10px; + align-items: center; + padding: 10px 15px; + background: #f8f9fa; + border-radius: 4px; + transition: all 0.2s; +} + +.highscore-entry:hover { + background: #e9ecef; +} + +.highscore-entry.current-player { + background: #fff3cd; + border: 2px solid #ffc107; + font-weight: 600; +} + +.highscore-entry.current-player:hover { + background: #ffe8a1; +} + +.highscore-rank { + font-size: 1.2rem; + font-weight: 700; + color: #F9A22C; + text-align: center; +} + +.highscore-name { + font-size: 1rem; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.highscore-points { + font-size: 1rem; + font-weight: 600; + color: #666; + text-align: right; +} + +.highscore-separator { + text-align: center; + font-size: 1.5rem; + font-weight: 700; + color: #999; + padding: 10px 0; + letter-spacing: 5px; +} + /* Responsive Design */ @media (max-width: 1024px) {