Änderung: Hinzufügen von TaxiHighscore-Logik und Verbesserung der API-Integration

Ä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.
This commit is contained in:
Torsten Schulz (local)
2025-10-05 00:04:28 +02:00
parent 75d7ac6222
commit 42349e46c8
12 changed files with 775 additions and 79 deletions

View File

@@ -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);

View File

@@ -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();

View File

@@ -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' });
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Object>} 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>} 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<Object>} 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>} 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<number>} 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<number>} 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<Object>} 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();

View File

@@ -3,9 +3,13 @@
<div class="logo"><img src="/images/logos/logo.png" /></div>
<div class="advertisement">Advertisement</div>
<div class="connection-status" v-if="isLoggedIn">
<div class="status-indicator" :class="connectionStatusClass">
<div class="status-indicator" :class="backendStatusClass">
<span class="status-dot"></span>
<span class="status-text">{{ connectionStatusText }}</span>
<span class="status-text">B</span>
</div>
<div class="status-indicator" :class="daemonStatusClass">
<span class="status-dot"></span>
<span class="status-text">D</span>
</div>
</div>
</header>
@@ -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;
}

View File

@@ -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,

View File

@@ -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() {

View File

@@ -114,9 +114,6 @@
<button @click="restartLevel" class="control-button">
{{ $t('minigames.taxi.restartLevel') }}
</button>
<button @click="goBack" class="control-button">
{{ $t('minigames.backToGames') }}
</button>
</div>
</div>
@@ -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;
// 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
// 4. Bestimme Straßenname basierend auf der Haus-Ecke
let streetName = null;
const corner = selectedHouse.corner;
if (corner === 'lo' || corner === 'ro') {
// Horizontale Straße
if (houseTile.streetNameH && houseTile.streetNameH.name) {
streetName = houseTile.streetNameH.name;
if (selectedStreet.streetNameH && selectedStreet.streetNameH.name) {
streetName = selectedStreet.streetNameH.name;
}
} else if (corner === 'lu' || corner === 'ru') {
// Vertikale Straße
if (houseTile.streetNameV && houseTile.streetNameV.name) {
streetName = houseTile.streetNameV.name;
if (selectedStreet.streetNameV && selectedStreet.streetNameV.name) {
streetName = selectedStreet.streetNameV.name;
}
}
}
if (streetName === 'Unbekannte Straße') {
console.log(houseTile)
}
// Finde die Hausnummer für dieses spezifische Haus
// 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;
}
// 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() {