Änderung: Erweiterung der Passagierverwaltung und Benutzeroberfläche im Taxi-Spiel

Änderungen:
- Hinzufügung einer neuen Anzeige für geladene Passagiere mit Informationen zu Namen, Ziel, Bonus und verbleibender Zeit.
- Implementierung von Logik zur Aktualisierung der Timer für geladene Passagiere und zur Berechnung von Bonus und Zeit basierend auf dem kürzesten Weg.
- Verbesserung der Interaktivität durch automatische Einladungen von wartenden Passagieren und Absetzen geladener Passagiere.

Diese Anpassungen erhöhen die Benutzererfahrung und die Spielmechanik, indem sie eine detaillierte Verwaltung und Anzeige von Passagieren ermöglichen.
This commit is contained in:
Torsten Schulz (local)
2025-09-21 01:53:07 +02:00
parent 54c7e134d1
commit 1244c87d45

View File

@@ -122,6 +122,43 @@
<!-- Sidebar (rechts) -->
<div class="sidebar-section">
<!-- Geladene Passagiere -->
<div class="loaded-passengers-card">
<div class="loaded-passengers-header">
<h3 class="loaded-passengers-title">Geladene Passagiere</h3>
</div>
<div class="loaded-passengers-content">
<div v-if="loadedPassengersList.length === 0" class="no-passengers">
Keine Passagiere im Taxi
</div>
<table v-else class="passengers-table">
<thead>
<tr>
<th>Name</th>
<th>Ziel</th>
<th>Bonus</th>
<th>Zeit</th>
</tr>
</thead>
<tbody>
<tr
v-for="(passenger, index) in loadedPassengersList"
:key="index"
class="passenger-row"
:class="{ 'time-warning': passenger.timeLeft <= 10, 'time-critical': passenger.timeLeft <= 5 }"
>
<td class="passenger-name-cell">{{ passenger.name }}</td>
<td class="passenger-destination-cell">{{ passenger.destination.location }}</td>
<td class="passenger-bonus-cell">{{ passenger.bonusData ? passenger.bonusData.bonus : 0 }}</td>
<td class="passenger-time-cell" :class="{ 'time-warning': passenger.timeLeft <= 10, 'time-critical': passenger.timeLeft <= 5 }">
{{ passenger.timeLeft }}s
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Wartende Passagiere -->
<div class="waiting-passengers-card">
<div class="waiting-passengers-header">
@@ -201,9 +238,12 @@ export default {
money: 0,
passengersDelivered: 0,
waitingPassengersList: [],
loadedPassengersList: [], // Aktuell geladene Passagiere im Taxi
occupiedHouses: new Set(), // Verhindert doppelte Belegung von Häusern
lastPassengerGeneration: 0,
passengerGenerationInterval: 0,
bonusMultiplier: 15, // Bonus pro Tile
timePerTile: 8, // Sekunden pro Tile
fuel: 100,
currentLevel: 1,
gameRunning: false,
@@ -686,15 +726,30 @@ export default {
},
initializePassengerGeneration() {
// Setze zufälliges Intervall für erste Passagier-Generierung (10-35 Sekunden)
// Setze zufälliges Intervall für nächste Passagier-Generierung (10-35 Sekunden)
this.passengerGenerationInterval = Math.floor(Math.random() * 25000) + 10000; // 10-35 Sekunden
this.lastPassengerGeneration = Date.now();
this.occupiedHouses.clear();
// Passagier-Generierung initialisiert
// Generiere sofort einen Passagier, falls die Map bereits geladen ist
if (this.currentMap && Array.isArray(this.currentMap.tileHouses) && this.currentMap.tileHouses.length > 0) {
this.generateWaitingPassenger();
} else {
// Falls die Map noch nicht geladen ist, versuche es in 2 Sekunden erneut
setTimeout(() => {
this.generateWaitingPassenger();
}, 2000);
}
},
generateWaitingPassenger() {
if (!this.currentMap || !Array.isArray(this.currentMap.tileHouses)) return;
if (!this.currentMap || !Array.isArray(this.currentMap.tileHouses)) {
// Versuche es in 2 Sekunden erneut
setTimeout(() => {
this.generateWaitingPassenger();
}, 2000);
return;
}
// Finde alle verfügbaren Häuser auf der Karte
const availableHouses = [];
@@ -786,6 +841,82 @@ export default {
return Math.floor(Math.random() * 6) + 1;
},
calculateShortestPath(startX, startY, endX, endY) {
// Berechne Manhattan-Distanz (kürzester Weg in einem Raster)
const distance = Math.abs(endX - startX) + Math.abs(endY - startY);
return distance;
},
calculateBonusAndTime(startX, startY, endX, endY) {
const shortestPath = this.calculateShortestPath(startX, startY, endX, endY);
const bonus = shortestPath * this.bonusMultiplier;
const maxTime = shortestPath * this.timePerTile;
return {
shortestPath,
bonus,
maxTime
};
},
generatePassengerDestination() {
// Generiere ein zufälliges Ziel für den Passagier
if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets) || !Array.isArray(this.currentMap.tileHouses)) return null;
// Finde alle Straßen-Tiles auf der Karte
const streets = this.currentMap.tileStreets || [];
const houses = this.currentMap.tileHouses || [];
// Versuche maximal 10 Mal, ein gültiges Ziel zu finden
for (let attempt = 0; attempt < 10; attempt++) {
// Wähle zufällige Straße
const randomStreet = streets[Math.floor(Math.random() * streets.length)];
// Prüfe ob diese Straße Häuser hat
const housesOnThisTile = houses.filter(house => house.x === randomStreet.x && house.y === randomStreet.y);
if (housesOnThisTile.length > 0) {
// Wähle zufälliges Haus auf diesem Tile
const selectedHouse = housesOnThisTile[Math.floor(Math.random() * housesOnThisTile.length)];
// Bestimme die Straße basierend auf der Haus-Ecke
let streetName = "Unbekannte Straße";
let houseNumber = 1;
const corner = selectedHouse.corner;
if (corner === 'lo' || corner === 'ro') {
// Horizontale Straße
if (randomStreet.streetNameH && randomStreet.streetNameH.name) {
streetName = randomStreet.streetNameH.name;
}
} else if (corner === 'lu' || corner === 'ru') {
// Vertikale Straße
if (randomStreet.streetNameV && randomStreet.streetNameV.name) {
streetName = randomStreet.streetNameV.name;
}
}
// Finde die Hausnummer für diese Straße
const houseNumbers = this.getHouseNumbersForStreet(streetName);
if (houseNumbers.length > 0) {
houseNumber = houseNumbers[Math.floor(Math.random() * houseNumbers.length)];
}
return {
streetName,
houseNumber,
location: `${streetName} ${houseNumber}`,
tileX: randomStreet.x,
tileY: randomStreet.y,
houseCorner: selectedHouse.corner
};
}
}
// Wenn nach 10 Versuchen kein Ziel gefunden wurde
return null;
},
updatePassengerGeneration() {
const now = Date.now();
const timeSinceLastGeneration = now - this.lastPassengerGeneration;
@@ -798,6 +929,38 @@ export default {
}
},
updatePassengerTimers() {
// Aktualisiere die verbleibende Zeit für alle geladenen Passagiere
const now = Date.now();
this.loadedPassengersList.forEach(passenger => {
if (passenger.pickedUpAt && passenger.bonusData) {
// Aktiviere Timer nach 2 Sekunden
if (!passenger.timerActive) {
const timeSincePickup = now - passenger.pickedUpAt;
if (timeSincePickup >= 2000) {
passenger.timerActive = true;
passenger.lastUpdateTime = now;
console.log('Timer für Passagier', passenger.name, 'aktiviert nach', timeSincePickup, 'ms');
}
return; // Timer noch nicht aktiv
}
// Throttle: Nur alle 100ms aktualisieren
if (passenger.lastUpdateTime && (now - passenger.lastUpdateTime) < 100) {
return;
}
const timeSincePickup = now - passenger.pickedUpAt;
const elapsedTime = Math.floor(timeSincePickup / 1000);
// Berechne die verbleibende Zeit, aber lasse sie nicht unter 0 fallen
const remainingTime = passenger.bonusData.maxTime - elapsedTime;
passenger.timeLeft = Math.max(0, remainingTime);
passenger.lastUpdateTime = now;
}
});
},
removePassengerFromWaitingList() {
if (this.waitingPassengersList.length > 0) {
// Entferne den ersten Passagier aus der Liste
@@ -963,6 +1126,9 @@ export default {
this.updateTaxi();
this.handlePassengerActions();
// Timer für geladene Passagiere aktualisieren (nach Passagier-Aktionen)
this.updatePassengerTimers();
this.checkCollisions();
this.render();
@@ -1049,7 +1215,14 @@ export default {
},
handlePassengerActions() {
// S - Passagier aufnehmen
// WICHTIG: Zuerst absetzen, dann einladen!
// Automatisches Absetzen von geladenen Passagieren
this.checkForPassengerDropoff();
// Automatisches Einladen von wartenden Passagieren
this.checkForWaitingPassengers();
// S - Passagier aufnehmen (manuell)
if (this.keys['s'] || this.keys['S']) {
this.pickupPassenger();
}
@@ -1064,6 +1237,197 @@ export default {
this.refuel();
}
},
checkForWaitingPassengers() {
// Nur einladen wenn das Taxi steht (Geschwindigkeit = 0)
if (this.taxi.speed > 0) return;
// Prüfe alle wartenden Passagiere
for (let i = this.waitingPassengersList.length - 1; i >= 0; i--) {
const passenger = this.waitingPassengersList[i];
// Berechne die Position des Hauses auf dem aktuellen Tile
const housePosition = this.getHousePositionOnTile(passenger.tileX, passenger.tileY, passenger.houseCorner);
if (!housePosition) {
continue;
}
// Prüfe ob das Taxi in der Nähe des Hauses ist
const isNear = this.isTaxiNearHouse(housePosition, passenger.houseCorner);
if (isNear) {
// Lade Passagier ein
this.pickupWaitingPassenger(passenger, i);
break; // Nur einen Passagier pro Frame einladen
}
}
},
getHousePositionOnTile(tileX, tileY, corner) {
// Berechne die Position des Hauses basierend auf der Ecke
// Diese Methode muss exakt mit drawHousesOnMainCanvas übereinstimmen
const tileSize = 500; // Tile-Größe
const HOUSE_W = 150; // Hausbreite
const HOUSE_H = 150; // Haushöhe
const pad = 30; // Abstand vom Rand
let houseX, houseY;
switch (corner) {
case 'lo': // links-outer
houseX = pad;
houseY = pad;
break;
case 'ro': // rechts-outer
houseX = tileSize - HOUSE_W - pad;
houseY = pad;
break;
case 'lu': // links-upper
houseX = pad;
houseY = tileSize - HOUSE_H - pad;
break;
case 'ru': // rechts-upper
houseX = tileSize - HOUSE_W - pad;
houseY = tileSize - HOUSE_H - pad;
break;
default:
return null;
}
return { x: houseX, y: houseY, corner };
},
isTaxiNearHouse(housePosition, corner) {
const taxiX = this.taxi.x;
const taxiY = this.taxi.y;
const houseX = housePosition.x;
const houseY = housePosition.y;
const HOUSE_W = 150; // Hausbreite
const HOUSE_H = 150; // Haushöhe
const maxDistance = 100; // 100px Entfernung
// Einfache Distanzberechnung zum Hauszentrum
const houseCenterX = houseX + HOUSE_W / 2;
const houseCenterY = houseY + HOUSE_H / 2;
const distance = Math.sqrt(
Math.pow(taxiX - houseCenterX, 2) + Math.pow(taxiY - houseCenterY, 2)
);
return distance <= maxDistance;
},
pickupWaitingPassenger(passenger, index) {
console.log('Passagier wird eingeladen:', passenger.name);
// Entferne Passagier aus der Warteliste
this.waitingPassengersList.splice(index, 1);
// Gib das Haus wieder frei
if (passenger.houseId) {
this.occupiedHouses.delete(passenger.houseId);
}
// Generiere ein Ziel für den Passagier
const destination = this.generatePassengerDestination();
console.log('Generiertes Ziel:', destination);
if (destination) {
// Berechne Bonus und Zeit basierend auf kürzestem Weg
const startX = passenger.tileX;
const startY = passenger.tileY;
const endX = destination.tileX;
const endY = destination.tileY;
const bonusData = this.calculateBonusAndTime(startX, startY, endX, endY);
// Füge Passagier zur geladenen Liste hinzu
const now = Date.now();
this.loadedPassengersList.push({
...passenger,
destination: destination,
pickedUpAt: now,
bonusData: bonusData,
timeLeft: bonusData.maxTime,
lastUpdateTime: 0, // Timer-Aktualisierung erst nach expliziter Aktivierung
timerActive: false // Flag um Timer-Aktivierung zu kontrollieren
});
console.log('Passagier zur geladenen Liste hinzugefügt. Aktuelle Anzahl:', this.loadedPassengersList.length);
console.log('Bonus-Daten:', bonusData);
} else {
console.log('Kein Ziel generiert - Passagier wird nicht hinzugefügt');
}
// Optional: Sound-Effekt oder Nachricht
// this.playPickupSound();
},
checkForPassengerDropoff() {
// Nur absetzen wenn das Taxi steht (Geschwindigkeit = 0)
if (this.taxi.speed > 0) return;
// Prüfe ob geladene Passagiere abgesetzt werden können
if (this.loadedPassengersList.length === 0) return;
console.log('Prüfe Dropoff für', this.loadedPassengersList.length, 'geladene Passagiere');
console.log('Aktuelle Tile-Position:', { col: this.currentTile.col, row: this.currentTile.row });
// Prüfe alle geladene Passagiere
for (let i = this.loadedPassengersList.length - 1; i >= 0; i--) {
const passenger = this.loadedPassengersList[i];
const destination = passenger.destination;
console.log('Prüfe Passagier:', passenger.name, 'Ziel:', destination.location, 'Tile:', { x: destination.tileX, y: destination.tileY });
// Prüfe ob das Taxi am Zielort ist
if (this.currentTile.col === destination.tileX && this.currentTile.row === destination.tileY) {
console.log('Taxi ist am Zielort');
// Berechne die Position des Zielhauses auf dem aktuellen Tile
const housePosition = this.getHousePositionOnTile(destination.tileX, destination.tileY, destination.houseCorner);
if (housePosition) {
console.log('Haus-Position gefunden:', housePosition);
// Prüfe ob das Taxi in der Nähe des Zielhauses ist
const isNear = this.isTaxiNearHouse(housePosition, destination.houseCorner);
console.log('Ist Taxi nah genug?', isNear);
if (isNear) {
// Setze Passagier ab
console.log('Setze Passagier ab:', passenger.name);
this.dropoffLoadedPassenger(passenger, i);
break; // Nur einen Passagier pro Frame absetzen
}
} else {
console.log('Keine Haus-Position gefunden');
}
} else {
console.log('Taxi ist nicht am Zielort');
}
}
},
dropoffLoadedPassenger(passenger, index) {
// Entferne Passagier aus der geladenen Liste
this.loadedPassengersList.splice(index, 1);
// Berechne Bonus basierend auf verbleibender Zeit
let bonus = 0;
if (passenger.bonusData && passenger.timeLeft > 0) {
// Bonus wird nur vergeben, wenn noch Zeit übrig ist
bonus = passenger.bonusData.bonus;
}
// Belohne den Spieler
this.passengersDelivered++;
this.score += 50 + bonus;
this.money += 25 + Math.floor(bonus / 2);
console.log(`Passagier abgesetzt: ${passenger.name}, Bonus: ${bonus}, Zeit übrig: ${passenger.timeLeft}s`);
// Optional: Sound-Effekt oder Nachricht
// this.playDropoffSound();
},
pickupPassenger() {
// Finde nächsten Passagier in der Nähe
@@ -1154,13 +1518,20 @@ export default {
const vx = currCX - prevCX;
const vy = currCY - prevCY;
// WICHTIG: Nur prüfen wenn das Taxi tatsächlich bewegt wurde (mindestens 1 Pixel)
const movementThreshold = 1;
if (Math.abs(vx) < movementThreshold && Math.abs(vy) < movementThreshold) {
return; // Keine Bewegung, keine Verletzung möglich
}
// Top-Band: von oben nach unten (prev oben, curr unten), Eintritt über obere Bandkante y0
if (rects.top && approaches.top) {
const x0 = rects.top.x, x1 = rects.top.x + rects.top.width;
const y0 = rects.top.y, y1 = rects.top.y + rects.top.height;
const inX = (prevCX >= x0 && prevCX <= x1) || (currCX >= x0 && currCX <= x1);
if (vy > 0 && inX && prevCY < y0 && currCY >= y0) {
if (isVerRed) violated = true;
// Nur verletzen wenn das Taxi tatsächlich die Linie überquert (von außerhalb zu innerhalb)
if (vy > 0 && inX && prevCY < y0 && currCY >= y0 && isVerRed) {
violated = true;
}
}
// Bottom-Band: von unten nach oben (prev unten, curr oben), Eintritt über untere Bandkante y1
@@ -1168,8 +1539,9 @@ export default {
const x0 = rects.bottom.x, x1 = rects.bottom.x + rects.bottom.width;
const y0 = rects.bottom.y, y1 = rects.bottom.y + rects.bottom.height;
const inX = (prevCX >= x0 && prevCX <= x1) || (currCX >= x0 && currCX <= x1);
if (vy < 0 && inX && prevCY > y1 && currCY <= y1) {
if (isVerRed) violated = true;
// Nur verletzen wenn das Taxi tatsächlich die Linie überquert (von außerhalb zu innerhalb)
if (vy < 0 && inX && prevCY > y1 && currCY <= y1 && isVerRed) {
violated = true;
}
}
// Left-Band: von links nach rechts (prev links, curr rechts), Eintritt über linke Bandkante x0
@@ -1177,8 +1549,9 @@ export default {
const x0 = rects.left.x, x1 = rects.left.x + rects.left.width;
const y0 = rects.left.y, y1 = rects.left.y + rects.left.height;
const inY = (prevCY >= y0 && prevCY <= y1) || (currCY >= y0 && currCY <= y1);
if (vx > 0 && inY && prevCX < x0 && currCX >= x0) {
if (isHorRed) violated = true;
// Nur verletzen wenn das Taxi tatsächlich die Linie überquert (von außerhalb zu innerhalb)
if (vx > 0 && inY && prevCX < x0 && currCX >= x0 && isHorRed) {
violated = true;
}
}
// Right-Band: von rechts nach links (prev rechts, curr links), Eintritt über rechte Bandkante x1
@@ -1186,8 +1559,9 @@ export default {
const x0 = rects.right.x, x1 = rects.right.x + rects.right.width;
const y0 = rects.right.y, y1 = rects.right.y + rects.right.height;
const inY = (prevCY >= y0 && prevCY <= y1) || (currCY >= y0 && currCY <= y1);
if (vx < 0 && inY && prevCX > x1 && currCX <= x1) {
if (isHorRed) violated = true;
// Nur verletzen wenn das Taxi tatsächlich die Linie überquert (von außerhalb zu innerhalb)
if (vx < 0 && inY && prevCX > x1 && currCX <= x1 && isHorRed) {
violated = true;
}
}
@@ -1319,8 +1693,10 @@ export default {
this.motorSound.stop();
}
// Taxi sofort zurücksetzen
this.resetTaxiPosition();
// Taxi mittig im aktuellen Tile platzieren
this.taxi.speed = 0;
this.taxi.angle = 0;
this.centerTaxiInCurrentTile();
// Dialog über globale MessageDialog öffnen
this.$nextTick(() => {
@@ -1356,8 +1732,10 @@ export default {
console.log('Nach Dialog-Close - isPaused:', this.isPaused, 'showPauseOverlay:', this.showPauseOverlay);
// Taxi-Position zurücksetzen
this.resetTaxiPosition();
// Taxi bleibt auf dem aktuellen Tile, mittig platzieren
this.taxi.speed = 0;
this.taxi.angle = 0;
this.centerTaxiInCurrentTile();
// Fokus zurück auf Canvas setzen
this.$nextTick(() => {
@@ -1399,6 +1777,14 @@ export default {
this.currentTile.col = 0;
},
centerTaxiInCurrentTile() {
// Taxi mittig im aktuellen Tile platzieren (ohne Tile-Position zu ändern)
this.taxi.x = 250 - this.taxi.width/2; // Mitte des Canvas (500px / 2)
this.taxi.y = 250 - this.taxi.height/2; // Mitte des Canvas (500px / 2)
this.taxi.speed = 0;
this.taxi.angle = 0;
},
isTaxiOnRoad() {
// Prüfe ob das Taxi innerhalb der Canvas-Grenzen ist
if (this.taxi.x < 0 || this.taxi.x >= this.canvas.width ||
@@ -2036,6 +2422,7 @@ export default {
this.money = 0;
this.passengersDelivered = 0;
this.waitingPassengersList = [];
this.loadedPassengersList = [];
this.occupiedHouses.clear();
this.fuel = 100;
this.taxi.x = 250;
@@ -2653,6 +3040,121 @@ export default {
top: 20px;
}
/* Loaded Passengers Card */
.loaded-passengers-card {
background: #e8f5e8;
border: 1px solid #4caf50;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 20px;
}
.loaded-passengers-header {
background: #4caf50;
border-bottom: 1px solid #4caf50;
padding: 15px 20px;
}
.loaded-passengers-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #fff;
}
.loaded-passengers-content {
padding: 15px;
max-height: 200px;
overflow-y: auto;
}
.passengers-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.passengers-table th {
background: #4caf50;
color: white;
padding: 8px 6px;
text-align: left;
font-weight: 600;
font-size: 0.8rem;
}
.passengers-table th:first-child {
width: 25%;
}
.passengers-table th:nth-child(2) {
width: 35%;
}
.passengers-table th:nth-child(3) {
width: 20%;
}
.passengers-table th:last-child {
width: 20%;
}
.passenger-row {
border-bottom: 1px solid #e0e0e0;
}
.passenger-row:last-child {
border-bottom: none;
}
.passenger-row:hover {
background: #f5f5f5;
}
.passenger-name-cell {
padding: 6px;
font-weight: 500;
color: #333;
}
.passenger-destination-cell {
padding: 6px;
color: #2e7d32;
font-size: 0.8rem;
}
.passenger-bonus-cell {
padding: 6px;
color: #ff9800;
font-weight: 600;
text-align: center;
font-size: 0.8rem;
}
.passenger-time-cell {
padding: 6px;
text-align: center;
font-weight: 600;
font-size: 0.8rem;
}
.time-warning {
background: #fff3cd !important;
color: #856404 !important;
}
.time-critical {
background: #f8d7da !important;
color: #721c24 !important;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
}
/* Waiting Passengers Card */
.waiting-passengers-card {
background: #fff;
@@ -2698,6 +3200,18 @@ export default {
border-bottom: none;
}
.passenger-item.loaded {
background: #f0f8f0;
padding: 12px;
border-radius: 6px;
border: 1px solid #4caf50;
margin-bottom: 8px;
}
.passenger-item.loaded:last-child {
margin-bottom: 0;
}
.passenger-info {
display: flex;
flex-direction: column;
@@ -2715,6 +3229,12 @@ export default {
color: #666;
}
.passenger-destination {
font-size: 0.85rem;
color: #2e7d32;
font-weight: 500;
}
.passenger-timer {
font-size: 0.8rem;
color: #ff5722;