Ä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:
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user