Änderung: Verbesserung der Socket-Verbindung und Implementierung von Autos im Taxi-Spiel

Änderungen:
- Hinzufügen von Logik zur Verwaltung von Backend- und Daemon-Verbindungsversuchen mit Retry-Mechanismus.
- Implementierung einer Autos-Generierung mit zufälligen Spawn-Positionen und Bewegungslogik.
- Einführung einer Minimap zur Anzeige der aktuellen Spielumgebung.
- Optimierung der Kollisionserkennung zwischen Taxi und Autos.
- Verbesserung der Speicherbereinigung und Performance durch throttling von Timer-Updates.

Diese Anpassungen erweitern die Spielmechanik und Benutzererfahrung, indem sie die Interaktivität und die grafische Darstellung im Taxi-Spiel verbessern.
This commit is contained in:
Torsten Schulz (local)
2025-10-06 11:58:51 +02:00
parent 1bde46430b
commit 828e45df35
2 changed files with 829 additions and 156 deletions

View File

@@ -11,6 +11,13 @@ const store = createStore({
connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error'
user: JSON.parse(localStorage.getItem('user')) || null, user: JSON.parse(localStorage.getItem('user')) || null,
// Reconnect state management
backendRetryCount: 0,
daemonRetryCount: 0,
backendRetryTimer: null,
daemonRetryTimer: null,
backendConnecting: false,
daemonConnecting: false,
language: (() => { language: (() => {
// Verwende die gleiche Logik wie in main.js // Verwende die gleiche Logik wie in main.js
const browserLanguage = navigator.language || navigator.languages[0]; const browserLanguage = navigator.language || navigator.languages[0];
@@ -112,6 +119,12 @@ const store = createStore({
state.socket.disconnect(); state.socket.disconnect();
} }
state.socket = null; state.socket = null;
// Cleanup retry timer
if (state.backendRetryTimer) {
clearTimeout(state.backendRetryTimer);
state.backendRetryTimer = null;
}
state.backendConnecting = false;
}, },
setDaemonSocket(state, daemonSocket) { setDaemonSocket(state, daemonSocket) {
state.daemonSocket = daemonSocket; state.daemonSocket = daemonSocket;
@@ -122,6 +135,12 @@ const store = createStore({
} }
state.daemonSocket = null; state.daemonSocket = null;
state.daemonConnectionStatus = 'disconnected'; state.daemonConnectionStatus = 'disconnected';
// Cleanup retry timer
if (state.daemonRetryTimer) {
clearTimeout(state.daemonRetryTimer);
state.daemonRetryTimer = null;
}
state.daemonConnecting = false;
}, },
}, },
actions: { actions: {
@@ -142,75 +161,98 @@ const store = createStore({
commit('dologout'); commit('dologout');
router.push('/'); router.push('/');
}, },
initializeSocket({ commit, state }) { initializeSocket({ commit, state, dispatch }) {
if (state.isLoggedIn && state.user) { if (!state.isLoggedIn || !state.user || state.backendConnecting) {
let currentSocket = state.socket;
const connectSocket = () => {
if (currentSocket) {
currentSocket.disconnect();
}
commit('setConnectionStatus', 'connecting');
// Socket.io URL für lokale Entwicklung und Produktion
let socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
// Für lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
}
const socket = io(socketIoUrl, {
secure: true,
transports: ['websocket', 'polling']
});
socket.on('connect', () => {
retryCount = 0; // Reset retry counter on successful connection
commit('setConnectionStatus', 'connected');
const idForSocket = state.user?.hashedId || state.user?.id;
if (idForSocket) socket.emit('setUserId', idForSocket);
});
socket.on('disconnect', (reason) => {
commit('setConnectionStatus', 'disconnected');
retryConnection(connectSocket);
});
socket.on('connect_error', (error) => {
commit('setConnectionStatus', 'error');
});
commit('setSocket', socket);
};
let retryCount = 0;
const maxRetries = 10;
const retryConnection = (reconnectFn) => {
console.log(`Backend-Reconnect-Versuch ${retryCount + 1}/${maxRetries}`);
if (retryCount >= maxRetries) {
// Nach maxRetries alle 5 Sekunden weiter versuchen
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(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
setTimeout(() => {
reconnectFn();
}, delay);
};
connectSocket();
}
},
initializeDaemonSocket({ commit, state }) {
if (!state.isLoggedIn || !state.user) {
return; return;
} }
state.backendConnecting = true;
const connectSocket = () => {
// Cleanup existing socket and timer
if (state.socket) {
state.socket.disconnect();
}
if (state.backendRetryTimer) {
clearTimeout(state.backendRetryTimer);
state.backendRetryTimer = null;
}
commit('setConnectionStatus', 'connecting');
// Socket.io URL für lokale Entwicklung und Produktion
let socketIoUrl = import.meta.env.VITE_SOCKET_IO_URL || import.meta.env.VITE_API_BASE_URL;
// Für lokale Entwicklung: direkte Backend-Verbindung
if (!socketIoUrl && (import.meta.env.DEV || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
socketIoUrl = 'http://localhost:3001';
}
const socket = io(socketIoUrl, {
secure: true,
transports: ['websocket', 'polling']
});
socket.on('connect', () => {
state.backendRetryCount = 0; // Reset retry counter on successful connection
state.backendConnecting = false;
commit('setConnectionStatus', 'connected');
const idForSocket = state.user?.hashedId || state.user?.id;
if (idForSocket) socket.emit('setUserId', idForSocket);
});
socket.on('disconnect', (reason) => {
commit('setConnectionStatus', 'disconnected');
dispatch('retryBackendConnection');
});
socket.on('connect_error', (error) => {
commit('setConnectionStatus', 'error');
dispatch('retryBackendConnection');
});
commit('setSocket', socket);
};
connectSocket();
},
retryBackendConnection({ commit, state }) {
if (state.backendRetryTimer || state.backendConnecting) {
return; // Already retrying or connecting
}
const maxRetries = 10;
console.log(`Backend-Reconnect-Versuch ${state.backendRetryCount + 1}/${maxRetries}`);
if (state.backendRetryCount >= maxRetries) {
// Nach maxRetries alle 5 Sekunden weiter versuchen
console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
state.backendRetryTimer = setTimeout(() => {
state.backendRetryCount = 0; // Reset für nächsten Zyklus
state.backendRetryTimer = null;
commit('setConnectionStatus', 'connecting');
// Recursive call to retry
setTimeout(() => this.dispatch('retryBackendConnection'), 100);
}, 5000);
return;
}
state.backendRetryCount++;
const delay = 5000; // Alle 5 Sekunden versuchen
console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
state.backendRetryTimer = setTimeout(() => {
state.backendRetryTimer = null;
this.dispatch('initializeSocket');
}, delay);
},
initializeDaemonSocket({ commit, state, dispatch }) {
if (!state.isLoggedIn || !state.user || state.daemonConnecting) {
return;
}
state.daemonConnecting = true;
// Daemon URL für lokale Entwicklung und Produktion // Daemon URL für lokale Entwicklung und Produktion
let daemonUrl = import.meta.env.VITE_DAEMON_SOCKET; let daemonUrl = import.meta.env.VITE_DAEMON_SOCKET;
@@ -224,8 +266,16 @@ const store = createStore({
daemonUrl = 'wss://www.your-part.de:4551'; daemonUrl = 'wss://www.your-part.de:4551';
} }
const connectDaemonSocket = () => { const connectDaemonSocket = () => {
// Cleanup existing socket and timer
if (state.daemonSocket) {
state.daemonSocket.close();
}
if (state.daemonRetryTimer) {
clearTimeout(state.daemonRetryTimer);
state.daemonRetryTimer = null;
}
// Protokoll-Fallback: zuerst mit Subprotokoll, dann ohne // Protokoll-Fallback: zuerst mit Subprotokoll, dann ohne
const protocols = ['yourpart-protocol', undefined]; const protocols = ['yourpart-protocol', undefined];
let attemptIndex = 0; let attemptIndex = 0;
@@ -242,7 +292,8 @@ const store = createStore({
daemonSocket.onopen = () => { daemonSocket.onopen = () => {
opened = true; opened = true;
retryCount = 0; // Reset retry counter on successful connection state.daemonRetryCount = 0; // Reset retry counter on successful connection
state.daemonConnecting = false;
commit('setDaemonConnectionStatus', 'connected'); commit('setDaemonConnectionStatus', 'connected');
const payload = JSON.stringify({ const payload = JSON.stringify({
event: 'setUserId', event: 'setUserId',
@@ -259,7 +310,7 @@ const store = createStore({
tryConnectWithProtocol(); tryConnectWithProtocol();
return; return;
} }
retryConnection(connectDaemonSocket); dispatch('retryDaemonConnection');
}; };
daemonSocket.onerror = (error) => { daemonSocket.onerror = (error) => {
@@ -270,7 +321,7 @@ const store = createStore({
tryConnectWithProtocol(); tryConnectWithProtocol();
return; return;
} }
retryConnection(connectDaemonSocket); dispatch('retryDaemonConnection');
}; };
daemonSocket.addEventListener('message', (event) => { daemonSocket.addEventListener('message', (event) => {
@@ -295,35 +346,45 @@ const store = createStore({
tryConnectWithProtocol(); tryConnectWithProtocol();
return; return;
} }
retryConnection(connectDaemonSocket); dispatch('retryDaemonConnection');
} }
}; };
tryConnectWithProtocol(); tryConnectWithProtocol();
}; };
let retryCount = 0;
const maxRetries = 15; // Increased max retries
const retryConnection = (reconnectFn) => {
console.log(`Daemon-Reconnect-Versuch ${retryCount + 1}/${maxRetries}`);
if (retryCount >= maxRetries) {
// 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;
}
retryCount++;
const delay = 5000; // Alle 5 Sekunden versuchen
console.log(`Daemon: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
setTimeout(() => {
reconnectFn();
}, delay);
};
connectDaemonSocket(); connectDaemonSocket();
}, },
retryDaemonConnection({ commit, state }) {
if (state.daemonRetryTimer || state.daemonConnecting) {
return; // Already retrying or connecting
}
const maxRetries = 15;
console.log(`Daemon-Reconnect-Versuch ${state.daemonRetryCount + 1}/${maxRetries}`);
if (state.daemonRetryCount >= maxRetries) {
// Nach maxRetries alle 5 Sekunden weiter versuchen
console.log('Daemon: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
state.daemonRetryTimer = setTimeout(() => {
state.daemonRetryCount = 0; // Reset für nächsten Zyklus
state.daemonRetryTimer = null;
commit('setDaemonConnectionStatus', 'connecting');
// Recursive call to retry
setTimeout(() => this.dispatch('retryDaemonConnection'), 100);
}, 5000);
return;
}
state.daemonRetryCount++;
const delay = 5000; // Alle 5 Sekunden versuchen
console.log(`Daemon: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
state.daemonRetryTimer = setTimeout(() => {
state.daemonRetryTimer = null;
this.dispatch('initializeDaemonSocket');
}, delay);
},
setLanguage({ commit }, language) { setLanguage({ commit }, language) {
commit('setLanguage', language); commit('setLanguage', language);
}, },
@@ -352,7 +413,8 @@ const store = createStore({
}, },
}); });
if (store.state.isLoggedIn && store.state.user) { // Initialisierung beim Laden der Anwendung - nur einmal ausführen
if (store.state.isLoggedIn && store.state.user && !store.state.backendConnecting && !store.state.daemonConnecting) {
store.dispatch('initializeSocket'); store.dispatch('initializeSocket');
store.dispatch('initializeDaemonSocket'); store.dispatch('initializeDaemonSocket');
} }

View File

@@ -162,6 +162,32 @@
<!-- Sidebar (rechts) --> <!-- Sidebar (rechts) -->
<div class="sidebar-section"> <div class="sidebar-section">
<!-- Minimap -->
<div class="minimap-card">
<div class="minimap-header">
<h3 class="minimap-title">Minimap</h3>
<div class="map-selector">
<select v-model="selectedMapId" @change="onMapChange" class="map-select">
<option
v-for="map in maps"
:key="map.id"
:value="map.id"
>
{{ map.name }}
</option>
</select>
</div>
</div>
<div class="minimap-container">
<canvas
ref="minimapCanvas"
width="200"
height="150"
class="minimap-canvas"
></canvas>
</div>
</div>
<!-- Geladene Passagiere --> <!-- Geladene Passagiere -->
<div class="loaded-passengers-card"> <div class="loaded-passengers-card">
<div class="loaded-passengers-header"> <div class="loaded-passengers-header">
@@ -208,49 +234,16 @@
<div v-if="waitingPassengersList.length === 0" class="no-passengers"> <div v-if="waitingPassengersList.length === 0" class="no-passengers">
Keine wartenden Passagiere Keine wartenden Passagiere
</div> </div>
<div v-else> <table v-else>
<div <tr v-for="(passenger, index) in waitingPassengersList"
v-for="(passenger, index) in waitingPassengersList" :key="index" >
:key="index" <td><b>{{ passenger.name }}</b><span v-if="getPassengerTimeLeft(passenger) <= 5" class="passenger-timer"> ({{ getPassengerTimeLeft(passenger) }}s)</span></td>
class="passenger-item" <td>{{ passenger.location }}</td>
> </tr>
<div class="passenger-info"> </table>
<span class="passenger-name">
{{ passenger.name }}
<span v-if="getPassengerTimeLeft(passenger) <= 5" class="passenger-timer">({{ getPassengerTimeLeft(passenger) }}s)</span>
</span>
<span class="passenger-location">{{ passenger.location }}</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- Minimap -->
<div class="minimap-card">
<div class="minimap-header">
<h3 class="minimap-title">Minimap</h3>
<div class="map-selector">
<select v-model="selectedMapId" @change="onMapChange" class="map-select">
<option
v-for="map in maps"
:key="map.id"
:value="map.id"
>
{{ map.name }}
</option>
</select>
</div>
</div>
<div class="minimap-container">
<canvas
ref="minimapCanvas"
width="200"
height="150"
class="minimap-canvas"
></canvas>
</div>
</div>
</div> </div>
</div> </div>
@@ -328,6 +321,7 @@ export default {
lastViolationSound: 0, lastViolationSound: 0,
lastMinimapDraw: 0, lastMinimapDraw: 0,
minimapDrawInterval: 120, minimapDrawInterval: 120,
lastPassengerTimerUpdate: 0, // Throttling für Passagier-Timer-Updates
radarImg: null, radarImg: null,
activeRadar: false, activeRadar: false,
radarAtTopEdge: true, // legacy flag (nicht mehr genutzt) radarAtTopEdge: true, // legacy flag (nicht mehr genutzt)
@@ -360,6 +354,12 @@ export default {
,loadingHighscore: false // Lade-Status für Highscore ,loadingHighscore: false // Lade-Status für Highscore
,currentPlayerEntry: null // Eintrag des aktuellen Spielers ,currentPlayerEntry: null // Eintrag des aktuellen Spielers
,showCurrentPlayerBelow: false // Zeige aktuellen Spieler nach Platz 20 ,showCurrentPlayerBelow: false // Zeige aktuellen Spieler nach Platz 20
// Autos-System
,cars: [] // Liste der aktiven Autos
,carImage: null // Geladenes Auto-Bild
,lastCarGeneration: 0 // Zeitstempel der letzten Autos-Generierung
,carGenerationInterval: 1000 // Autos-Generierung alle 1000ms (1 Sekunde) prüfen
,carSpawnProbability: 0.2 // 20% Wahrscheinlichkeit pro Sekunde
} }
}, },
computed: { computed: {
@@ -401,15 +401,22 @@ export default {
this.loadTaxiImage(); this.loadTaxiImage();
this.loadHouseImage(); this.loadHouseImage();
this.loadPassengerImages(); this.loadPassengerImages();
this.loadCarImage();
this.loadMaps(); this.loadMaps();
this.setupEventListeners(); this.setupEventListeners();
await this.initializeMotorSound(); await this.initializeMotorSound();
this.setupAudioUnlockHandlers(); this.setupAudioUnlockHandlers();
this.lastTrafficLightTick = Date.now(); this.lastTrafficLightTick = Date.now();
// Memory-Cleanup seltener ausführen, um Render-Glitches zu vermeiden
this.memoryCleanupInterval = setInterval(() => {
this.performMemoryCleanup();
}, 2 * 60 * 1000); // alle 2 Minuten
}, },
beforeUnmount() { beforeUnmount() {
console.log('🚪 Component unmounting, cleaning up...');
this.cleanup(); this.cleanup();
}, },
methods: { methods: {
// Ampelschaltung: sekündliche Phasen-Updates; pro Tile ein State // Ampelschaltung: sekündliche Phasen-Updates; pro Tile ein State
updateTrafficLights() { updateTrafficLights() {
@@ -738,19 +745,25 @@ export default {
}, },
cleanup() { cleanup() {
console.log('🧹 Starting cleanup...');
// Game Loop stoppen
if (this.gameLoop) { if (this.gameLoop) {
cancelAnimationFrame(this.gameLoop); cancelAnimationFrame(this.gameLoop);
this.gameLoop = null; this.gameLoop = null;
} }
if (this.motorSound) { this.motorSound.stop(); }
// Motor Sound stoppen
if (this.motorSound) {
this.motorSound.stop();
this.motorSound = null;
}
// Alle Timeouts bereinigen
if (this.motorStopTimeout) { if (this.motorStopTimeout) {
clearTimeout(this.motorStopTimeout); clearTimeout(this.motorStopTimeout);
this.motorStopTimeout = null; this.motorStopTimeout = null;
} }
// Event-Listener von Document entfernen
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
// Cleanup aller Timeouts
if (this.passengerGenerationTimeout) { if (this.passengerGenerationTimeout) {
clearTimeout(this.passengerGenerationTimeout); clearTimeout(this.passengerGenerationTimeout);
this.passengerGenerationTimeout = null; this.passengerGenerationTimeout = null;
@@ -759,17 +772,116 @@ export default {
clearTimeout(this.crashDialogTimeout); clearTimeout(this.crashDialogTimeout);
this.crashDialogTimeout = null; this.crashDialogTimeout = null;
} }
// AudioContext bleibt global erhalten, nicht schließen if (this.memoryCleanupInterval) {
clearInterval(this.memoryCleanupInterval);
this.memoryCleanupInterval = null;
}
// Event-Listener von Document entfernen
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
// Audio Unlock Handler bereinigen
if (this.audioUnlockHandler) { if (this.audioUnlockHandler) {
document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true }); document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true });
document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true }); document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true });
document.removeEventListener('keydown', this.audioUnlockHandler, { capture: true }); document.removeEventListener('keydown', this.audioUnlockHandler, { capture: true });
this.audioUnlockHandler = null; this.audioUnlockHandler = null;
} }
// Cleanup von Listen und Sets
// Listen und Sets bereinigen
this.waitingPassengersList = []; this.waitingPassengersList = [];
this.loadedPassengersList = []; this.loadedPassengersList = [];
this.occupiedHouses.clear(); this.occupiedHouses.clear();
this.cars = []; // Autos bereinigen
// Canvas Context bereinigen
if (this.ctx) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx = null;
}
// Canvas Referenz bereinigen
this.canvas = null;
// Audio Context bereinigen (optional - kann global bleiben)
// if (this.audioContext && this.audioContext.state !== 'closed') {
// this.audioContext.close();
// this.audioContext = null;
// }
// Maps und Objekte bereinigen
this.trafficLightStates = {};
this.keys = {};
// Bilder und Assets bereinigen
this.taxiImage = null;
this.houseImage = null;
this.passengerImages = {};
this.carImage = null; // Auto-Bild bereinigen
this.tiles = null;
console.log('🧹 Cleanup completed');
},
// Regelmäßige Memory-Cleanup-Methode
performMemoryCleanup() {
console.log('🧹 Performing memory cleanup...');
// Canvas NICHT leeren das verursacht sichtbares Flackern / Grau
// Wir verlassen uns auf das reguläre render() zum Überschreiben des Frames
// Traffic Light States aggressiver bereinigen
if (this.trafficLightStates && Object.keys(this.trafficLightStates).length > 20) {
console.log('🧹 Cleaning up traffic light states');
// Nur States für aktuelle Map behalten
if (this.currentMap && this.currentMap.tiles) {
const currentTileKeys = new Set();
this.currentMap.tiles.forEach(tile => {
if (tile.trafficLight || (tile.meta && tile.meta.trafficLight)) {
currentTileKeys.add(`${tile.x},${tile.y}`);
}
});
// Entferne States für nicht mehr existierende Tiles
Object.keys(this.trafficLightStates).forEach(key => {
if (!currentTileKeys.has(key)) {
delete this.trafficLightStates[key];
}
});
} else {
// Fallback: komplett leeren wenn keine Map
this.trafficLightStates = {};
}
}
// Passagier-Listen aggressiver begrenzen
if (this.waitingPassengersList && this.waitingPassengersList.length > 20) {
console.log('🧹 Trimming waiting passengers list');
this.waitingPassengersList = this.waitingPassengersList.slice(-10);
}
if (this.loadedPassengersList && this.loadedPassengersList.length > 20) {
console.log('🧹 Trimming loaded passengers list');
this.loadedPassengersList = this.loadedPassengersList.slice(-10);
}
// Autos-Liste NICHT hart trimmen, um keine abrupten Desyncs zu erzeugen
// Keys-Objekt bereinigen (entferne nicht mehr gedrückte Tasten)
if (this.keys) {
const activeKeys = Object.keys(this.keys).filter(key => this.keys[key]);
if (activeKeys.length === 0) {
this.keys = {};
}
}
// Force Garbage Collection (falls verfügbar)
if (window.gc) {
window.gc();
}
console.log('🧹 Memory cleanup completed');
}, },
generateLevel() { generateLevel() {
@@ -1229,6 +1341,363 @@ export default {
// Passagiere wurden entfernt (abgelaufen) // Passagiere wurden entfernt (abgelaufen)
}, },
// Autos-Generierung mit 20% Wahrscheinlichkeit pro Sekunde
updateCarGeneration() {
const now = Date.now();
// Prüfe alle Sekunde
if (now - this.lastCarGeneration < this.carGenerationInterval) {
return;
}
this.lastCarGeneration = now;
// 20% Wahrscheinlichkeit pro Sekunde
if (!this.isPaused && Math.random() < this.carSpawnProbability) {
this.spawnCar(now);
}
},
// Spawne ein neues Auto
spawnCar(now = Date.now()) {
// Begrenze die Anzahl der Autos
if (this.cars.length >= 8) {
return;
}
// Spawne Auto auf einer befahrbaren Position
const spawnPosition = this.getRandomCarSpawnPosition();
if (!spawnPosition) {
return; // Keine gültige Spawn-Position gefunden
}
const car = {
id: Date.now() + Math.random(), // Eindeutige ID
x: spawnPosition.x,
y: spawnPosition.y,
angle: spawnPosition.angle,
speed: 0.3 + Math.random() * 0.7, // Langsamere Geschwindigkeit zwischen 0.3-1.0
width: 60, // Etwas breiter als Taxi
height: 50, // Gleiche Höhe wie Taxi
createdAt: now,
color: this.getRandomCarColor(),
isCar: true,
currentTile: { row: 0, col: 0 }, // Aktuelle Tile-Position
direction: spawnPosition.direction, // Fahrtrichtung
lastPosition: { x: spawnPosition.x, y: spawnPosition.y }, // Letzte Position für Bewegung
hasTurnedAtIntersection: false, // Flag um mehrfaches Abbiegen zu verhindern
targetDirection: null, // Zielrichtung an der Kreuzung (wird beim Tile-Betreten festgelegt)
targetLane: null // Zielspur basierend auf Zielrichtung
};
this.cars.push(car);
},
// Finde eine zufällige befahrbare Spawn-Position für ein Auto
getRandomCarSpawnPosition() {
if (!this.currentMap || !this.currentMap.tiles) {
return null;
}
const tileSize = this.tiles.size;
// Definiere die erlaubten Spawn-Positionen (relativ 0-1)
// Mit korrekter Straßenseite basierend auf echten Straßenkoordinaten
const spawnPositions = [
// Links spawnen, nach rechts fahren, auf der rechten Straßenseite (y=0.5-0.625)
{ relativeX: 0.1, relativeY: 0.5 + Math.random() * 0.125, angle: 0, direction: 'right' },
// Rechts spawnen, nach links fahren, auf der linken Straßenseite (y=0.375-0.5)
{ relativeX: 0.9, relativeY: 0.375 + Math.random() * 0.125, angle: Math.PI, direction: 'left' },
// Oben spawnen, nach unten fahren, auf der linken Straßenseite (x=0.375-0.5)
{ relativeX: 0.375 + Math.random() * 0.125, relativeY: 0.1, angle: Math.PI / 2, direction: 'down' },
// Unten spawnen, nach oben fahren, auf der rechten Straßenseite (x=0.5-0.625)
{ relativeX: 0.5 + Math.random() * 0.125, relativeY: 0.9, angle: -Math.PI / 2, direction: 'up' }
];
// Wähle eine zufällige Spawn-Position
const spawnPos = spawnPositions[Math.floor(Math.random() * spawnPositions.length)];
// Konvertiere relative Koordinaten zu absoluten Pixeln
const x = spawnPos.relativeX * tileSize;
const y = spawnPos.relativeY * tileSize;
// Prüfe ob Position befahrbar ist
if (this.isPositionDriveable(x, y)) {
return {
x: x,
y: y,
angle: spawnPos.angle,
direction: spawnPos.direction
};
}
// Fallback: Versuche andere Positionen
for (let i = 0; i < spawnPositions.length; i++) {
const pos = spawnPositions[i];
const testX = pos.relativeX * tileSize;
const testY = pos.relativeY * tileSize;
if (this.isPositionDriveable(testX, testY)) {
return {
x: testX,
y: testY,
angle: pos.angle,
direction: pos.direction
};
}
}
return null; // Keine gültige Position gefunden
},
// Prüfe ob eine Position befahrbar ist
isPositionDriveable(x, y) {
const tileType = this.getCurrentTileType();
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
// Konvertiere absolute Koordinaten zu relativen (0-1)
const relativeX = x / this.tiles.size;
const relativeY = y / this.tiles.size;
return streetCoordinates.isPointDriveable(relativeX, relativeY, streetTileType, 1);
},
// Zufällige Auto-Farbe
getRandomCarColor() {
const colors = [
'#FF0000', // Rot
'#0000FF', // Blau
'#00FF00', // Grün
'#FFFF00', // Gelb
'#FF00FF', // Magenta
'#00FFFF', // Cyan
'#FFFFFF', // Weiß
'#000000' // Schwarz
];
return colors[Math.floor(Math.random() * colors.length)];
},
// Aktualisiere alle Autos
updateCars() {
const now = Date.now();
const maxAge = 45000; // 45 Sekunden
this.cars = this.cars.filter(car => {
// Entferne alte Autos
if (now - car.createdAt > maxAge) {
return false;
}
// Bewege das Auto nur wenn es auf einer befahrbaren Position ist
this.updateCarMovement(car);
// Entferne Autos, die außerhalb des Bildschirms sind
if (car.x < -50 || car.x > 550 || car.y < -50 || car.y > 550) {
return false;
}
return true;
});
},
// Aktualisiere die Bewegung eines einzelnen Autos
updateCarMovement(car) {
// Speichere aktuelle Position
car.lastPosition = { x: car.x, y: car.y };
// Setze Zielrichtung beim ersten Betreten des Tiles
if (!car.targetDirection) {
car.targetDirection = this.getRandomTargetDirection(car.direction);
car.targetLane = this.getTargetLane(car.targetDirection);
}
// Prüfe ob Auto an einer Kreuzung ist und zur Zielrichtung abbiegen soll
if (this.shouldCarTurnAtIntersection(car) && !car.hasTurnedAtIntersection) {
this.turnCarToTarget(car);
car.hasTurnedAtIntersection = true;
}
// Berechne neue Position
const newX = car.x + Math.cos(car.angle) * car.speed;
const newY = car.y + Math.sin(car.angle) * car.speed;
// Prüfe ob neue Position befahrbar ist
if (this.isPositionDriveable(newX, newY)) {
// Position ist befahrbar - bewege Auto
car.x = newX;
car.y = newY;
} else {
// Vorzugsweise zur geplanten Zielrichtung abbiegen (z. B. Kurve unten->rechts)
if (car.targetDirection) {
const prevAngle = car.angle;
const prevDir = car.direction;
this.turnCarToTarget(car);
const tryX = car.x + Math.cos(car.angle) * car.speed;
const tryY = car.y + Math.sin(car.angle) * car.speed;
if (this.isPositionDriveable(tryX, tryY)) {
car.x = tryX;
car.y = tryY;
} else {
// Rückgängig machen, falls Zielrichtung auch nicht geht
car.angle = prevAngle;
car.direction = prevDir;
// Fallback: alternative Richtung suchen (ohne zurückzufahren)
this.adjustCarDirection(car);
}
} else {
// Position ist nicht befahrbar - suche alternative Richtung (ohne zurückzufahren)
this.adjustCarDirection(car);
}
}
},
// Bestimme zufällige Zielrichtung basierend auf Tile-Art und aktueller Richtung
getRandomTargetDirection(currentDirection) {
const tileType = this.getCurrentTileType();
const possibleDirections = [];
// Bestimme mögliche Richtungen basierend auf Tile-Typ
switch (tileType) {
case 'cornerTopLeft':
// L-förmige Kurve: oben-links
if (currentDirection === 'right') possibleDirections.push('up');
else if (currentDirection === 'down') possibleDirections.push('left');
break;
case 'cornerTopRight':
// L-förmige Kurve: oben-rechts
if (currentDirection === 'left') possibleDirections.push('up');
else if (currentDirection === 'down') possibleDirections.push('right');
break;
case 'cornerBottomLeft':
// L-förmige Kurve: unten-links
if (currentDirection === 'right') possibleDirections.push('down');
else if (currentDirection === 'up') possibleDirections.push('left');
break;
case 'cornerBottomRight':
// L-förmige Kurve: unten-rechts
if (currentDirection === 'left') possibleDirections.push('down');
else if (currentDirection === 'up') possibleDirections.push('right');
break;
case 'crossing':
// Kreuzung: alle Richtungen außer zurück
switch (currentDirection) {
case 'right': possibleDirections.push('up', 'down'); break;
case 'left': possibleDirections.push('up', 'down'); break;
case 'up': possibleDirections.push('left', 'right'); break;
case 'down': possibleDirections.push('left', 'right'); break;
}
break;
default:
// Für andere Tile-Typen: nur geradeaus
possibleDirections.push(currentDirection);
break;
}
// Fallback: wenn keine Richtungen gefunden, geradeaus
if (possibleDirections.length === 0) {
possibleDirections.push(currentDirection);
}
return possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
},
// Bestimme Zielspur basierend auf Zielrichtung
getTargetLane(targetDirection) {
switch (targetDirection) {
case 'left':
return { y: 0.375 + Math.random() * 0.125 }; // y=0.375-0.5
case 'right':
return { y: 0.5 + Math.random() * 0.125 }; // y=0.5-0.625
case 'down':
return { x: 0.375 + Math.random() * 0.125 }; // x=0.375-0.5
case 'up':
return { x: 0.5 + Math.random() * 0.125 }; // x=0.5-0.625
}
},
// Prüfe ob Auto an Kreuzung zur Zielrichtung abbiegen soll
shouldCarTurnAtIntersection(car) {
const relativeX = car.x / this.tiles.size;
const relativeY = car.y / this.tiles.size;
// Prüfe ob Auto sehr nah an der Straßenmitte ist (genauere Kreuzungserkennung)
const atCenter = (relativeX > 0.48 && relativeX < 0.52) && (relativeY > 0.48 && relativeY < 0.52);
return atCenter;
},
// Auto zur Zielrichtung abbiegen
turnCarToTarget(car) {
const targetDirection = car.targetDirection;
switch (targetDirection) {
case 'left':
car.angle = Math.PI;
car.direction = 'left';
break;
case 'right':
car.angle = 0;
car.direction = 'right';
break;
case 'down':
car.angle = Math.PI / 2;
car.direction = 'down';
break;
case 'up':
car.angle = -Math.PI / 2;
car.direction = 'up';
break;
}
},
// Passe die Richtung eines Autos an, wenn es nicht weiterfahren kann
adjustCarDirection(car) {
const directions = [
{ angle: 0, name: 'right' },
{ angle: Math.PI, name: 'left' },
{ angle: Math.PI / 2, name: 'down' },
{ angle: -Math.PI / 2, name: 'up' }
];
// Gegengerichtete Richtung (zurückfahren) ausschließen
const opposite = {
right: 'left',
left: 'right',
up: 'down',
down: 'up'
}[car.direction];
// Versuche verschiedene Richtungen, zuerst die geplante Zielrichtung, dann andere (ohne zurück)
const preferredOrder = [];
if (car.targetDirection && car.targetDirection !== opposite) preferredOrder.push(car.targetDirection);
directions.forEach(d => {
if (d.name !== opposite && d.name !== (preferredOrder[0] || '')) preferredOrder.push(d.name);
});
for (let i = 0; i < preferredOrder.length; i++) {
const name = preferredOrder[i];
const angle = name === 'right' ? 0 : (name === 'left' ? Math.PI : (name === 'down' ? Math.PI/2 : -Math.PI/2));
const testX = car.x + Math.cos(angle) * car.speed;
const testY = car.y + Math.sin(angle) * car.speed;
if (this.isPositionDriveable(testX, testY)) {
car.angle = angle;
car.direction = name;
return;
}
}
// Keine befahrbare Richtung gefunden - bewege zurück zur letzten Position
car.x = car.lastPosition.x;
car.y = car.lastPosition.y;
},
getPassengerTimeLeft(passenger) { getPassengerTimeLeft(passenger) {
const now = Date.now(); const now = Date.now();
const age = now - passenger.createdAt; const age = now - passenger.createdAt;
@@ -1354,7 +1823,7 @@ export default {
update() { update() {
if (!this.gameRunning || this.isPaused) { if (!this.gameRunning || this.isPaused) {
this.gameLoop = requestAnimationFrame(this.update); // Game Loop komplett stoppen wenn pausiert - kein unnötiges requestAnimationFrame
return; return;
} }
@@ -1364,14 +1833,25 @@ export default {
// Passagier-Generierung prüfen // Passagier-Generierung prüfen
this.updatePassengerGeneration(); this.updatePassengerGeneration();
// Autos-Generierung prüfen
this.updateCarGeneration();
// Autos aktualisieren
this.updateCars();
// Abgelaufene Passagiere entfernen // Abgelaufene Passagiere entfernen
this.removeExpiredPassengers(); this.removeExpiredPassengers();
this.updateTaxi(); this.updateTaxi();
this.handlePassengerActions(); this.handlePassengerActions();
// Timer für geladene Passagiere aktualisieren (nach Passagier-Aktionen) // Timer für geladene Passagiere aktualisieren (nach Passagier-Aktionen) - gedrosselt
this.updatePassengerTimers(); const nowTs = Date.now();
if (nowTs - this.lastPassengerTimerUpdate >= 200) { // Alle 200ms statt jeden Frame
this.updatePassengerTimers();
this.lastPassengerTimerUpdate = nowTs;
}
this.checkCollisions(); this.checkCollisions();
this.render(); this.render();
@@ -1379,7 +1859,6 @@ export default {
this.checkRadarMeasurement(); this.checkRadarMeasurement();
// Minimap zeichnen (gedrosselt) // Minimap zeichnen (gedrosselt)
const nowTs = Date.now();
if (nowTs - this.lastMinimapDraw >= this.minimapDrawInterval) { if (nowTs - this.lastMinimapDraw >= this.minimapDrawInterval) {
this.drawMinimap(); this.drawMinimap();
this.lastMinimapDraw = nowTs; this.lastMinimapDraw = nowTs;
@@ -1775,6 +2254,13 @@ export default {
this.handleCrash(); this.handleCrash();
} }
}); });
// Prüfe Autos-Kollisionen
this.cars.forEach(car => {
if (this.checkCollision(this.taxi, car)) {
this.handleCrash('auto');
}
});
} }
}, },
// Prüft Überfahren der (virtuell verdoppelten) Haltelinie aus der straßenzugewandten Seite // Prüft Überfahren der (virtuell verdoppelten) Haltelinie aus der straßenzugewandten Seite
@@ -2004,6 +2490,24 @@ export default {
this.decrementVehicle(reason); this.decrementVehicle(reason);
this.taxi.speed = 0; this.taxi.speed = 0;
this.isPaused = true; // Zuerst pausieren this.isPaused = true; // Zuerst pausieren
this.showPauseOverlay = true; // Overlay sichtbar machen
// Alle KI-Autos sofort entfernen
try { this.cars = []; } catch (_) {}
// Game Loop stoppen
if (this.gameLoop) {
cancelAnimationFrame(this.gameLoop);
this.gameLoop = null;
}
// Motor sofort stoppen
try { if (this.motorSound && this.motorSound.isPlaying) this.motorSound.stop(); } catch (_) {}
// Eingaben zurücksetzen, sonst bleibt "beschleunigen" aus
this.keys = {};
this.lastSpeedChange = 0;
this.fuel = 100; this.fuel = 100;
}, },
@@ -2055,11 +2559,31 @@ export default {
console.log('Nach Dialog-Close - isPaused:', this.isPaused, 'showPauseOverlay:', this.showPauseOverlay); console.log('Nach Dialog-Close - isPaused:', this.isPaused, 'showPauseOverlay:', this.showPauseOverlay);
// Game Loop sicher neu starten (unabhängig vom vorherigen Zustand)
this.gameRunning = true;
try { cancelAnimationFrame(this.gameLoop); } catch (_) {}
this.gameLoop = requestAnimationFrame(this.update);
// Taxi bleibt auf dem aktuellen Tile, mittig platzieren // Taxi bleibt auf dem aktuellen Tile, mittig platzieren
this.taxi.speed = 0; this.taxi.speed = 0;
this.taxi.angle = 0; this.taxi.angle = 0;
this.centerTaxiInCurrentTile(); this.centerTaxiInCurrentTile();
// Eingabestatus zurücksetzen, damit Beschleunigen wieder funktioniert
this.keys = {};
this.lastSpeedChange = 0;
// Key-Listener frisch registrieren und Fokus auf Canvas erzwingen
try { document.removeEventListener('keydown', this.handleKeyDown); } catch (_) {}
document.addEventListener('keydown', this.handleKeyDown);
try { this.canvas && this.canvas.focus && this.canvas.focus(); } catch (_) {}
// Motor neu initialisieren, falls erforderlich
try {
if (this.motorSound && this.motorSound.isPlaying) this.motorSound.stop();
if (this.motorSound && !this.motorSound.isInitialized && this.audioContext) {
this.motorSound.init();
}
} catch (_) {}
// Fokus zurück auf Canvas setzen // Fokus zurück auf Canvas setzen
this.$nextTick(() => { this.$nextTick(() => {
if (this.canvas) { if (this.canvas) {
@@ -2511,10 +3035,24 @@ export default {
}, },
checkCollision(rect1, rect2) { checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width && // Optionaler Puffer, um die Kollision weniger sensibel zu machen (sichtbarer Kontakt nötig)
rect1.x + rect1.width > rect2.x && // Für Car-vs-Taxi prüfen wir einen negativen Puffer (verkleinert Hitboxen)
rect1.y < rect2.y + rect2.height && const pad = (rect1.isCar || rect2.isCar) ? 4 : 0; // 4px Puffer pro Seite bei Autos (präziser Crash)
rect1.y + rect1.height > rect2.y;
const aLeft = rect1.x + pad;
const aRight = rect1.x + rect1.width - pad;
const aTop = rect1.y + pad;
const aBottom = rect1.y + rect1.height - pad;
const bLeft = rect2.x + pad;
const bRight = rect2.x + rect2.width - pad;
const bTop = rect2.y + pad;
const bBottom = rect2.y + rect2.height - pad;
return aLeft < bRight &&
aRight > bLeft &&
aTop < bBottom &&
aBottom > bTop;
}, },
render() { render() {
@@ -2541,6 +3079,8 @@ export default {
// Passagiere/Ziele werden aktuell nicht gezeichnet // Passagiere/Ziele werden aktuell nicht gezeichnet
// Zeichne Autos
this.drawCars();
// Zeichne Taxi // Zeichne Taxi
this.ctx.save(); this.ctx.save();
@@ -2567,6 +3107,39 @@ export default {
this.ctx.restore(); this.ctx.restore();
}, },
// Zeichne alle Autos
drawCars() {
this.cars.forEach(car => {
this.ctx.save();
this.ctx.translate(car.x + car.width/2, car.y + car.height/2);
// Korrigiere die Rotation um 90° + weitere 180° - das Auto-Bild zeigt bereits in die richtige Richtung
this.ctx.rotate(car.angle + Math.PI / 2 + Math.PI);
if (this.carImage) {
// Zeichne Auto-Bild
this.ctx.drawImage(
this.carImage,
-car.width/2,
-car.height/2,
car.width,
car.height
);
} else {
// Fallback: Zeichne farbiges Rechteck wenn Bild nicht geladen
this.ctx.fillStyle = car.color;
this.ctx.fillRect(-car.width/2, -car.height/2, car.width, car.height);
// Zeichne schwarze Umrandung
this.ctx.strokeStyle = '#000000';
this.ctx.lineWidth = 2;
this.ctx.strokeRect(-car.width/2, -car.height/2, car.width, car.height);
}
this.ctx.restore();
});
},
drawRoads() { drawRoads() {
const tileSize = this.tiles.size; // 400x400px const tileSize = this.tiles.size; // 400x400px
@@ -2911,7 +3484,15 @@ export default {
}, },
async handleKeyDown(event) { async handleKeyDown(event) {
this.keys[event.key] = true; // Browser-Shortcuts (F-Tasten, Strg/Meta+R) passieren lassen
const key = event.key;
const isFunctionKey = /^F\d{1,2}$/.test(key);
const isReloadShortcut = (event.ctrlKey || event.metaKey) && (key === 'r' || key === 'R');
if (isFunctionKey || isReloadShortcut) {
return; // nicht abfangen, Browser soll handeln
}
this.keys[key] = true;
// AudioContext bei erster Benutzerinteraktion initialisieren // AudioContext bei erster Benutzerinteraktion initialisieren
this.ensureAudioUnlockedInEvent(); this.ensureAudioUnlockedInEvent();
@@ -2962,7 +3543,13 @@ export default {
}, },
handleKeyUp(event) { handleKeyUp(event) {
this.keys[event.key] = false; const key = event.key;
const isFunctionKey = /^F\d{1,2}$/.test(key);
const isReloadShortcut = (event.ctrlKey || event.metaKey) && (key === 'r' || key === 'R');
if (isFunctionKey || isReloadShortcut) {
return; // Browser-Shortcut nichts am Spielzustand ändern
}
this.keys[key] = false;
}, },
togglePause() { togglePause() {
@@ -3098,6 +3685,17 @@ export default {
} }
}, },
loadCarImage() {
const img = new Image();
img.onload = () => {
this.carImage = img;
};
img.onerror = () => {
console.warn('Fehler beim Laden von car1.svg');
};
img.src = '/images/taxi/car1.svg';
},
async loadMaps() { async loadMaps() {
try { try {
const response = await apiClient.get('/api/taxi-maps/maps'); const response = await apiClient.get('/api/taxi-maps/maps');
@@ -3501,6 +4099,13 @@ export default {
// Spiel pausieren wenn Highscore angezeigt wird // Spiel pausieren wenn Highscore angezeigt wird
if (!this.isPaused) { if (!this.isPaused) {
this.isPaused = true; this.isPaused = true;
// Game Loop stoppen
if (this.gameLoop) {
cancelAnimationFrame(this.gameLoop);
this.gameLoop = null;
}
// Motorgeräusch stoppen wenn pausiert // Motorgeräusch stoppen wenn pausiert
if (this.motorSound && this.motorSound.isPlaying) { if (this.motorSound && this.motorSound.isPlaying) {
this.motorSound.stop(); this.motorSound.stop();
@@ -3512,6 +4117,12 @@ export default {
// Highscore geschlossen - Spiel automatisch fortsetzen // Highscore geschlossen - Spiel automatisch fortsetzen
this.isPaused = false; this.isPaused = false;
this.showPauseOverlay = false; this.showPauseOverlay = false;
// Game Loop neu starten
if (this.gameRunning && !this.gameLoop) {
this.gameLoop = requestAnimationFrame(this.update);
}
// Motor startet automatisch bei der nächsten Beschleunigung // Motor startet automatisch bei der nächsten Beschleunigung
} }
}, },
@@ -3727,7 +4338,7 @@ export default {
.loaded-passengers-header { .loaded-passengers-header {
background: #4caf50; background: #4caf50;
border-bottom: 1px solid #4caf50; border-bottom: 1px solid #4caf50;
padding: 15px 20px; padding: 5px 20px;
} }
.loaded-passengers-title { .loaded-passengers-title {
@@ -3738,7 +4349,7 @@ export default {
} }
.loaded-passengers-content { .loaded-passengers-content {
padding: 15px; padding: 5px 20px;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
} }
@@ -3842,7 +4453,7 @@ export default {
.waiting-passengers-header { .waiting-passengers-header {
background: #f8f9fa; background: #f8f9fa;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 15px 20px; padding: 5px 20px;
} }
.waiting-passengers-title { .waiting-passengers-title {
@@ -3853,7 +4464,7 @@ export default {
} }
.waiting-passengers-list { .waiting-passengers-list {
padding: 20px; padding: 5px 20px;
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }
@@ -3862,7 +4473,7 @@ export default {
text-align: center; text-align: center;
color: #666; color: #666;
font-style: italic; font-style: italic;
padding: 20px 0; padding: 5px 0;
} }
.passenger-item { .passenger-item {
@@ -3928,7 +4539,7 @@ export default {
.minimap-header { .minimap-header {
background: #f8f9fa; background: #f8f9fa;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
padding: 15px 20px; padding: 5px 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;