From 42349e46c867add5fceef6d70df77e66986a5881 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 5 Oct 2025 00:04:28 +0200 Subject: [PATCH] =?UTF-8?q?=C3=84nderung:=20Hinzuf=C3=BCgen=20von=20TaxiHi?= =?UTF-8?q?ghscore-Logik=20und=20Verbesserung=20der=20API-Integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Änderungen: - Implementierung des neuen Routers für TaxiHighscore zur Verwaltung von Highscore-Daten. - Anpassung der Datenbankmodelle zur Unterstützung von TaxiHighscore-Associations. - Erweiterung der Vue-Komponenten zur Anzeige und Speicherung von Highscores im Taxi-Spiel. - Verbesserung der Statusanzeige im AppHeader zur besseren Benutzerinteraktion. Diese Anpassungen erweitern die Spielmechanik und Benutzererfahrung, indem sie die Verwaltung von Highscores integrieren und die Benutzeroberfläche optimieren. --- backend/app.js | 2 + .../controllers/taxiHighscoreController.js | 198 +++++++++++++ backend/models/associations.js | 9 + backend/models/index.js | 2 + backend/models/minigames/taxiHighscore.js | 100 +++++++ backend/routers/taxiHighscoreRouter.js | 12 + backend/server.js | 2 +- backend/services/taxiHighscoreService.js | 271 ++++++++++++++++++ frontend/src/components/AppHeader.vue | 38 +-- frontend/src/store/index.js | 30 +- .../src/views/falukant/UndergroundView.vue | 4 +- frontend/src/views/minigames/TaxiGame.vue | 186 ++++++++---- 12 files changed, 775 insertions(+), 79 deletions(-) create mode 100644 backend/controllers/taxiHighscoreController.js create mode 100644 backend/models/minigames/taxiHighscore.js create mode 100644 backend/routers/taxiHighscoreRouter.js create mode 100644 backend/services/taxiHighscoreService.js diff --git a/backend/app.js b/backend/app.js index f10d657..766d8ab 100644 --- a/backend/app.js +++ b/backend/app.js @@ -15,6 +15,7 @@ import blogRouter from './routers/blogRouter.js'; import match3Router from './routers/match3Router.js'; import taxiRouter from './routers/taxiRouter.js'; import taxiMapRouter from './routers/taxiMapRouter.js'; +import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js'; import cors from 'cors'; import './jobs/sessionCleanup.js'; @@ -43,6 +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('/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 new file mode 100644 index 0000000..a285ee1 --- /dev/null +++ b/backend/controllers/taxiHighscoreController.js @@ -0,0 +1,198 @@ +import taxiHighscoreService from '../services/taxiHighscoreService.js'; + +class TaxiHighscoreController { + /** + * Erstellt oder aktualisiert einen Highscore-Eintrag + */ + async createHighscore(req, res) { + try { + const { userId, nickname, passengersDelivered, playtime, points, mapId, mapName } = req.body; + + // Validierung der erforderlichen Felder + if (!userId || !nickname || passengersDelivered === undefined || playtime === undefined || points === undefined || !mapId || !mapName) { + return res.status(400).json({ + success: false, + message: 'Alle Felder sind erforderlich: userId, nickname, passengersDelivered, playtime, points, mapId, mapName' + }); + } + + const highscoreData = { + userId: parseInt(userId), + nickname, + passengersDelivered: parseInt(passengersDelivered), + playtime: parseInt(playtime), + points: parseInt(points), + mapId: parseInt(mapId), + mapName + }; + + const highscore = await taxiHighscoreService.createHighscore(highscoreData); + + res.status(200).json({ + success: true, + data: highscore, + message: 'Highscore erfolgreich gespeichert' + }); + } catch (error) { + console.error('Fehler beim Erstellen des Highscores:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Speichern des Highscores', + error: error.message + }); + } + } + + /** + * Holt die Top-Highscores + */ + async getTopHighscores(req, res) { + try { + const { mapId, limit = 10, orderBy = 'points' } = req.query; + + const highscores = await taxiHighscoreService.getTopHighscores( + mapId ? parseInt(mapId) : null, + parseInt(limit), + orderBy + ); + + res.status(200).json({ + success: true, + data: highscores + }); + } catch (error) { + console.error('Fehler beim Laden der Top-Highscores:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Highscores', + error: error.message + }); + } + } + + /** + * Holt die persönlichen Bestleistungen eines Users + */ + async getUserBestScores(req, res) { + try { + const { userId } = req.params; + const { mapId } = req.query; + + if (!userId) { + return res.status(400).json({ + success: false, + message: 'userId ist erforderlich' + }); + } + + const bestScores = await taxiHighscoreService.getUserBestScores( + parseInt(userId), + mapId ? parseInt(mapId) : null + ); + + res.status(200).json({ + success: true, + data: bestScores + }); + } catch (error) { + console.error('Fehler beim Laden der User-Bestleistungen:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Bestleistungen', + error: error.message + }); + } + } + + /** + * Holt alle Highscores eines Users + */ + async getUserHighscores(req, res) { + try { + const { userId } = req.params; + const { limit = 20 } = req.query; + + if (!userId) { + return res.status(400).json({ + success: false, + message: 'userId ist erforderlich' + }); + } + + const highscores = await taxiHighscoreService.getUserHighscores( + parseInt(userId), + parseInt(limit) + ); + + res.status(200).json({ + success: true, + data: highscores + }); + } catch (error) { + console.error('Fehler beim Laden der User-Highscores:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der User-Highscores', + error: error.message + }); + } + } + + /** + * Holt die Rangliste-Position eines Users + */ + async getUserRank(req, res) { + try { + const { userId } = req.params; + const { mapId, orderBy = 'points' } = req.query; + + if (!userId) { + return res.status(400).json({ + success: false, + message: 'userId ist erforderlich' + }); + } + + const rank = await taxiHighscoreService.getUserRank( + parseInt(userId), + mapId ? parseInt(mapId) : null, + orderBy + ); + + res.status(200).json({ + success: true, + data: { rank } + }); + } catch (error) { + console.error('Fehler beim Berechnen der User-Rangliste:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Berechnen der Rangliste', + error: error.message + }); + } + } + + /** + * Holt Statistiken über die Highscores + */ + async getHighscoreStats(req, res) { + try { + const stats = await taxiHighscoreService.getHighscoreStats(); + + res.status(200).json({ + success: true, + data: stats + }); + } catch (error) { + console.error('Fehler beim Laden der Highscore-Statistiken:', error); + res.status(500).json({ + success: false, + message: 'Fehler beim Laden der Statistiken', + error: error.message + }); + } + } +} + +export default new TaxiHighscoreController(); diff --git a/backend/models/associations.js b/backend/models/associations.js index 9b7859d..2c3692f 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -110,6 +110,7 @@ import TaxiMapTile from './taxi/taxiMapTile.js'; import TaxiMapTileHouse from './taxi/taxiMapTileHouse.js'; import TaxiStreetName from './taxi/taxiStreetName.js'; import TaxiMapTileStreet from './taxi/taxiMapTileStreet.js'; +import TaxiHighscore from './minigames/taxiHighscore.js'; export default function setupAssociations() { // RoomType 1:n Room @@ -820,4 +821,12 @@ export default function setupAssociations() { // Houses per tile (one row per corner) TaxiMap.hasMany(TaxiMapTileHouse, { foreignKey: 'map_id', as: 'tileHouses' }); TaxiMapTileHouse.belongsTo(TaxiMap, { foreignKey: 'map_id', as: 'map' }); + + // Taxi Highscore associations + TaxiHighscore.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + User.hasMany(TaxiHighscore, { foreignKey: 'userId', as: 'taxiHighscores' }); + + TaxiHighscore.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' }); + TaxiMap.hasMany(TaxiHighscore, { foreignKey: 'mapId', as: 'highscores' }); } + diff --git a/backend/models/index.js b/backend/models/index.js index bf3710b..7616c56 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -97,6 +97,7 @@ import Match3UserLevelProgress from './match3/userLevelProgress.js'; // — Taxi Minigame — import { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap, TaxiMapTile, TaxiStreetName, TaxiMapTileStreet, TaxiMapTileHouse } from './taxi/index.js'; +import TaxiHighscore from './minigames/taxiHighscore.js'; // — Politische Ämter (Politics) — import PoliticalOfficeType from './falukant/type/political_office_type.js'; @@ -243,6 +244,7 @@ const models = { TaxiStreetName, TaxiMapTileStreet, TaxiMapTileHouse, + TaxiHighscore, }; export default models; diff --git a/backend/models/minigames/taxiHighscore.js b/backend/models/minigames/taxiHighscore.js new file mode 100644 index 0000000..1f01a28 --- /dev/null +++ b/backend/models/minigames/taxiHighscore.js @@ -0,0 +1,100 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiHighscore = sequelize.define('TaxiHighscore', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.INTEGER, + allowNull: true, // Kann null sein, falls User gelöscht wird + references: { + model: { + tableName: 'user', + schema: 'community' + }, + key: 'id' + } + }, + nickname: { + type: DataTypes.STRING(100), + allowNull: true, // Kann null sein, falls User gelöscht wird + comment: 'Nickname zum Zeitpunkt des Spiels - bleibt erhalten wenn User gelöscht wird' + }, + passengersDelivered: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: 'Anzahl der abgelieferten Passagiere' + }, + playtime: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: 'Spielzeit in Sekunden' + }, + points: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: 'Erreichte Punkte' + }, + mapId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: { + tableName: 'taxi_map', + schema: 'taxi' + }, + key: 'id' + }, + comment: 'ID der gespielten Map' + }, + mapName: { + type: DataTypes.STRING(100), + allowNull: true, + comment: 'Name der Map zum Zeitpunkt des Spiels' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } + }, { + tableName: 'taxi_highscores', + schema: 'taxi', + timestamps: true, + underscored: true, + indexes: [ + { + fields: ['user_id'] + }, + { + fields: ['points'] + }, + { + fields: ['passengers_delivered'] + }, + { + fields: ['map_id'] + }, + { + fields: ['created_at'] + }, + { + unique: true, + fields: ['user_id', 'map_id'], + name: 'unique_user_map_highscore' + } + ] + }); + +export default TaxiHighscore; diff --git a/backend/routers/taxiHighscoreRouter.js b/backend/routers/taxiHighscoreRouter.js new file mode 100644 index 0000000..e1375bc --- /dev/null +++ b/backend/routers/taxiHighscoreRouter.js @@ -0,0 +1,12 @@ +import express from 'express'; +import taxiHighscoreController from '../controllers/taxiHighscoreController.js'; + +const router = express.Router(); +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); +router.get('/stats', taxiHighscoreController.getHighscoreStats); + +export default router; diff --git a/backend/server.js b/backend/server.js index 031c3ec..9b586fb 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,7 +11,7 @@ setupWebSocket(server); syncDatabase().then(() => { const port = process.env.PORT || 3001; - server.listen(port, () => { + server.listen(port, () => { console.log('Server is running on port', port); }); }).catch(err => { diff --git a/backend/services/taxiHighscoreService.js b/backend/services/taxiHighscoreService.js new file mode 100644 index 0000000..6739b18 --- /dev/null +++ b/backend/services/taxiHighscoreService.js @@ -0,0 +1,271 @@ +import models from '../models/index.js'; +import BaseService from './BaseService.js'; + +const { TaxiHighscore, User, TaxiMap } = models; + +class TaxiHighscoreService extends BaseService { + constructor() { + super(); + this.model = TaxiHighscore; + } + + /** + * 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.nickname - Nickname des Users + * @param {number} highscoreData.passengersDelivered - Anzahl abgelieferter Passagiere + * @param {number} highscoreData.playtime - Spielzeit in Sekunden + * @param {number} highscoreData.points - Erreichte Punkte + * @param {number} highscoreData.mapId - ID der Map + * @param {string} highscoreData.mapName - Name der Map + * @returns {Promise} Gespeicherter oder aktualisierter Highscore-Eintrag + */ + async createHighscore(highscoreData) { + try { + // Prüfen ob bereits ein Eintrag für diesen User und diese Map existiert + const existingHighscore = await this.model.findOne({ + where: { + userId: highscoreData.userId, + mapId: highscoreData.mapId + } + }); + + if (existingHighscore) { + // Nur aktualisieren wenn der neue Score besser ist (mehr Punkte) + if (highscoreData.points > existingHighscore.points) { + await existingHighscore.update({ + nickname: highscoreData.nickname, + passengersDelivered: highscoreData.passengersDelivered, + playtime: highscoreData.playtime, + points: highscoreData.points, + mapName: highscoreData.mapName + }); + return existingHighscore; + } else { + // Neuer Score ist nicht besser, existierenden zurückgeben + return existingHighscore; + } + } else { + // Kein existierender Eintrag, neuen erstellen + const highscore = await this.model.create({ + userId: highscoreData.userId, + nickname: highscoreData.nickname, + passengersDelivered: highscoreData.passengersDelivered, + playtime: highscoreData.playtime, + points: highscoreData.points, + mapId: highscoreData.mapId, + mapName: highscoreData.mapName + }); + + return highscore; + } + } catch (error) { + console.error('Fehler beim Erstellen/Aktualisieren des Highscores:', error); + throw error; + } + } + + /** + * Holt die Top-Highscores für eine bestimmte Map + * @param {number} mapId - ID der Map (optional) + * @param {number} limit - Anzahl der Einträge (Standard: 10) + * @param {string} orderBy - Sortierung ('points', 'passengersDelivered', 'playtime') + * @returns {Promise} Array der Highscore-Einträge + */ + async getTopHighscores(mapId = null, limit = 10, orderBy = 'points') { + try { + const whereClause = mapId ? { mapId } : {}; + + const highscores = await this.model.findAll({ + where: whereClause, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username'], + required: false // LEFT JOIN, da User gelöscht sein könnte + }, + { + model: TaxiMap, + as: 'map', + attributes: ['id', 'name'], + required: false + } + ], + order: [[orderBy, 'DESC']], + limit: parseInt(limit) + }); + + return highscores; + } catch (error) { + console.error('Fehler beim Laden der Highscores:', error); + throw error; + } + } + + /** + * Holt die persönlichen Bestleistungen eines Users + * @param {number} userId - ID des Users + * @param {number} mapId - ID der Map (optional) + * @returns {Promise} Bestleistungen des Users + */ + async getUserBestScores(userId, mapId = null) { + try { + const whereClause = { userId }; + if (mapId) { + whereClause.mapId = mapId; + } + + const [bestPoints, bestPassengers, bestPlaytime] = await Promise.all([ + this.model.findOne({ + where: whereClause, + order: [['points', 'DESC']] + }), + this.model.findOne({ + where: whereClause, + order: [['passengersDelivered', 'DESC']] + }), + this.model.findOne({ + where: whereClause, + order: [['playtime', 'DESC']] + }) + ]); + + return { + bestPoints, + bestPassengers, + bestPlaytime + }; + } catch (error) { + console.error('Fehler beim Laden der User-Bestleistungen:', error); + throw error; + } + } + + /** + * Holt alle Highscores eines Users + * @param {number} userId - 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) { + try { + const highscores = await this.model.findAll({ + where: { userId }, + include: [ + { + model: TaxiMap, + as: 'map', + attributes: ['id', 'name'] + } + ], + order: [['createdAt', 'DESC']], + limit: parseInt(limit) + }); + + return highscores; + } catch (error) { + console.error('Fehler beim Laden der User-Highscores:', error); + throw error; + } + } + + /** + * Holt die Rangliste-Position eines Users für eine bestimmte Map + * @param {number} userId - 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') { + try { + const whereClause = mapId ? { mapId } : {}; + + const userHighscore = await this.model.findOne({ + where: { userId, ...whereClause }, + order: [[orderBy, 'DESC']] + }); + + if (!userHighscore) { + return null; // User hat noch keinen Highscore + } + + const rank = await this.model.count({ + where: { + ...whereClause, + [orderBy]: { + [this.model.sequelize.Sequelize.Op.gt]: userHighscore[orderBy] + } + } + }); + + return rank + 1; // 1-basierte Position + } catch (error) { + console.error('Fehler beim Berechnen der User-Rangliste:', error); + throw error; + } + } + + /** + * Löscht alle Highscores eines Users (z.B. bei Account-Löschung) + * @param {number} userId - ID des Users + * @returns {Promise} Anzahl der gelöschten Einträge + */ + async deleteUserHighscores(userId) { + try { + const deletedCount = await this.model.destroy({ + where: { userId } + }); + + return deletedCount; + } catch (error) { + console.error('Fehler beim Löschen der User-Highscores:', error); + throw error; + } + } + + /** + * Holt Statistiken über die Highscores + * @returns {Promise} Statistiken + */ + async getHighscoreStats() { + try { + const [ + totalHighscores, + totalPlayers, + averagePoints, + totalPassengersDelivered, + totalPlaytime + ] = await Promise.all([ + this.model.count(), + this.model.count({ + distinct: true, + col: 'userId' + }), + this.model.findOne({ + attributes: [ + [this.model.sequelize.fn('AVG', this.model.sequelize.col('points')), 'avg'] + ], + raw: true + }), + this.model.sum('passengersDelivered'), + this.model.sum('playtime') + ]); + + return { + totalHighscores, + totalPlayers, + averagePoints: averagePoints ? parseFloat(averagePoints.avg).toFixed(2) : 0, + totalPassengersDelivered: totalPassengersDelivered || 0, + totalPlaytime: totalPlaytime || 0 + }; + } catch (error) { + console.error('Fehler beim Laden der Highscore-Statistiken:', error); + throw error; + } + } +} + +export default new TaxiHighscoreService(); diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 0271bc2..45457b0 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -3,9 +3,13 @@
-
+
- {{ connectionStatusText }} + B +
+
+ + D
@@ -17,8 +21,8 @@ import { mapGetters } from 'vuex'; export default { name: 'AppHeader', computed: { - ...mapGetters(['isLoggedIn', 'connectionStatus']), - connectionStatusClass() { + ...mapGetters(['isLoggedIn', 'connectionStatus', 'daemonConnectionStatus']), + backendStatusClass() { return { 'status-connected': this.connectionStatus === 'connected', 'status-connecting': this.connectionStatus === 'connecting', @@ -26,14 +30,13 @@ export default { 'status-error': this.connectionStatus === 'error' }; }, - connectionStatusText() { - switch (this.connectionStatus) { - case 'connected': return 'Verbunden'; - case 'connecting': return 'Verbinde...'; - case 'disconnected': return 'Getrennt'; - case 'error': return 'Fehler'; - default: return 'Unbekannt'; - } + daemonStatusClass() { + return { + 'status-connected': this.daemonConnectionStatus === 'connected', + 'status-connecting': this.daemonConnectionStatus === 'connecting', + 'status-disconnected': this.daemonConnectionStatus === 'disconnected', + 'status-error': this.daemonConnectionStatus === 'error' + }; } } }; @@ -60,22 +63,23 @@ header { display: flex; align-items: center; margin-left: 10px; + gap: 5px; } .status-indicator { display: flex; align-items: center; - padding: 4px 8px; + padding: 2px 6px; border-radius: 4px; - font-size: 12px; + font-size: 6pt; font-weight: 500; } .status-dot { - width: 8px; - height: 8px; + width: 6px; + height: 6px; border-radius: 50%; - margin-right: 6px; + margin-right: 4px; animation: pulse 2s infinite; } diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 9879543..d2a0788 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -9,6 +9,7 @@ const store = createStore({ state: { isLoggedIn: localStorage.getItem('isLoggedIn') === 'true', connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' + daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' user: JSON.parse(localStorage.getItem('user')) || null, language: (() => { // Verwende die gleiche Logik wie in main.js @@ -103,6 +104,9 @@ const store = createStore({ setConnectionStatus(state, status) { state.connectionStatus = status; }, + setDaemonConnectionStatus(state, status) { + state.daemonConnectionStatus = status; + }, clearSocket(state) { if (state.socket) { state.socket.disconnect(); @@ -117,6 +121,7 @@ const store = createStore({ state.daemonSocket.close(); } state.daemonSocket = null; + state.daemonConnectionStatus = 'disconnected'; }, }, actions: { @@ -180,11 +185,18 @@ const store = createStore({ let retryCount = 0; const maxRetries = 10; const retryConnection = (reconnectFn) => { + console.log(`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...'); + setTimeout(() => { + reconnectFn(); + }, 5000); return; } retryCount++; - const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s + const delay = 5000; // Alle 5 Sekunden versuchen + console.log(`Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); setTimeout(() => { reconnectFn(); }, delay); @@ -220,6 +232,7 @@ const store = createStore({ const tryConnectWithProtocol = () => { const currentProtocol = protocols[attemptIndex]; try { + commit('setDaemonConnectionStatus', 'connecting'); const daemonSocket = currentProtocol ? new WebSocket(daemonUrl, currentProtocol) : new WebSocket(daemonUrl); @@ -229,8 +242,8 @@ const store = createStore({ daemonSocket.onopen = () => { opened = true; retryCount = 0; // Reset retry counter on successful connection + commit('setDaemonConnectionStatus', 'connected'); const payload = JSON.stringify({ - user_id: state.user.id, event: 'setUserId', data: { userId: state.user.id } }); @@ -238,6 +251,7 @@ const store = createStore({ }; daemonSocket.onclose = (event) => { + commit('setDaemonConnectionStatus', 'disconnected'); // Falls Verbindungsaufbau nicht offen war und es noch einen Fallback gibt → nächsten Versuch ohne Subprotokoll if (!opened && attemptIndex < protocols.length - 1) { attemptIndex += 1; @@ -248,6 +262,7 @@ const store = createStore({ }; daemonSocket.onerror = (error) => { + commit('setDaemonConnectionStatus', 'error'); // Bei Fehler vor Open: Fallback versuchen if (!opened && attemptIndex < protocols.length - 1) { attemptIndex += 1; @@ -289,16 +304,18 @@ const store = createStore({ let retryCount = 0; const maxRetries = 15; // Increased max retries const retryConnection = (reconnectFn) => { + console.log(`Daemon-Reconnect-Versuch ${retryCount + 1}/${maxRetries}`); if (retryCount >= maxRetries) { - // Reset counter after a longer delay to allow for network recovery + // Nach maxRetries alle 5 Sekunden weiter versuchen + console.log('Daemon: Max Retries erreicht, versuche weiter alle 5 Sekunden...'); setTimeout(() => { - retryCount = 0; reconnectFn(); - }, 60000); // Wait 1 minute before resetting + }, 5000); return; } retryCount++; - const delay = Math.min(1000 * Math.pow(1.5, retryCount - 1), 30000); // Exponential backoff, max 30s + const delay = 5000; // Alle 5 Sekunden versuchen + console.log(`Daemon: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); setTimeout(() => { reconnectFn(); }, delay); @@ -326,6 +343,7 @@ const store = createStore({ daemonSocket: state => state.daemonSocket, menuNeedsUpdate: state => state.menuNeedsUpdate, connectionStatus: state => state.connectionStatus, + daemonConnectionStatus: state => state.daemonConnectionStatus, }, modules: { dialogs, diff --git a/frontend/src/views/falukant/UndergroundView.vue b/frontend/src/views/falukant/UndergroundView.vue index 188558b..be3e3b8 100644 --- a/frontend/src/views/falukant/UndergroundView.vue +++ b/frontend/src/views/falukant/UndergroundView.vue @@ -287,7 +287,8 @@ export default { }, async loadActivities() { - return; + return; // TODO: Aktivierung der Methode geplant + /* Temporär deaktiviert: this.loading.activities = true; try { const { data } = await apiClient.get( @@ -297,6 +298,7 @@ export default { } finally { this.loading.activities = false; } + */ }, async loadAttacks() { diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 13920dc..60d49bf 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -114,9 +114,6 @@ -
@@ -297,6 +294,7 @@ export default { radarLinePos: 0, vehicleCount: 5, redLightSincePenalty: 0, + gameStartTime: null, // Zeitstempel wann das Spiel gestartet wurde maps: [], // Geladene Maps aus der Datenbank currentMap: null, // Aktuell verwendete Map selectedMapId: null, // ID der ausgewählten Map @@ -313,6 +311,7 @@ export default { ,prevTaxiX: 250 ,prevTaxiY: 250 ,skipRedLightOneFrame: false + ,gasStations: [] // Tankstellen im Spiel } }, computed: { @@ -761,7 +760,7 @@ export default { }, generateWaitingPassenger() { - if (!this.currentMap || !Array.isArray(this.currentMap.tileHouses)) { + if (!this.currentMap || !Array.isArray(this.currentMap.tileHouses) || !Array.isArray(this.currentMap.tileStreets)) { // Versuche es in 2 Sekunden erneut this.passengerGenerationTimeout = setTimeout(() => { this.generateWaitingPassenger(); @@ -769,24 +768,41 @@ export default { return; } - // Erstelle Liste aller Tiles mit Häusern - const tilesWithHouses = this.getTilesWithHouses(); + // 1. Sammle alle Straßen mit verfügbaren Häusern + const streetsWithHouses = []; - if (tilesWithHouses.length === 0) { - console.log('Keine Tiles mit Häusern gefunden'); + for (const street of this.currentMap.tileStreets) { + // Finde alle Häuser auf diesem Straßen-Tile + const housesOnThisTile = this.currentMap.tileHouses.filter(house => + house.x === street.x && house.y === street.y + ); + + if (housesOnThisTile.length > 0) { + // Prüfe ob diese Straße gültige Straßennamen hat + const hasValidStreetName = (street.streetNameH && street.streetNameH.name && street.streetNameH.name.trim() !== '') || + (street.streetNameV && street.streetNameV.name && street.streetNameV.name.trim() !== ''); + + if (hasValidStreetName) { + streetsWithHouses.push({ + street: street, + houses: housesOnThisTile + }); + } + } + } + + if (streetsWithHouses.length === 0) { + console.log('Keine Straßen mit verfügbaren Häusern gefunden'); return; } - // Wähle zufälliges Tile mit Häusern - const selectedTile = tilesWithHouses[Math.floor(Math.random() * tilesWithHouses.length)]; + // 2. Wähle zufällige Straße mit Häusern + const selectedStreetData = streetsWithHouses[Math.floor(Math.random() * streetsWithHouses.length)]; + const selectedStreet = selectedStreetData.street; + const availableHouses = selectedStreetData.houses; - // Finde alle Häuser auf diesem Tile - const housesOnTile = this.currentMap.tileHouses.filter(house => - house.x === selectedTile.x && house.y === selectedTile.y - ); - - // Wähle zufälliges Haus auf diesem Tile - const selectedHouse = housesOnTile[Math.floor(Math.random() * housesOnTile.length)]; + // 3. Wähle zufälliges Haus auf dieser Straße + const selectedHouse = availableHouses[Math.floor(Math.random() * availableHouses.length)]; const houseIndex = this.currentMap.tileHouses.findIndex(h => h === selectedHouse); const houseId = `${selectedHouse.x}-${selectedHouse.y}-${houseIndex}`; @@ -797,39 +813,37 @@ export default { return; } - // Finde die Straße für dieses spezifische Haus - let streetName = "Unbekannte Straße"; - let houseNumber = 1; + // 4. Bestimme Straßenname basierend auf der Haus-Ecke + let streetName = null; + const corner = selectedHouse.corner; - // Suche nach Straßennamen für das gewählte Haus-Tile - const houseTile = this.currentMap.tileStreets?.find(ts => ts.x === selectedHouse.x && ts.y === selectedHouse.y); - if (houseTile) { - // Bestimme die Straße basierend auf der Haus-Ecke - const corner = selectedHouse.corner; - if (corner === 'lo' || corner === 'ro') { - // Horizontale Straße - if (houseTile.streetNameH && houseTile.streetNameH.name) { - streetName = houseTile.streetNameH.name; - } - } else if (corner === 'lu' || corner === 'ru') { - // Vertikale Straße - if (houseTile.streetNameV && houseTile.streetNameV.name) { - streetName = houseTile.streetNameV.name; - } + if (corner === 'lo' || corner === 'ro') { + // Horizontale Straße + if (selectedStreet.streetNameH && selectedStreet.streetNameH.name) { + streetName = selectedStreet.streetNameH.name; + } + } else if (corner === 'lu' || corner === 'ru') { + // Vertikale Straße + if (selectedStreet.streetNameV && selectedStreet.streetNameV.name) { + streetName = selectedStreet.streetNameV.name; } } - if (streetName === 'Unbekannte Straße') { - console.log(houseTile) + + // Fallback sollte nie auftreten, da wir nur Straßen mit gültigen Namen auswählen + if (!streetName || streetName.trim() === '') { + console.error('Fehler: Kein gültiger Straßenname gefunden für Haus:', selectedHouse); + this.generateWaitingPassenger(); // Versuche es erneut + return; } - // Finde die Hausnummer für dieses spezifische Haus + // 5. Finde die Hausnummer für dieses spezifische Haus const houseKey = `${selectedHouse.x},${selectedHouse.y},${selectedHouse.corner}`; - houseNumber = this.houseNumbers[houseKey] || 1; + const houseNumber = this.houseNumbers[houseKey] || 1; - // Generiere Namen und Geschlecht + // 6. Generiere Namen und Geschlecht const nameData = this.generatePassengerName(); - // Erstelle Passagier + // 7. Erstelle Passagier const passenger = { id: Date.now() + Math.random(), // Eindeutige ID name: nameData.fullName, @@ -844,7 +858,7 @@ export default { createdAt: Date.now() // Zeitstempel der Erstellung }; - // Füge Passagier zur Liste hinzu und markiere Haus als belegt + // 8. Füge Passagier zur Liste hinzu und markiere Haus als belegt this.waitingPassengersList.push(passenger); this.occupiedHouses.add(houseId); }, @@ -1277,6 +1291,10 @@ export default { startGame() { this.gameRunning = true; + // Setze Spielstart-Zeit + if (!this.gameStartTime) { + this.gameStartTime = Date.now(); + } // Stoppe bestehende Game-Loop falls vorhanden if (this.gameLoop) { cancelAnimationFrame(this.gameLoop); @@ -1669,6 +1687,12 @@ export default { }, refuel() { + // Prüfe ob Tankstellen verfügbar sind + if (!this.gasStations || this.gasStations.length === 0) { + console.log('Keine Tankstellen verfügbar'); + return; + } + // Finde nächste Tankstelle in der Nähe for (let i = 0; i < this.gasStations.length; i++) { const station = this.gasStations[i]; @@ -1967,12 +1991,7 @@ export default { console.log('Crash-Dialog wird angezeigt:', { crashes: this.crashes, isPaused: this.isPaused, - taxiSpeed: this.taxi.speed, - messageTest: this.$t('message.test'), - crashMessage: this.$t('minigames.taxi.crash.message'), - allKeys: Object.keys(this.$t('minigames')), - taxiKeys: Object.keys(this.$t('minigames.taxi')), - crashKeys: Object.keys(this.$t('minigames.taxi.crash')) + taxiSpeed: this.taxi.speed }); // Spiel bleibt pausiert bis Dialog geschlossen wird @@ -2007,15 +2026,74 @@ export default { this.vehicleCount = Math.max(0, this.vehicleCount - 1); }, - handleOutOfVehicles() { - const title = 'Hinweis'; - const msg = 'Keine Fahrzeuge mehr. Neustart.'; + getPlayTime() { + if (!this.gameStartTime) return 0; + return Math.floor((Date.now() - this.gameStartTime) / 1000); + }, + + async saveHighscore() { + try { + const playTime = this.getPlayTime(); + const highscoreData = { + passengersDelivered: this.passengersDelivered, + playtime: playTime, + points: this.score, + mapId: this.currentMap ? this.currentMap.id : null + }; + + console.log('Highscore-Daten:', highscoreData); + console.log('Current Map:', this.currentMap); + console.log('Passengers Delivered:', this.passengersDelivered); + console.log('Playtime:', playTime); + console.log('Points:', this.score); + + const response = await apiClient.post('/api/taxi/highscore', highscoreData); + + if (response.data.success) { + console.log('Highscore erfolgreich gespeichert:', response.data.data); + return response.data.data; + } else { + console.error('Fehler beim Speichern des Highscores:', response.data.message); + return null; + } + } catch (error) { + console.error('Fehler beim Speichern des Highscores:', error); + if (error.response) { + console.error('Backend-Fehler:', error.response.data); + console.error('Status:', error.response.status); + } + return null; + } + }, + + async handleOutOfVehicles() { + // Spiel stoppen + this.isPaused = true; + this.gameLoop = null; + + // Highscore speichern + const highscore = await this.saveHighscore(); + + const playTime = this.getPlayTime(); + const playTimeMinutes = Math.floor(playTime / 60); + const playTimeSeconds = playTime % 60; + + const title = 'Spiel beendet!'; + const msg = `Keine Fahrzeuge mehr. Spiel beendet!\n\n` + + `Deine Leistung:\n` + + `• Passagiere: ${this.passengersDelivered}\n` + + `• Punkte: ${this.score}\n` + + `• Spielzeit: ${playTimeMinutes}:${playTimeSeconds.toString().padStart(2, '0')}\n` + + `• Map: ${this.currentMap ? this.currentMap.name : 'Unbekannt'}\n\n` + + `Highscore wurde gespeichert!`; + this.$root?.$refs?.messageDialog?.open?.(msg, title, {}, () => { this.restartLevel(); this.vehicleCount = 5; this.crashes = 0; this.redLightViolations = 0; this.redLightSincePenalty = 0; + this.gameStartTime = null; }); }, @@ -2854,6 +2932,9 @@ export default { this.taxi.angle = 0; this.taxi.speed = 0; + // Reset Spielzeit + this.gameStartTime = null; + // Cleanup bestehender Timeouts if (this.passengerGenerationTimeout) { clearTimeout(this.passengerGenerationTimeout); @@ -2868,9 +2949,6 @@ export default { this.initializePassengerGeneration(); }, - goBack() { - this.$router.push('/minigames'); - }, initializeMinimap() {