diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 1d40b02..b74282d 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -11,6 +11,13 @@ const store = createStore({ connectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' daemonConnectionStatus: 'disconnected', // 'connected', 'connecting', 'disconnected', 'error' user: JSON.parse(localStorage.getItem('user')) || null, + // Reconnect state management + backendRetryCount: 0, + daemonRetryCount: 0, + backendRetryTimer: null, + daemonRetryTimer: null, + backendConnecting: false, + daemonConnecting: false, language: (() => { // Verwende die gleiche Logik wie in main.js const browserLanguage = navigator.language || navigator.languages[0]; @@ -112,6 +119,12 @@ const store = createStore({ state.socket.disconnect(); } state.socket = null; + // Cleanup retry timer + if (state.backendRetryTimer) { + clearTimeout(state.backendRetryTimer); + state.backendRetryTimer = null; + } + state.backendConnecting = false; }, setDaemonSocket(state, daemonSocket) { state.daemonSocket = daemonSocket; @@ -122,6 +135,12 @@ const store = createStore({ } state.daemonSocket = null; state.daemonConnectionStatus = 'disconnected'; + // Cleanup retry timer + if (state.daemonRetryTimer) { + clearTimeout(state.daemonRetryTimer); + state.daemonRetryTimer = null; + } + state.daemonConnecting = false; }, }, actions: { @@ -142,74 +161,97 @@ const store = createStore({ commit('dologout'); router.push('/'); }, - initializeSocket({ commit, state }) { - if (state.isLoggedIn && state.user) { - 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) { + initializeSocket({ commit, state, dispatch }) { + if (!state.isLoggedIn || !state.user || state.backendConnecting) { 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 let daemonUrl = import.meta.env.VITE_DAEMON_SOCKET; @@ -224,8 +266,16 @@ const store = createStore({ daemonUrl = 'wss://www.your-part.de:4551'; } - 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 const protocols = ['yourpart-protocol', undefined]; let attemptIndex = 0; @@ -242,7 +292,8 @@ const store = createStore({ daemonSocket.onopen = () => { 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'); const payload = JSON.stringify({ event: 'setUserId', @@ -259,7 +310,7 @@ const store = createStore({ tryConnectWithProtocol(); return; } - retryConnection(connectDaemonSocket); + dispatch('retryDaemonConnection'); }; daemonSocket.onerror = (error) => { @@ -270,7 +321,7 @@ const store = createStore({ tryConnectWithProtocol(); return; } - retryConnection(connectDaemonSocket); + dispatch('retryDaemonConnection'); }; daemonSocket.addEventListener('message', (event) => { @@ -295,35 +346,45 @@ const store = createStore({ tryConnectWithProtocol(); return; } - retryConnection(connectDaemonSocket); + dispatch('retryDaemonConnection'); } }; 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(); }, + 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) { 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('initializeDaemonSocket'); } diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 30ddf35..8cf3f2e 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -162,6 +162,32 @@ @@ -328,6 +321,7 @@ export default { lastViolationSound: 0, lastMinimapDraw: 0, minimapDrawInterval: 120, + lastPassengerTimerUpdate: 0, // Throttling für Passagier-Timer-Updates radarImg: null, activeRadar: false, radarAtTopEdge: true, // legacy flag (nicht mehr genutzt) @@ -360,6 +354,12 @@ export default { ,loadingHighscore: false // Lade-Status für Highscore ,currentPlayerEntry: null // Eintrag des aktuellen Spielers ,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: { @@ -401,15 +401,22 @@ export default { this.loadTaxiImage(); this.loadHouseImage(); this.loadPassengerImages(); + this.loadCarImage(); this.loadMaps(); this.setupEventListeners(); await this.initializeMotorSound(); this.setupAudioUnlockHandlers(); 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() { + console.log('🚪 Component unmounting, cleaning up...'); this.cleanup(); - }, + }, methods: { // Ampelschaltung: sekündliche Phasen-Updates; pro Tile ein State updateTrafficLights() { @@ -738,19 +745,25 @@ export default { }, cleanup() { + console.log('🧹 Starting cleanup...'); + + // Game Loop stoppen if (this.gameLoop) { cancelAnimationFrame(this.gameLoop); 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) { clearTimeout(this.motorStopTimeout); this.motorStopTimeout = null; } - // Event-Listener von Document entfernen - document.removeEventListener('keydown', this.handleKeyDown); - document.removeEventListener('keyup', this.handleKeyUp); - // Cleanup aller Timeouts if (this.passengerGenerationTimeout) { clearTimeout(this.passengerGenerationTimeout); this.passengerGenerationTimeout = null; @@ -759,17 +772,116 @@ export default { clearTimeout(this.crashDialogTimeout); 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) { document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true }); document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true }); document.removeEventListener('keydown', this.audioUnlockHandler, { capture: true }); this.audioUnlockHandler = null; } - // Cleanup von Listen und Sets + + // Listen und Sets bereinigen this.waitingPassengersList = []; this.loadedPassengersList = []; 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() { @@ -1229,6 +1341,363 @@ export default { // 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) { const now = Date.now(); const age = now - passenger.createdAt; @@ -1354,7 +1823,7 @@ export default { update() { if (!this.gameRunning || this.isPaused) { - this.gameLoop = requestAnimationFrame(this.update); + // Game Loop komplett stoppen wenn pausiert - kein unnötiges requestAnimationFrame return; } @@ -1364,14 +1833,25 @@ export default { // Passagier-Generierung prüfen this.updatePassengerGeneration(); + // Autos-Generierung prüfen + this.updateCarGeneration(); + + // Autos aktualisieren + this.updateCars(); + // Abgelaufene Passagiere entfernen this.removeExpiredPassengers(); this.updateTaxi(); this.handlePassengerActions(); - // Timer für geladene Passagiere aktualisieren (nach Passagier-Aktionen) - this.updatePassengerTimers(); + // Timer für geladene Passagiere aktualisieren (nach Passagier-Aktionen) - gedrosselt + const nowTs = Date.now(); + if (nowTs - this.lastPassengerTimerUpdate >= 200) { // Alle 200ms statt jeden Frame + this.updatePassengerTimers(); + this.lastPassengerTimerUpdate = nowTs; + } + this.checkCollisions(); this.render(); @@ -1379,7 +1859,6 @@ export default { this.checkRadarMeasurement(); // Minimap zeichnen (gedrosselt) - const nowTs = Date.now(); if (nowTs - this.lastMinimapDraw >= this.minimapDrawInterval) { this.drawMinimap(); this.lastMinimapDraw = nowTs; @@ -1775,6 +2254,13 @@ export default { 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 @@ -2004,6 +2490,24 @@ export default { this.decrementVehicle(reason); this.taxi.speed = 0; 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; }, @@ -2055,11 +2559,31 @@ export default { 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 this.taxi.speed = 0; this.taxi.angle = 0; 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 this.$nextTick(() => { if (this.canvas) { @@ -2511,10 +3035,24 @@ export default { }, checkCollision(rect1, rect2) { - return rect1.x < rect2.x + rect2.width && - rect1.x + rect1.width > rect2.x && - rect1.y < rect2.y + rect2.height && - rect1.y + rect1.height > rect2.y; + // Optionaler Puffer, um die Kollision weniger sensibel zu machen (sichtbarer Kontakt nötig) + // Für Car-vs-Taxi prüfen wir einen negativen Puffer (verkleinert Hitboxen) + const pad = (rect1.isCar || rect2.isCar) ? 4 : 0; // 4px Puffer pro Seite bei Autos (präziser Crash) + + 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() { @@ -2541,6 +3079,8 @@ export default { // Passagiere/Ziele werden aktuell nicht gezeichnet + // Zeichne Autos + this.drawCars(); // Zeichne Taxi this.ctx.save(); @@ -2566,6 +3106,39 @@ export default { 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() { const tileSize = this.tiles.size; // 400x400px @@ -2911,7 +3484,15 @@ export default { }, 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 this.ensureAudioUnlockedInEvent(); @@ -2962,7 +3543,13 @@ export default { }, 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() { @@ -3097,6 +3684,17 @@ export default { img.src = `/images/taxi/passenger${i}.png`; } }, + + 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() { try { @@ -3501,6 +4099,13 @@ export default { // Spiel pausieren wenn Highscore angezeigt wird if (!this.isPaused) { this.isPaused = true; + + // Game Loop stoppen + if (this.gameLoop) { + cancelAnimationFrame(this.gameLoop); + this.gameLoop = null; + } + // Motorgeräusch stoppen wenn pausiert if (this.motorSound && this.motorSound.isPlaying) { this.motorSound.stop(); @@ -3512,6 +4117,12 @@ export default { // Highscore geschlossen - Spiel automatisch fortsetzen this.isPaused = 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 } }, @@ -3727,7 +4338,7 @@ export default { .loaded-passengers-header { background: #4caf50; border-bottom: 1px solid #4caf50; - padding: 15px 20px; + padding: 5px 20px; } .loaded-passengers-title { @@ -3738,7 +4349,7 @@ export default { } .loaded-passengers-content { - padding: 15px; + padding: 5px 20px; max-height: 200px; overflow-y: auto; } @@ -3842,7 +4453,7 @@ export default { .waiting-passengers-header { background: #f8f9fa; border-bottom: 1px solid #ddd; - padding: 15px 20px; + padding: 5px 20px; } .waiting-passengers-title { @@ -3853,7 +4464,7 @@ export default { } .waiting-passengers-list { - padding: 20px; + padding: 5px 20px; max-height: 300px; overflow-y: auto; } @@ -3862,7 +4473,7 @@ export default { text-align: center; color: #666; font-style: italic; - padding: 20px 0; + padding: 5px 0; } .passenger-item { @@ -3928,7 +4539,7 @@ export default { .minimap-header { background: #f8f9fa; border-bottom: 1px solid #ddd; - padding: 15px 20px; + padding: 5px 20px; display: flex; justify-content: space-between; align-items: center;