Änderungen: - Implementierung der Autoplay-Policy zur sofortigen Wiederaufnahme des AudioContext bei Benutzerinteraktionen. - Anpassung der Motorgeräuschparameter basierend auf der Geschwindigkeit des Taxis für eine realistischere Klangdarstellung. - Sicherstellung, dass der AudioContext aktiv ist, bevor Motorgeräusche gestartet werden. - Entfernung der Passagier- und Zielgenerierung sowie deren Zeichnung, um den Fokus auf die Audio-Integration zu legen. Diese Anpassungen verbessern die akustische Benutzererfahrung und optimieren die Audioverwaltung im Taxi-Minispiel.
1721 lines
51 KiB
Vue
1721 lines
51 KiB
Vue
<template>
|
||
<div class="contenthidden">
|
||
<StatusBar />
|
||
<div class="contentscroll">
|
||
<!-- Spiel-Titel -->
|
||
<div class="game-title">
|
||
<h1>{{ $t('minigames.taxi.title') }}</h1>
|
||
<p>{{ $t('minigames.taxi.description') }}</p>
|
||
</div>
|
||
|
||
<!-- Spiel-Layout -->
|
||
<div class="game-layout">
|
||
<!-- Spielbrett (links) -->
|
||
<div class="game-board-section">
|
||
<!-- Spielbereich mit Canvas und Legende -->
|
||
<div class="game-area">
|
||
<!-- Legende (links) -->
|
||
<div class="controls-legend">
|
||
<h3>Steuerung</h3>
|
||
<div class="legend-grid">
|
||
<div class="legend-item">
|
||
<span class="legend-key">↑ W</span>
|
||
<span class="legend-text">Gas geben</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-key">↓ X</span>
|
||
<span class="legend-text">Bremsen</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-key">→ D</span>
|
||
<span class="legend-text">Rechts lenken</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-key">← A</span>
|
||
<span class="legend-text">Links lenken</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ziele -->
|
||
<div class="game-objectives">
|
||
<h4>Ziele</h4>
|
||
<ul>
|
||
<li>Vermeide Kollisionen mit anderen Fahrzeugen</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spiel-Canvas mit Tacho -->
|
||
<div class="game-canvas-section">
|
||
<!-- Tacho-Display -->
|
||
<div class="tacho-display">
|
||
<div class="tacho-speed">
|
||
<span class="tacho-icon">⏱️</span>
|
||
<span class="speed-value">{{ taxi.speed * 5 }}</span>
|
||
<span class="speed-unit">km/h</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pause-Anzeige -->
|
||
<div v-if="showPauseOverlay" class="pause-overlay">
|
||
<div class="pause-message">
|
||
<h2>{{ $t('minigames.taxi.paused') }}</h2>
|
||
<button @click="togglePause" class="resume-button">
|
||
{{ $t('minigames.taxi.resume') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Canvas -->
|
||
<div class="game-canvas-container">
|
||
<canvas
|
||
ref="gameCanvas"
|
||
width="500"
|
||
height="500"
|
||
class="game-canvas"
|
||
@click="handleCanvasClick"
|
||
@keydown="handleKeyDown"
|
||
tabindex="0"
|
||
></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spiel-Controls -->
|
||
<div class="game-controls">
|
||
<button @click="togglePause" class="control-button">
|
||
{{ isPaused ? $t('minigames.taxi.resume') : $t('minigames.taxi.pause') }}
|
||
</button>
|
||
<button @click="restartLevel" class="control-button">
|
||
{{ $t('minigames.taxi.restartLevel') }}
|
||
</button>
|
||
<button @click="goBack" class="control-button">
|
||
{{ $t('minigames.backToGames') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar (rechts) -->
|
||
<div class="sidebar-section">
|
||
<!-- Statistiken -->
|
||
<div class="stats-card">
|
||
<div class="stats-header">
|
||
<div class="stats-header-content">
|
||
<h3 class="stats-title">{{ $t('minigames.taxi.gameStats') }}</h3>
|
||
<button class="toggle-button" @click="toggleStats">
|
||
<span class="toggle-icon">{{ toggleIcon }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="stats-list" v-if="isStatsExpanded">
|
||
<div class="stat-row">
|
||
<span class="stat-value score-value">{{ score }}</span>
|
||
<span class="stat-label">{{ $t('minigames.taxi.score') }}</span>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<span class="stat-value money-value">€{{ money }}</span>
|
||
<span class="stat-label">{{ $t('minigames.taxi.money') }}</span>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<span class="stat-value passengers-value">{{ passengersDelivered }}</span>
|
||
<span class="stat-label">{{ $t('minigames.taxi.passengers') }}</span>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<span class="stat-value crash-value">{{ crashes }}</span>
|
||
<span class="stat-label">Unfälle</span>
|
||
</div>
|
||
|
||
<div class="stat-row">
|
||
<span class="stat-value fuel-value">{{ fuel }}%</span>
|
||
<span class="stat-label">{{ $t('minigames.taxi.fuel') }}</span>
|
||
</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>
|
||
</template>
|
||
|
||
<script>
|
||
import streetCoordinates from '../../utils/streetCoordinates.js';
|
||
import { MotorSound, NoiseGenerator } from '../../assets/motorSound.js';
|
||
import apiClient from '../../utils/axios.js';
|
||
import StatusBar from '../../components/falukant/StatusBar.vue';
|
||
|
||
export default {
|
||
name: 'TaxiGame',
|
||
components: {
|
||
StatusBar
|
||
},
|
||
data() {
|
||
return {
|
||
isPaused: false,
|
||
showPauseOverlay: false,
|
||
score: 0,
|
||
money: 0,
|
||
passengersDelivered: 0,
|
||
fuel: 100,
|
||
currentLevel: 1,
|
||
gameRunning: false,
|
||
crashes: 0,
|
||
gameLoop: null,
|
||
lastSpeedChange: 0,
|
||
speedChangeInterval: 100, // 200ms = 0.2 Sekunden
|
||
lastSteerChange: 0,
|
||
steeringBaseInterval: 300, // Basisintervall in ms bei sehr niedriger Geschwindigkeit
|
||
canvas: null,
|
||
ctx: null,
|
||
minimapCanvas: null,
|
||
minimapCtx: null,
|
||
isStatsExpanded: true,
|
||
taxi: {
|
||
x: 250, // Mitte des 400x400px Tiles
|
||
y: 250, // Mitte des 400x400px Tiles
|
||
width: 30, // Skalierte Breite (50px Höhe * 297/506)
|
||
height: 50, // 50px Höhe
|
||
angle: 0, // Fahrtrichtung
|
||
imageAngle: Math.PI / 2, // Bildrotation (90° nach rechts)
|
||
speed: 0,
|
||
maxSpeed: 24, // 24 = 120 km/h (5er-Schritte: 0, 5, 10, ..., 120)
|
||
image: null // Taxi-Bild
|
||
},
|
||
currentTile: {
|
||
row: 0,
|
||
col: 0
|
||
},
|
||
tiles: {
|
||
size: 500, // 400x400px pro Tile
|
||
images: {}
|
||
},
|
||
maps: [], // Geladene Maps aus der Datenbank
|
||
currentMap: null, // Aktuell verwendete Map
|
||
selectedMapId: null, // ID der ausgewählten Map
|
||
passengers: [],
|
||
destinations: [],
|
||
obstacles: [],
|
||
keys: {},
|
||
motorSound: null,
|
||
audioContext: null
|
||
}
|
||
},
|
||
async mounted() {
|
||
this.initializeGame();
|
||
this.initializeMinimap();
|
||
this.loadTiles();
|
||
this.loadTaxiImage();
|
||
this.loadMaps();
|
||
this.setupEventListeners();
|
||
await this.initializeMotorSound();
|
||
},
|
||
beforeUnmount() {
|
||
this.cleanup();
|
||
},
|
||
methods: {
|
||
// Mapping zwischen Spiel-Tile-Namen (lowercase) und streetCoordinates.json (camelCase)
|
||
mapTileTypeToStreetCoordinates(tileType) {
|
||
const mapping = {
|
||
'cornertopleft': 'cornerTopLeft',
|
||
'cornertopright': 'cornerTopRight',
|
||
'cornerbottomleft': 'cornerBottomLeft',
|
||
'cornerbottomright': 'cornerBottomRight',
|
||
'horizontal': 'horizontal',
|
||
'vertical': 'vertical',
|
||
'cross': 'cross',
|
||
'fuelhorizontal': 'fuelHorizontal',
|
||
'fuelvertical': 'fuelVertical'
|
||
};
|
||
return mapping[tileType] || tileType;
|
||
},
|
||
|
||
initializeGame() {
|
||
this.canvas = this.$refs.gameCanvas;
|
||
this.ctx = this.canvas.getContext('2d');
|
||
this.canvas.focus();
|
||
|
||
// Taxi in die Mitte des 400x400px Tiles positionieren
|
||
this.taxi.x = 250 - this.taxi.width/2; // Zentriert in der Mitte
|
||
this.taxi.y = 250 - this.taxi.height/2; // Zentriert in der Mitte
|
||
this.taxi.angle = 0; // Fahrtrichtung nach oben
|
||
this.taxi.speed = 0; // Geschwindigkeit zurücksetzen
|
||
|
||
// Aktuelle Tile-Position setzen
|
||
this.currentTile.row = 0;
|
||
this.currentTile.col = 0;
|
||
|
||
this.generateLevel();
|
||
this.startGame();
|
||
},
|
||
|
||
updateCanvasSize() {
|
||
// Canvas ist immer 400x400px für ein einzelnes Tile
|
||
this.canvas.width = 500;
|
||
this.canvas.height = 500;
|
||
},
|
||
|
||
updateCurrentTile() {
|
||
// Berechne aktuelle Tile-Position basierend auf Taxi-Position
|
||
if (this.currentMap && this.currentMap.mapData) {
|
||
const mapData = this.currentMap.mapData;
|
||
const rows = mapData.length;
|
||
const cols = mapData[0] ? mapData[0].length : 0;
|
||
|
||
// Da das Taxi nur in einem 400x400px Canvas ist, verwende das erste Tile der Map
|
||
// Das sollte das korrekte Tile sein, das in der Map definiert ist
|
||
this.currentTile.row = 0;
|
||
this.currentTile.col = 0;
|
||
|
||
|
||
// Stelle sicher, dass Taxi innerhalb des Canvas bleibt
|
||
this.taxi.x = Math.max(0, Math.min(this.canvas.width - this.taxi.width, this.taxi.x));
|
||
this.taxi.y = Math.max(0, Math.min(this.canvas.height - this.taxi.height, this.taxi.y));
|
||
}
|
||
},
|
||
|
||
setupEventListeners() {
|
||
document.addEventListener('keydown', this.handleKeyDown);
|
||
document.addEventListener('keyup', this.handleKeyUp);
|
||
},
|
||
|
||
async initializeMotorSound() {
|
||
// AudioContext wird erst bei erster Benutzerinteraktion erstellt
|
||
console.log('MotorSound wird bei erster Benutzerinteraktion initialisiert');
|
||
},
|
||
|
||
async initAudioOnUserInteraction() {
|
||
if (this.audioContext) return; // Bereits initialisiert
|
||
|
||
try {
|
||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
// Autoplay-Policy: Context sofort im User-Gesture fortsetzen
|
||
if (this.audioContext.state === 'suspended') {
|
||
await this.audioContext.resume();
|
||
}
|
||
const generator = new NoiseGenerator();
|
||
this.motorSound = new MotorSound(this.audioContext, generator);
|
||
await this.motorSound.init();
|
||
console.log('MotorSound initialisiert');
|
||
// Falls bereits Bewegung anliegt, Sound direkt starten
|
||
const speedKmh = this.taxi.speed * 5;
|
||
if (speedKmh > 0) {
|
||
this.motorSound.start();
|
||
// Sofort Parameter setzen
|
||
const speedFactor = Math.min(speedKmh / 120, 1);
|
||
const motorSpeed = 0.3 + (speedFactor * 0.3);
|
||
const volume = 0.1 + (speedFactor * 0.4);
|
||
this.motorSound.setSpeed(motorSpeed);
|
||
this.motorSound.setVolume(volume);
|
||
}
|
||
} catch (error) {
|
||
console.warn('MotorSound konnte nicht initialisiert werden:', error);
|
||
}
|
||
},
|
||
|
||
updateMotorSound() {
|
||
if (!this.motorSound) return;
|
||
|
||
const speedKmh = this.taxi.speed * 5; // Geschwindigkeit in km/h
|
||
const isMoving = speedKmh > 0;
|
||
|
||
// Sicherstellen, dass der AudioContext läuft
|
||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||
this.audioContext.resume().catch(() => {});
|
||
}
|
||
|
||
if (isMoving && !this.motorSound.isPlaying) {
|
||
this.motorSound.start();
|
||
} else if (!isMoving && this.motorSound.isPlaying) {
|
||
this.motorSound.stop();
|
||
}
|
||
|
||
if (isMoving) {
|
||
// Geschwindigkeitsabhängige Tonhöhe und Lautstärke
|
||
const speedFactor = Math.min(speedKmh / 120, 1); // 0-1 basierend auf 0-120 km/h
|
||
const motorSpeed = 0.3 + (speedFactor * 0.3); // 0.3 bis 1.5
|
||
const volume = 0.1 + (speedFactor * 0.4); // 0.1 bis 0.5
|
||
|
||
this.motorSound.setSpeed(motorSpeed);
|
||
this.motorSound.setVolume(volume);
|
||
}
|
||
},
|
||
|
||
cleanup() {
|
||
if (this.gameLoop) {
|
||
cancelAnimationFrame(this.gameLoop);
|
||
}
|
||
if (this.motorSound) {
|
||
this.motorSound.stop();
|
||
}
|
||
if (this.audioContext) {
|
||
this.audioContext.close();
|
||
}
|
||
document.removeEventListener('keydown', this.handleKeyDown);
|
||
document.removeEventListener('keyup', this.handleKeyUp);
|
||
},
|
||
|
||
generateLevel() {
|
||
this.passengers = [];
|
||
this.destinations = [];
|
||
this.obstacles = [];
|
||
|
||
// Passagiere/Ziele werden aktuell nicht generiert
|
||
|
||
|
||
// Generiere Hindernisse (außerhalb der Straßen)
|
||
for (let i = 0; i < 3; i++) {
|
||
const position = this.getRandomOffRoadPosition();
|
||
this.obstacles.push({
|
||
x: position.x,
|
||
y: position.y,
|
||
width: 20,
|
||
height: 20
|
||
});
|
||
}
|
||
},
|
||
|
||
getRandomRoadPosition() {
|
||
const tileSize = this.tiles.size;
|
||
const cols = 8;
|
||
const rows = 8;
|
||
|
||
let attempts = 0;
|
||
while (attempts < 100) {
|
||
const tileCol = Math.floor(Math.random() * cols);
|
||
const tileRow = Math.floor(Math.random() * rows);
|
||
const tileType = this.getTileType(tileRow, tileCol, rows, cols);
|
||
|
||
// Zufällige Position innerhalb des Tiles
|
||
const relativeX = Math.random();
|
||
const relativeY = Math.random();
|
||
|
||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||
if (streetCoordinates.isPointDriveable(relativeX, relativeY, streetTileType, 1)) {
|
||
return {
|
||
x: tileCol * tileSize + relativeX * tileSize,
|
||
y: tileRow * tileSize + relativeY * tileSize
|
||
};
|
||
}
|
||
attempts++;
|
||
}
|
||
|
||
// Fallback: Mitte des Spielfelds
|
||
return { x: 250, y: 250 };
|
||
},
|
||
|
||
getRandomOffRoadPosition() {
|
||
const tileSize = this.tiles.size;
|
||
const cols = 8;
|
||
const rows = 8;
|
||
|
||
let attempts = 0;
|
||
while (attempts < 100) {
|
||
const tileCol = Math.floor(Math.random() * cols);
|
||
const tileRow = Math.floor(Math.random() * rows);
|
||
const tileType = this.getTileType(tileRow, tileCol, rows, cols);
|
||
|
||
// Zufällige Position innerhalb des Tiles
|
||
const relativeX = Math.random();
|
||
const relativeY = Math.random();
|
||
|
||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||
if (!streetCoordinates.isPointDriveable(relativeX, relativeY, streetTileType, 1)) {
|
||
return {
|
||
x: tileCol * tileSize + relativeX * tileSize,
|
||
y: tileRow * tileSize + relativeY * tileSize
|
||
};
|
||
}
|
||
attempts++;
|
||
}
|
||
|
||
// Fallback: Ecke des Spielfelds
|
||
return { x: 20, y: 20 };
|
||
},
|
||
|
||
startGame() {
|
||
this.gameRunning = true;
|
||
this.gameLoop = requestAnimationFrame(this.update);
|
||
},
|
||
|
||
update() {
|
||
if (!this.gameRunning || this.isPaused) {
|
||
this.gameLoop = requestAnimationFrame(this.update);
|
||
return;
|
||
}
|
||
|
||
this.updateTaxi();
|
||
this.handlePassengerActions();
|
||
this.checkCollisions();
|
||
this.render();
|
||
|
||
// Minimap zeichnen
|
||
this.drawMinimap();
|
||
|
||
this.gameLoop = requestAnimationFrame(this.update);
|
||
},
|
||
|
||
updateTaxi() {
|
||
// Prüfe ob das Spiel pausiert ist
|
||
if (this.isPaused) {
|
||
return;
|
||
}
|
||
|
||
const currentTime = Date.now();
|
||
|
||
// Prüfe ob genug Zeit vergangen ist für Geschwindigkeitsänderung
|
||
if (currentTime - this.lastSpeedChange >= this.speedChangeInterval) {
|
||
// Beschleunigung bei gedrücktem Pfeil nach oben (5er-Schritte)
|
||
if (this.keys['ArrowUp'] || this.keys['w'] || this.keys['W']) {
|
||
this.taxi.speed = Math.min(this.taxi.speed + 1, this.taxi.maxSpeed);
|
||
this.lastSpeedChange = currentTime;
|
||
}
|
||
|
||
// Abbremsen bei gedrücktem Pfeil nach unten (5er-Schritte)
|
||
if (this.keys['ArrowDown'] || this.keys['x'] || this.keys['X']) {
|
||
this.taxi.speed = Math.max(this.taxi.speed - 1, 0); // Bremsen bis 0, nicht rückwärts
|
||
this.lastSpeedChange = currentTime;
|
||
}
|
||
}
|
||
|
||
// Lenkung in diskreten 11,25°-Schritten mit geschwindigkeitsabhängiger Verzögerung
|
||
const steeringAngle = Math.PI / 16; // 11,25°
|
||
const speedLevel = this.taxi.speed; // 0..24 (5 km/h je Stufe)
|
||
// Nicht-lineare Beschleunigung der Eingabe: schneller bei hoher Geschwindigkeit
|
||
const minInterval = 4; // schnellste Annahme (ms) - erneut halbiert
|
||
const maxInterval = 60; // langsamste Annahme (ms) - erneut halbiert
|
||
const factor = Math.pow(speedLevel / this.taxi.maxSpeed, 1.5); // 0..1, gewichtet
|
||
const steeringInterval = Math.max(minInterval, maxInterval - (maxInterval - minInterval) * factor);
|
||
|
||
// Richtung bestimmen (-1 links, +1 rechts)
|
||
let steerDir = 0;
|
||
if (this.keys['ArrowLeft'] || this.keys['a'] || this.keys['A']) steerDir = -1;
|
||
if (this.keys['ArrowRight'] || this.keys['d'] || this.keys['D']) steerDir = steerDir === -1 ? 0 : 1; // beide gedrückt -> neutral
|
||
|
||
if (steerDir !== 0) {
|
||
const elapsed = currentTime - this.lastSteerChange;
|
||
const steps = Math.floor(elapsed / steeringInterval);
|
||
if (steps > 0) {
|
||
this.taxi.angle += steerDir * steeringAngle * steps;
|
||
// Restzeit beibehalten, damit Timing gleichmäßig bleibt
|
||
this.lastSteerChange = currentTime - (elapsed % steeringInterval);
|
||
}
|
||
} else {
|
||
// Keine Lenkung gedrückt: Timer resetten, um Burst bei erstem Druck zu vermeiden
|
||
this.lastSteerChange = currentTime;
|
||
}
|
||
|
||
// Aktualisiere Position (1/4 der ursprünglichen Bewegung)
|
||
this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.1;
|
||
this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.1;
|
||
|
||
// Aktualisiere aktuelle Tile-Position
|
||
this.updateCurrentTile();
|
||
|
||
// Verbrauche Treibstoff
|
||
if (Math.abs(this.taxi.speed) > 0.1) {
|
||
this.fuel = Math.max(0, this.fuel - 0.01);
|
||
}
|
||
|
||
// Motorgeräusch aktualisieren
|
||
this.updateMotorSound();
|
||
},
|
||
|
||
handlePassengerActions() {
|
||
// S - Passagier aufnehmen
|
||
if (this.keys['s'] || this.keys['S']) {
|
||
this.pickupPassenger();
|
||
}
|
||
|
||
// Q - Passagier absetzen
|
||
if (this.keys['q'] || this.keys['Q']) {
|
||
this.dropoffPassenger();
|
||
}
|
||
|
||
// Enter - Tanken
|
||
if (this.keys['Enter']) {
|
||
this.refuel();
|
||
}
|
||
},
|
||
|
||
pickupPassenger() {
|
||
// Finde nächsten Passagier in der Nähe
|
||
for (let i = 0; i < this.passengers.length; i++) {
|
||
const passenger = this.passengers[i];
|
||
if (!passenger.pickedUp && this.checkCollision(this.taxi, passenger)) {
|
||
passenger.pickedUp = true;
|
||
this.score += 10;
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
dropoffPassenger() {
|
||
// Finde nächstes Ziel in der Nähe
|
||
for (let i = 0; i < this.destinations.length; i++) {
|
||
const destination = this.destinations[i];
|
||
if (!destination.completed && this.checkCollision(this.taxi, destination)) {
|
||
destination.completed = true;
|
||
this.passengersDelivered++;
|
||
this.score += 50;
|
||
this.money += 25;
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
refuel() {
|
||
// Finde nächste Tankstelle in der Nähe
|
||
for (let i = 0; i < this.gasStations.length; i++) {
|
||
const station = this.gasStations[i];
|
||
if (this.checkCollision(this.taxi, station)) {
|
||
this.fuel = Math.min(100, this.fuel + 50);
|
||
this.score += 5;
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
|
||
checkCollisions() {
|
||
// Prüfe Straßenkollisionen nur wenn das Spiel nicht pausiert ist
|
||
if (!this.isPaused && !this.isTaxiOnRoad()) {
|
||
this.handleCrash();
|
||
}
|
||
|
||
// Prüfe Hindernisse nur wenn das Spiel nicht pausiert ist
|
||
if (!this.isPaused) {
|
||
this.obstacles.forEach(obstacle => {
|
||
if (this.checkCollision(this.taxi, obstacle)) {
|
||
this.handleCrash();
|
||
}
|
||
});
|
||
}
|
||
},
|
||
|
||
handleCrash() {
|
||
// Verhindere mehrfache Crashes in kurzer Zeit
|
||
if (this.isPaused) {
|
||
console.log('Crash bereits erkannt, ignoriere weitere');
|
||
return;
|
||
}
|
||
|
||
console.log('🚨 CRASH ERKANNT!');
|
||
this.crashes++;
|
||
this.taxi.speed = 0;
|
||
this.isPaused = true; // Zuerst pausieren
|
||
|
||
// Motorgeräusch sofort stoppen
|
||
if (this.motorSound && this.motorSound.isPlaying) {
|
||
this.motorSound.stop();
|
||
}
|
||
|
||
// Taxi sofort zurücksetzen
|
||
this.resetTaxiPosition();
|
||
|
||
// Dialog über globale MessageDialog öffnen
|
||
this.$nextTick(() => {
|
||
// Temporär direkte Übersetzung verwenden, bis i18n-Problem gelöst ist
|
||
const crashMessage = `Du hattest einen Unfall! Crashes: ${this.crashes}`;
|
||
const crashTitle = 'Unfall!';
|
||
this.$root?.$refs?.messageDialog?.open?.(crashMessage, crashTitle, {}, this.handleCrashDialogClose);
|
||
|
||
// Test: Direkter Aufruf nach 3 Sekunden (falls Dialog-Callback nicht funktioniert)
|
||
setTimeout(() => {
|
||
console.log('Test: Automatischer Reset nach 3 Sekunden');
|
||
this.handleCrashDialogClose();
|
||
}, 3000);
|
||
console.log('Crash-Dialog wird angezeigt:', {
|
||
crashes: this.crashes,
|
||
isPaused: this.isPaused,
|
||
taxiSpeed: this.taxi.speed,
|
||
messageTest: this.$t('message.test'),
|
||
crashMessage: this.$t('minigames.taxi.crash.message'),
|
||
allKeys: Object.keys(this.$t('minigames')),
|
||
taxiKeys: Object.keys(this.$t('minigames.taxi')),
|
||
crashKeys: Object.keys(this.$t('minigames.taxi.crash'))
|
||
});
|
||
|
||
// Spiel bleibt pausiert bis Dialog geschlossen wird
|
||
});
|
||
},
|
||
|
||
handleCrashDialogClose() {
|
||
console.log('Crash-Dialog geschlossen, Spiel wird fortgesetzt');
|
||
this.isPaused = false;
|
||
this.showPauseOverlay = false;
|
||
|
||
console.log('Nach Dialog-Close - isPaused:', this.isPaused, 'showPauseOverlay:', this.showPauseOverlay);
|
||
|
||
// Taxi-Position zurücksetzen
|
||
this.resetTaxiPosition();
|
||
|
||
// Fokus zurück auf Canvas setzen
|
||
this.$nextTick(() => {
|
||
if (this.canvas) {
|
||
this.canvas.focus();
|
||
}
|
||
console.log('Nach nextTick - isPaused:', this.isPaused);
|
||
});
|
||
},
|
||
|
||
resetTaxiPosition() {
|
||
// Taxi in der Mitte des Screens platzieren
|
||
this.taxi.x = 250 - this.taxi.width/2;
|
||
this.taxi.y = 250 - this.taxi.height/2;
|
||
this.taxi.speed = 0;
|
||
this.taxi.angle = 0;
|
||
|
||
// Aktuelle Tile-Position zurücksetzen
|
||
this.currentTile.row = 0;
|
||
this.currentTile.col = 0;
|
||
},
|
||
|
||
isTaxiOnRoad() {
|
||
// Prüfe ob das Taxi innerhalb der Canvas-Grenzen ist
|
||
if (this.taxi.x < 0 || this.taxi.x >= this.canvas.width ||
|
||
this.taxi.y < 0 || this.taxi.y >= this.canvas.height) {
|
||
if (this.taxi.speed > 0) {
|
||
console.log('Taxi außerhalb Canvas-Grenzen:', { x: this.taxi.x, y: this.taxi.y });
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
// Hole das aktuelle Tile und dessen Polygone
|
||
const tileType = this.getCurrentTileType();
|
||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||
const originalTileSize = streetCoordinates.getOriginalTileSize(); // 640px
|
||
const currentTileSize = 500; // Aktuelle Render-Größe
|
||
|
||
// Hole die Polygon-Koordinaten für dieses Tile
|
||
const regions = streetCoordinates.getDriveableRegions(streetTileType, originalTileSize);
|
||
|
||
if (regions.length === 0) {
|
||
// Keine Polygone definiert = befahrbar
|
||
return true;
|
||
}
|
||
|
||
// Erstelle Taxi-Rechteck in relativen Koordinaten (0-1)
|
||
const taxiRect = {
|
||
x: this.taxi.x / currentTileSize,
|
||
y: this.taxi.y / currentTileSize,
|
||
width: this.taxi.width / currentTileSize,
|
||
height: this.taxi.height / currentTileSize
|
||
};
|
||
|
||
|
||
// Prüfe Kollision mit jedem Polygon
|
||
for (let i = 0; i < regions.length; i++) {
|
||
const region = regions[i];
|
||
|
||
if (this.rectPolygonCollision(taxiRect, region)) {
|
||
if (this.taxi.speed > 0) {
|
||
console.log(`🚨 KOLLISION mit Polygon ${i}!`);
|
||
}
|
||
return false; // Kollision = nicht befahrbar
|
||
}
|
||
}
|
||
|
||
return true; // Keine Kollision = befahrbar
|
||
},
|
||
|
||
// Prüft Kollision zwischen Rechteck und Polygon
|
||
rectPolygonCollision(rect, polygon) {
|
||
// Konvertiere Polygon-Koordinaten von absoluten Pixeln zu relativen (0-1)
|
||
const relativePolygon = polygon.map(point => ({
|
||
x: point.x / 640, // originalTileSize
|
||
y: point.y / 640
|
||
}));
|
||
|
||
// Prüfe ob eine der Rechteck-Ecken im Polygon liegt
|
||
const corners = [
|
||
{ x: rect.x, y: rect.y }, // Oben links
|
||
{ x: rect.x + rect.width, y: rect.y }, // Oben rechts
|
||
{ x: rect.x, y: rect.y + rect.height }, // Unten links
|
||
{ x: rect.x + rect.width, y: rect.y + rect.height } // Unten rechts
|
||
];
|
||
|
||
for (let i = 0; i < corners.length; i++) {
|
||
const corner = corners[i];
|
||
const isInside = this.isPointInPolygon(corner.x, corner.y, relativePolygon);
|
||
|
||
if (isInside) {
|
||
if (this.taxi.speed > 0) {
|
||
console.log(`🚨 Ecke ${i} ist im Polygon!`);
|
||
}
|
||
return true; // Rechteck-Ecke ist im Polygon = Kollision
|
||
}
|
||
}
|
||
|
||
// Prüfe ob eine Polygon-Ecke im Rechteck liegt
|
||
for (let i = 0; i < relativePolygon.length; i++) {
|
||
const point = relativePolygon[i];
|
||
const isInside = this.isPointInRect(point.x, point.y, rect);
|
||
|
||
if (isInside) {
|
||
if (this.taxi.speed > 0) {
|
||
console.log(`🚨 Polygon-Punkt ${i} ist im Rechteck!`);
|
||
}
|
||
return true; // Polygon-Ecke ist im Rechteck = Kollision
|
||
}
|
||
}
|
||
|
||
// Prüfe ob Rechteck-Kanten das Polygon schneiden
|
||
const rectEdges = [
|
||
{ x1: rect.x, y1: rect.y, x2: rect.x + rect.width, y2: rect.y }, // Oben
|
||
{ x1: rect.x + rect.width, y1: rect.y, x2: rect.x + rect.width, y2: rect.y + rect.height }, // Rechts
|
||
{ x1: rect.x, y1: rect.y + rect.height, x2: rect.x + rect.width, y2: rect.y + rect.height }, // Unten
|
||
{ x1: rect.x, y1: rect.y, x2: rect.x, y2: rect.y + rect.height } // Links
|
||
];
|
||
|
||
for (const edge of rectEdges) {
|
||
for (let i = 0; i < relativePolygon.length; i++) {
|
||
const j = (i + 1) % relativePolygon.length;
|
||
const polyEdge = {
|
||
x1: relativePolygon[i].x,
|
||
y1: relativePolygon[i].y,
|
||
x2: relativePolygon[j].x,
|
||
y2: relativePolygon[j].y
|
||
};
|
||
|
||
if (this.lineIntersection(edge, polyEdge)) {
|
||
return true; // Kanten schneiden sich = Kollision
|
||
}
|
||
}
|
||
}
|
||
|
||
return false; // Keine Kollision
|
||
},
|
||
|
||
// Prüft ob ein Punkt in einem Polygon liegt (Point-in-Polygon)
|
||
isPointInPolygon(x, y, polygon) {
|
||
let inside = false;
|
||
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
||
if (((polygon[i].y > y) !== (polygon[j].y > y)) &&
|
||
(x < (polygon[j].x - polygon[i].x) * (y - polygon[i].y) / (polygon[j].y - polygon[i].y) + polygon[i].x)) {
|
||
inside = !inside;
|
||
}
|
||
}
|
||
return inside;
|
||
},
|
||
|
||
// Prüft ob ein Punkt in einem Rechteck liegt
|
||
isPointInRect(x, y, rect) {
|
||
return x >= rect.x && x <= rect.x + rect.width &&
|
||
y >= rect.y && y <= rect.y + rect.height;
|
||
},
|
||
|
||
// Prüft ob zwei Linien sich schneiden
|
||
lineIntersection(line1, line2) {
|
||
const x1 = line1.x1, y1 = line1.y1, x2 = line1.x2, y2 = line1.y2;
|
||
const x3 = line2.x1, y3 = line2.y1, x4 = line2.x2, y4 = line2.y2;
|
||
|
||
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
||
if (Math.abs(denom) < 1e-10) return false; // Parallele Linien
|
||
|
||
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
||
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
|
||
|
||
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
||
},
|
||
|
||
getCurrentTileType() {
|
||
// Hole das aktuelle Tile aus der geladenen Map
|
||
if (this.currentMap && this.currentMap.mapData) {
|
||
const mapData = this.currentMap.mapData;
|
||
const tile = mapData[this.currentTile.row] && mapData[this.currentTile.row][this.currentTile.col];
|
||
|
||
// Prüfe ob tile ein String ist (direkter Tile-Name) oder ein Objekt mit type-Eigenschaft
|
||
if (tile) {
|
||
if (typeof tile === 'string') {
|
||
return tile;
|
||
} else if (tile.type) {
|
||
return tile.type;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: Standard-Tile
|
||
return 'cornertopleft';
|
||
},
|
||
|
||
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;
|
||
},
|
||
|
||
render() {
|
||
// Lösche Canvas
|
||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||
|
||
// Zeichne Straßen
|
||
this.drawRoads();
|
||
|
||
// Zeichne Hindernisse
|
||
this.obstacles.forEach(obstacle => {
|
||
this.ctx.fillStyle = '#666';
|
||
this.ctx.fillRect(obstacle.x, obstacle.y, obstacle.width, obstacle.height);
|
||
});
|
||
|
||
// Passagiere/Ziele werden aktuell nicht gezeichnet
|
||
|
||
|
||
// Zeichne Taxi
|
||
this.ctx.save();
|
||
this.ctx.translate(this.taxi.x + this.taxi.width/2, this.taxi.y + this.taxi.height/2);
|
||
this.ctx.rotate(this.taxi.imageAngle + this.taxi.angle);
|
||
|
||
if (this.taxi.image) {
|
||
// Zeichne Taxi-Bild
|
||
// console.log('Zeichne Taxi-Bild:', this.taxi.image, 'Position:', this.taxi.x, this.taxi.y);
|
||
this.ctx.drawImage(
|
||
this.taxi.image,
|
||
-this.taxi.width/2,
|
||
-this.taxi.height/2,
|
||
this.taxi.width,
|
||
this.taxi.height
|
||
);
|
||
} else {
|
||
// Fallback: Zeichne blaues Rechteck wenn Bild nicht geladen
|
||
// console.log('Taxi-Bild nicht geladen, zeichne Fallback-Rechteck');
|
||
this.ctx.fillStyle = '#2196F3';
|
||
this.ctx.fillRect(-this.taxi.width/2, -this.taxi.height/2, this.taxi.width, this.taxi.height);
|
||
}
|
||
|
||
this.ctx.restore();
|
||
},
|
||
|
||
drawRoads() {
|
||
const tileSize = this.tiles.size; // 400x400px
|
||
|
||
// Zeichne nur das aktuelle Tile
|
||
if (this.currentMap && this.currentMap.mapData) {
|
||
const mapData = this.currentMap.mapData;
|
||
const rows = mapData.length;
|
||
const cols = mapData[0] ? mapData[0].length : 0;
|
||
|
||
// Prüfe ob aktuelle Tile-Position gültig ist
|
||
if (this.currentTile.row >= 0 && this.currentTile.row < rows &&
|
||
this.currentTile.col >= 0 && this.currentTile.col < cols) {
|
||
|
||
const tileType = mapData[this.currentTile.row][this.currentTile.col];
|
||
if (tileType) {
|
||
// Konvertiere Tile-Typ zu streetCoordinates.json Format
|
||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||
|
||
// Zeichne Straßenregionen für das aktuelle Tile
|
||
streetCoordinates.drawDriveableRegions(this.ctx, streetTileType, tileSize, 0, 0);
|
||
|
||
// Zeichne Tile-Overlay falls verfügbar
|
||
if (this.tiles.images[tileType]) {
|
||
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Fallback: Zeichne Standard-Tile wenn keine Map geladen ist
|
||
const tileType = 'cornertopleft'; // Standard-Tile
|
||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||
|
||
// Zeichne Straßenregionen
|
||
streetCoordinates.drawDriveableRegions(this.ctx, streetTileType, tileSize, 0, 0);
|
||
|
||
// Zeichne Tile-Overlay falls verfügbar
|
||
if (this.tiles.images[tileType]) {
|
||
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
|
||
}
|
||
}
|
||
},
|
||
|
||
getTileType(row, col, rows, cols) {
|
||
// Ecken
|
||
if (row === 0 && col === 0) return 'cornertopleft';
|
||
if (row === 0 && col === cols - 1) return 'cornertopright';
|
||
if (row === rows - 1 && col === 0) return 'cornerbottomleft';
|
||
if (row === rows - 1 && col === cols - 1) return 'cornerbottomright';
|
||
|
||
// Ränder
|
||
if (row === 0 || row === rows - 1) return 'horizontal';
|
||
if (col === 0 || col === cols - 1) return 'vertical';
|
||
|
||
// Innere Bereiche - zufällige Straßen
|
||
const rand = Math.random();
|
||
if (rand < 0.3) return 'cross';
|
||
if (rand < 0.5) return 'horizontal';
|
||
if (rand < 0.7) return 'vertical';
|
||
if (rand < 0.8) return 'fuelhorizontal';
|
||
if (rand < 0.9) return 'fuelvertical';
|
||
|
||
return 'horizontal'; // Fallback
|
||
},
|
||
|
||
async handleCanvasClick(event) {
|
||
// AudioContext bei erster Benutzerinteraktion initialisieren
|
||
await this.initAudioOnUserInteraction();
|
||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||
await this.audioContext.resume().catch(() => {});
|
||
}
|
||
// Motor ggf. direkt starten
|
||
if (this.motorSound && !this.motorSound.isPlaying) {
|
||
this.motorSound.start();
|
||
const speedKmh = this.taxi.speed * 5;
|
||
const speedFactor = Math.min(speedKmh / 120, 1);
|
||
const motorSpeed = 0.3 + (speedFactor * 0.3);
|
||
const volume = 0.1 + (speedFactor * 0.4);
|
||
this.motorSound.setSpeed(motorSpeed);
|
||
this.motorSound.setVolume(volume);
|
||
}
|
||
},
|
||
|
||
async handleKeyDown(event) {
|
||
this.keys[event.key] = true;
|
||
|
||
// AudioContext bei erster Benutzerinteraktion initialisieren
|
||
await this.initAudioOnUserInteraction();
|
||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||
await this.audioContext.resume().catch(() => {});
|
||
}
|
||
// Motor ggf. direkt starten
|
||
if (this.motorSound && !this.motorSound.isPlaying) {
|
||
this.motorSound.start();
|
||
const speedKmh = this.taxi.speed * 5;
|
||
const speedFactor = Math.min(speedKmh / 120, 1);
|
||
const motorSpeed = 0.3 + (speedFactor * 0.3);
|
||
const volume = 0.1 + (speedFactor * 0.4);
|
||
this.motorSound.setSpeed(motorSpeed);
|
||
this.motorSound.setVolume(volume);
|
||
}
|
||
|
||
event.preventDefault();
|
||
},
|
||
|
||
handleKeyUp(event) {
|
||
this.keys[event.key] = false;
|
||
},
|
||
|
||
togglePause() {
|
||
this.isPaused = !this.isPaused;
|
||
this.showPauseOverlay = this.isPaused;
|
||
},
|
||
|
||
restartLevel() {
|
||
this.score = 0;
|
||
this.money = 0;
|
||
this.passengersDelivered = 0;
|
||
this.fuel = 100;
|
||
this.taxi.x = 250;
|
||
this.taxi.y = 250;
|
||
this.taxi.angle = 0;
|
||
this.taxi.speed = 0;
|
||
this.generateLevel();
|
||
},
|
||
|
||
goBack() {
|
||
this.$router.push('/minigames');
|
||
},
|
||
|
||
toggleStats() {
|
||
this.isStatsExpanded = !this.isStatsExpanded;
|
||
},
|
||
|
||
initializeMinimap() {
|
||
this.minimapCanvas = this.$refs.minimapCanvas;
|
||
this.minimapCtx = this.minimapCanvas.getContext('2d');
|
||
},
|
||
|
||
async loadTiles() {
|
||
const tileNames = [
|
||
'cornerbottomleft', 'cornerbottomright', 'cornertopleft', 'cornertopright',
|
||
'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical'
|
||
];
|
||
|
||
const mapTileNames = [
|
||
'map-cornerbottomleft', 'map-cornerbottomright', 'map-cornertopleft', 'map-cornertopright',
|
||
'map-cross', 'map-fuelhorizontal', 'map-fuelvertical', 'map-horizontal', 'map-vertical'
|
||
];
|
||
|
||
// Lade normale Tiles
|
||
for (const tileName of tileNames) {
|
||
const img = new Image();
|
||
img.src = `/images/taxi/${tileName}.svg`;
|
||
this.tiles.images[tileName] = img;
|
||
}
|
||
|
||
// Lade Map-Tiles
|
||
for (const tileName of mapTileNames) {
|
||
const img = new Image();
|
||
img.src = `/images/taxi/${tileName}.svg`;
|
||
this.tiles.images[tileName] = img;
|
||
}
|
||
},
|
||
|
||
loadTaxiImage() {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
// console.log('Taxi-Bild erfolgreich geladen:', img);
|
||
this.taxi.image = img;
|
||
};
|
||
img.onerror = (error) => {
|
||
console.error('Fehler beim Laden des Taxi-Bildes:', error);
|
||
console.log('Versuche Pfad:', '/images/taxi/taxi.svg');
|
||
};
|
||
img.src = '/images/taxi/taxi.svg';
|
||
// console.log('Lade Taxi-Bild von:', '/images/taxi/taxi.svg');
|
||
},
|
||
|
||
async loadMaps() {
|
||
try {
|
||
const response = await apiClient.get('/api/taxi-maps/maps');
|
||
this.maps = response.data.data || [];
|
||
|
||
// Verwende die erste verfügbare Map als Standard
|
||
if (this.maps.length > 0) {
|
||
this.currentMap = this.maps[0];
|
||
this.selectedMap = this.maps[0]; // Auch selectedMap setzen
|
||
this.selectedMapId = this.maps[0].id;
|
||
|
||
// Canvas-Größe an geladene Map anpassen
|
||
this.updateCanvasSize();
|
||
|
||
// Minimap neu zeichnen nach dem Laden der Map
|
||
this.$nextTick(() => {
|
||
this.drawMinimap();
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der Maps:', error);
|
||
}
|
||
},
|
||
|
||
onMapChange() {
|
||
// Wechsle zur ausgewählten Map
|
||
const selectedMap = this.maps.find(map => map.id === this.selectedMapId);
|
||
if (selectedMap) {
|
||
this.currentMap = selectedMap;
|
||
// console.log('Gewechselt zu Map:', selectedMap);
|
||
|
||
// Canvas-Größe an neue Map anpassen
|
||
this.updateCanvasSize();
|
||
|
||
// Taxi in die Mitte des 400x400px Tiles positionieren
|
||
this.taxi.x = 250 - this.taxi.width/2; // Zentriert in der Mitte
|
||
this.taxi.y = 250 - this.taxi.height/2; // Zentriert in der Mitte
|
||
this.taxi.angle = 0; // Fahrtrichtung nach oben
|
||
this.taxi.speed = 0; // Geschwindigkeit zurücksetzen
|
||
|
||
// Aktuelle Tile-Position zurücksetzen
|
||
this.currentTile.row = 0;
|
||
this.currentTile.col = 0;
|
||
|
||
// Minimap neu zeichnen
|
||
this.$nextTick(() => {
|
||
this.drawMinimap();
|
||
});
|
||
}
|
||
},
|
||
|
||
drawMinimap() {
|
||
if (!this.minimapCtx) return;
|
||
|
||
const ctx = this.minimapCtx;
|
||
const canvas = this.minimapCanvas;
|
||
|
||
// Minimap löschen
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Hintergrund
|
||
ctx.fillStyle = '#f0f0f0';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Zeichne Map-Tiles aus der geladenen Map
|
||
if (this.currentMap && this.currentMap.mapData) {
|
||
const mapData = this.currentMap.mapData;
|
||
const rows = mapData.length;
|
||
const cols = mapData[0] ? mapData[0].length : 0;
|
||
|
||
// Berechne optimale Tile-Größe für quadratische Tiles
|
||
const maxTiles = Math.max(rows, cols);
|
||
const tileSize = Math.min(canvas.width / maxTiles, canvas.height / maxTiles);
|
||
|
||
// Zentriere die Map im Canvas
|
||
const offsetX = (canvas.width - cols * tileSize) / 2;
|
||
const offsetY = (canvas.height - rows * tileSize) / 2;
|
||
|
||
for (let row = 0; row < rows; row++) {
|
||
for (let col = 0; col < cols; col++) {
|
||
const x = offsetX + col * tileSize;
|
||
const y = offsetY + row * tileSize;
|
||
|
||
const tileType = mapData[row][col];
|
||
if (tileType) {
|
||
const mapTileType = 'map-' + tileType;
|
||
|
||
// Zeichne Map-Tile falls verfügbar
|
||
if (this.tiles.images[mapTileType]) {
|
||
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Fallback: Zeichne statische Map wenn keine Map geladen ist
|
||
const cols = 8;
|
||
const rows = 8;
|
||
|
||
// Berechne optimale Tile-Größe für quadratische Tiles
|
||
const maxTiles = Math.max(rows, cols);
|
||
const tileSize = Math.min(canvas.width / maxTiles, canvas.height / maxTiles);
|
||
|
||
// Zentriere die Map im Canvas
|
||
const offsetX = (canvas.width - cols * tileSize) / 2;
|
||
const offsetY = (canvas.height - rows * tileSize) / 2;
|
||
|
||
for (let row = 0; row < rows; row++) {
|
||
for (let col = 0; col < cols; col++) {
|
||
const x = offsetX + col * tileSize;
|
||
const y = offsetY + row * tileSize;
|
||
|
||
let tileType = this.getTileType(row, col, rows, cols);
|
||
let mapTileType = 'map-' + tileType;
|
||
|
||
// Zeichne Map-Tile falls verfügbar
|
||
if (this.tiles.images[mapTileType]) {
|
||
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Berechne Skalierung für Passagiere, Ziele und Tankstellen
|
||
let scaleX, scaleY, offsetX, offsetY;
|
||
if (this.currentMap && this.currentMap.mapData) {
|
||
const mapData = this.currentMap.mapData;
|
||
const rows = mapData.length;
|
||
const cols = mapData[0] ? mapData[0].length : 0;
|
||
const maxTiles = Math.max(rows, cols);
|
||
const tileSize = Math.min(canvas.width / maxTiles, canvas.height / maxTiles);
|
||
scaleX = tileSize / 500; // 400 ist die neue Tile-Größe
|
||
scaleY = tileSize / 500;
|
||
offsetX = (canvas.width - cols * tileSize) / 2;
|
||
offsetY = (canvas.height - rows * tileSize) / 2;
|
||
} else {
|
||
// Fallback für statische Map
|
||
const maxTiles = 8;
|
||
const tileSize = Math.min(canvas.width / maxTiles, canvas.height / maxTiles);
|
||
scaleX = tileSize / 500;
|
||
scaleY = tileSize / 500;
|
||
offsetX = (canvas.width - 8 * tileSize) / 2;
|
||
offsetY = (canvas.height - 8 * tileSize) / 2;
|
||
}
|
||
|
||
|
||
|
||
// Taxi (blau) - Position basierend auf aktueller Tile-Position
|
||
ctx.fillStyle = '#2196F3';
|
||
ctx.beginPath();
|
||
const taxiGlobalX = this.currentTile.col * 500 + this.taxi.x;
|
||
const taxiGlobalY = this.currentTile.row * 500 + this.taxi.y;
|
||
ctx.arc(offsetX + taxiGlobalX * scaleX, offsetY + taxiGlobalY * scaleY, 3, 0, 2 * Math.PI);
|
||
ctx.fill();
|
||
|
||
// Taxi-Richtung anzeigen
|
||
ctx.strokeStyle = '#2196F3';
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
const taxiX = offsetX + taxiGlobalX * scaleX;
|
||
const taxiY = offsetY + taxiGlobalY * scaleY;
|
||
const endX = taxiX + Math.cos(this.taxi.angle) * 6;
|
||
const endY = taxiY + Math.sin(this.taxi.angle) * 6;
|
||
ctx.moveTo(taxiX, taxiY);
|
||
ctx.lineTo(endX, endY);
|
||
ctx.stroke();
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
toggleIcon() {
|
||
return this.isStatsExpanded ? '−' : '+';
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Verwendet globale Scroll-Klassen: .contenthidden und .contentscroll */
|
||
|
||
.game-title {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.game-title h1 {
|
||
margin: 0 0 10px 0;
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.game-title p {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Spiel-Layout */
|
||
.game-layout {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 20px;
|
||
max-width: 1600px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
/* Game Canvas Section */
|
||
.game-canvas-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 500px;
|
||
margin: 0 auto; /* Zentriert den gesamten Container */
|
||
}
|
||
|
||
/* Tacho-Display */
|
||
.tacho-display {
|
||
width: 500px; /* Gleiche Breite wie das zentrale Tile */
|
||
background: #f8f9fa;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
margin: 0; /* Kein Margin */
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
box-sizing: border-box; /* Padding wird in die Breite eingerechnet */
|
||
}
|
||
|
||
.tacho-speed {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 10pt;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tacho-icon {
|
||
font-size: 10pt;
|
||
}
|
||
|
||
.speed-value {
|
||
font-weight: bold;
|
||
color: #2196F3;
|
||
}
|
||
|
||
.speed-unit {
|
||
color: #666;
|
||
font-size: 9pt;
|
||
}
|
||
|
||
.game-board-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
min-width: 0; /* Verhindert Overflow */
|
||
}
|
||
|
||
.game-area {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 20px;
|
||
align-items: flex-start;
|
||
width: 100%;
|
||
max-width: 1200px;
|
||
justify-content: center; /* Zentriert den Inhalt */
|
||
}
|
||
|
||
.sidebar-section {
|
||
width: 320px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
position: sticky;
|
||
top: 20px;
|
||
}
|
||
|
||
/* Stats Card */
|
||
.stats-card {
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stats-header {
|
||
background: #f8f9fa;
|
||
border-bottom: 1px solid #ddd;
|
||
padding: 15px 20px;
|
||
}
|
||
|
||
.stats-header-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.stats-title {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.toggle-button {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.2rem;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
color: #666;
|
||
}
|
||
|
||
.toggle-button:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.stats-list {
|
||
padding: 20px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.stat-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.2rem;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.9rem;
|
||
color: #666;
|
||
}
|
||
|
||
.score-value { color: #4CAF50; }
|
||
.money-value { color: #FF9800; }
|
||
.passengers-value { color: #2196F3; }
|
||
.fuel-value { color: #9C27B0; }
|
||
|
||
/* Minimap Card */
|
||
.minimap-card {
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.minimap-header {
|
||
background: #f8f9fa;
|
||
border-bottom: 1px solid #ddd;
|
||
padding: 15px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.minimap-title {
|
||
margin: 0;
|
||
font-size: 1.1rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.map-selector {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.map-select {
|
||
padding: 5px 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 0.9rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.map-select:focus {
|
||
outline: none;
|
||
border-color: #F9A22C;
|
||
}
|
||
|
||
.minimap-container {
|
||
padding: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.minimap-canvas {
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
background: #f9f9f9;
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
/* Controls Legend */
|
||
.controls-legend {
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
width: 200px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.controls-legend h3 {
|
||
margin: 0 0 15px 0;
|
||
color: #333;
|
||
font-size: 1.1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.legend-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.legend-key {
|
||
background: #f0f0f0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
font-weight: bold;
|
||
font-size: 0.8rem;
|
||
min-width: 40px;
|
||
text-align: center;
|
||
color: #333;
|
||
}
|
||
|
||
.legend-text {
|
||
color: #666;
|
||
font-size: 0.9rem;
|
||
flex: 1;
|
||
}
|
||
|
||
/* Game Objectives in Legend */
|
||
.controls-legend .game-objectives {
|
||
margin-top: 20px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.controls-legend .game-objectives h4 {
|
||
margin: 0 0 10px 0;
|
||
color: #333;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.controls-legend .game-objectives ul {
|
||
margin: 0;
|
||
padding-left: 15px;
|
||
color: #666;
|
||
font-size: 0.85rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.controls-legend .game-objectives li {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
/* Game Canvas */
|
||
.game-canvas-container {
|
||
margin: 0; /* Kein Margin */
|
||
text-align: center;
|
||
width: 500px; /* Feste Breite wie das Tacho-Display */
|
||
box-sizing: border-box; /* Border wird in die Breite eingerechnet */
|
||
}
|
||
|
||
.game-canvas {
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
background: #f0f0f0;
|
||
cursor: crosshair;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
/* Pause Overlay */
|
||
.pause-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0,0,0,0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.pause-message {
|
||
background: white;
|
||
padding: 40px;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.pause-message h2 {
|
||
margin: 0 0 20px 0;
|
||
color: #333;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.resume-button {
|
||
background: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 6px;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
transition: background 0.3s;
|
||
}
|
||
|
||
.resume-button:hover {
|
||
background: #45a049;
|
||
}
|
||
|
||
/* Game Controls */
|
||
.game-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin: 20px 0;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.control-button {
|
||
background: #F9A22C;
|
||
color: #000;
|
||
border: 1px solid #F9A22C;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.control-button:hover {
|
||
background: #fdf1db;
|
||
color: #7E471B;
|
||
border: 1px solid #7E471B;
|
||
}
|
||
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 1024px) {
|
||
.game-layout {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-section {
|
||
width: 100%;
|
||
position: static;
|
||
order: -1; /* Sidebar kommt vor dem Spielbrett */
|
||
}
|
||
|
||
.game-board-section {
|
||
width: 100%;
|
||
}
|
||
|
||
.game-area {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.controls-legend {
|
||
width: 100%;
|
||
max-width: 500px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.game-canvas {
|
||
width: 100%;
|
||
height: auto;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.instructions-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.game-controls {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.control-button {
|
||
width: 200px;
|
||
}
|
||
|
||
.minimap-canvas {
|
||
width: 100%;
|
||
height: auto;
|
||
}
|
||
}
|
||
</style>
|