Ä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:
@@ -162,6 +162,32 @@
|
||||
|
||||
<!-- Sidebar (rechts) -->
|
||||
<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 -->
|
||||
<div class="loaded-passengers-card">
|
||||
<div class="loaded-passengers-header">
|
||||
@@ -208,49 +234,16 @@
|
||||
<div v-if="waitingPassengersList.length === 0" class="no-passengers">
|
||||
Keine wartenden Passagiere
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(passenger, index) in waitingPassengersList"
|
||||
:key="index"
|
||||
class="passenger-item"
|
||||
>
|
||||
<div class="passenger-info">
|
||||
<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>
|
||||
<table v-else>
|
||||
<tr v-for="(passenger, index) in waitingPassengersList"
|
||||
:key="index" >
|
||||
<td><b>{{ passenger.name }}</b><span v-if="getPassengerTimeLeft(passenger) <= 5" class="passenger-timer"> ({{ getPassengerTimeLeft(passenger) }}s)</span></td>
|
||||
<td>{{ passenger.location }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user