Files
yourpart3/frontend/src/views/minigames/TaxiGame.vue
Torsten Schulz (local) abfa6f4b8d Änderung: Verbesserung der Audio-Integration und Anpassung der Geräuschparameter im Taxi-Spiel
Ä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.
2025-09-17 11:24:15 +02:00

1721 lines
51 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>