Ä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

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