Änderung: Verbesserung der MessageDialog-Komponente und Integration von Übersetzungen

Änderungen:
- Anpassung des MessageDialog zur Unterstützung von dynamischen Titeln und Schaltflächen mit Übersetzungen.
- Implementierung einer Methode zur Interpolation von Platzhaltern in Nachrichten.
- Erweiterung der i18n-Übersetzungen für Crash-Nachrichten im Minispiel.
- Aktualisierung der TaxiGame.vue zur Anzeige von Unfallmeldungen über den MessageDialog.

Diese Anpassungen verbessern die Benutzererfahrung durch mehrsprachige Unterstützung und dynamische Nachrichten im Taxi-Minispiel.
This commit is contained in:
Torsten Schulz (local)
2025-09-15 23:05:18 +02:00
parent 643c152194
commit 3f33da06e5
7 changed files with 372 additions and 55 deletions

View File

@@ -1,6 +1,6 @@
<template>
<DialogWidget ref="dialog" title="message.title" :show-close="true" :buttons="buttons" :modal="true" width="25em"
height="15em" name="MessageDialog" :isTitleTranslated=true>
<DialogWidget ref="dialog" :title="translatedTitle" :show-close="true" :buttons="translatedButtons" :modal="true" width="25em"
height="15em" name="MessageDialog" :isTitleTranslated=false>
<div class="message-content">
<p>{{ translatedMessage }}</p>
</div>
@@ -18,26 +18,80 @@ export default {
data() {
return {
message: '',
title: '',
parameters: {},
onClose: null,
buttons: [
{ text: 'message.close', action: 'close' }
{ text: 'tr:message.close', action: 'close' }
]
};
},
computed: {
translatedTitle() {
if (this.title.startsWith('tr:')) {
return this.$t(this.title.substring(3));
}
if (this.title) {
return this.title;
}
// Standard-Titel je nach Sprache
return this.$t('message.title');
},
translatedMessage() {
if (this.message.startsWith('tr:')) {
return this.$t(this.message.substring(3));
const i18nKey = this.message.substring(3);
const translation = this.$t(i18nKey);
console.log('translatedMessage:', {
i18nKey: i18nKey,
translation: translation,
parameters: this.parameters,
allMinigames: this.$t('minigames'),
crashSection: this.$t('minigames.taxi.crash')
});
// Ersetze Parameter in der Übersetzung
return this.interpolateParameters(translation);
}
return this.message;
},
translatedButtons() {
return this.buttons.map(button => ({
...button,
text: button.text.startsWith('tr:') ? this.$t(button.text.substring(3)) : button.text
}));
}
},
methods: {
open(message) {
open(message, title = '', parameters = {}, onClose = null) {
this.message = message;
this.title = title;
this.parameters = parameters;
this.onClose = onClose;
this.$refs.dialog.open();
},
close() {
this.$refs.dialog.close();
// Rufe Callback auf, wenn vorhanden
if (this.onClose && typeof this.onClose === 'function') {
this.onClose();
}
},
interpolateParameters(text) {
// Ersetze {key} Platzhalter mit den entsprechenden Werten
let result = text;
console.log('interpolateParameters:', {
originalText: text,
parameters: this.parameters
});
for (const [key, value] of Object.entries(this.parameters)) {
const placeholder = `{${key}}`;
const regex = new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g');
result = result.replace(regex, value);
console.log(`Replaced ${placeholder} with ${value}:`, result);
}
console.log('Final result:', result);
return result;
}
}
};

View File

@@ -17,6 +17,7 @@ import enFalukant from './locales/en/falukant.json';
import enPasswordReset from './locales/en/passwordReset.json';
import enBlog from './locales/en/blog.json';
import enMinigames from './locales/en/minigames.json';
import enMessage from './locales/en/message.json';
import deGeneral from './locales/de/general.json';
import deHeader from './locales/de/header.json';
@@ -34,6 +35,7 @@ import deFalukant from './locales/de/falukant.json';
import dePasswordReset from './locales/de/passwordReset.json';
import deBlog from './locales/de/blog.json';
import deMinigames from './locales/de/minigames.json';
import deMessage from './locales/de/message.json';
const messages = {
en: {
@@ -53,6 +55,7 @@ const messages = {
...enFalukant,
...enBlog,
...enMinigames,
...enMessage,
},
de: {
'Ok': 'Ok',
@@ -72,6 +75,7 @@ const messages = {
...deFalukant,
...deBlog,
...deMinigames,
...deMessage,
}
};

View File

@@ -0,0 +1,7 @@
{
"message": {
"title": "Mitteilung",
"close": "Schließen",
"test": "Test funktioniert"
}
}

View File

@@ -63,7 +63,11 @@
"deliverPassenger": "Passagier abliefern",
"refuel": "Tanken",
"startEngine": "Motor starten",
"stopEngine": "Motor stoppen"
"stopEngine": "Motor stoppen",
"crash": {
"title": "Unfall!",
"message": "Du hattest einen Unfall! Crashes: {crashes}"
}
}
}
}

View File

@@ -0,0 +1,6 @@
{
"message": {
"title": "Notice",
"close": "Close"
}
}

View File

@@ -63,7 +63,11 @@
"deliverPassenger": "Deliver Passenger",
"refuel": "Refuel",
"startEngine": "Start Engine",
"stopEngine": "Stop Engine"
"stopEngine": "Stop Engine",
"crash": {
"title": "Crash!",
"message": "You had an accident! Crashes: {crashes}"
}
}
}
}

View File

@@ -13,7 +13,7 @@
<!-- Spielbrett (links) -->
<div class="game-board-section">
<!-- Pause-Anzeige -->
<div v-if="isPaused" class="pause-overlay">
<div v-if="showPauseOverlay" class="pause-overlay">
<div class="pause-message">
<h2>{{ $t('minigames.taxi.paused') }}</h2>
<button @click="togglePause" class="resume-button">
@@ -126,6 +126,11 @@
<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>
@@ -162,24 +167,31 @@
</div>
</div>
</div>
</div>
</template>
<script>
import streetCoordinates from '../../utils/streetCoordinates.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,
canvas: null,
ctx: null,
@@ -224,9 +236,25 @@ export default {
},
beforeUnmount() {
this.cleanup();
},
methods: {
initializeGame() {
},
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();
@@ -258,13 +286,11 @@ export default {
const rows = mapData.length;
const cols = mapData[0] ? mapData[0].length : 0;
// Berechne Tile-Koordinaten basierend auf Taxi-Position
const tileRow = Math.floor(this.taxi.y / this.tiles.size);
const tileCol = Math.floor(this.taxi.x / this.tiles.size);
// 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;
// Begrenze auf Map-Grenzen
this.currentTile.row = Math.max(0, Math.min(rows - 1, tileRow));
this.currentTile.col = Math.max(0, Math.min(cols - 1, tileCol));
// 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));
@@ -342,7 +368,8 @@ export default {
const relativeX = Math.random();
const relativeY = Math.random();
if (streetCoordinates.isPointDriveable(relativeX, relativeY, tileType, 1)) {
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
if (streetCoordinates.isPointDriveable(relativeX, relativeY, streetTileType, 1)) {
return {
x: tileCol * tileSize + relativeX * tileSize,
y: tileRow * tileSize + relativeY * tileSize
@@ -370,7 +397,8 @@ export default {
const relativeX = Math.random();
const relativeY = Math.random();
if (!streetCoordinates.isPointDriveable(relativeX, relativeY, tileType, 1)) {
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
if (!streetCoordinates.isPointDriveable(relativeX, relativeY, streetTileType, 1)) {
return {
x: tileCol * tileSize + relativeX * tileSize,
y: tileRow * tileSize + relativeY * tileSize
@@ -396,7 +424,7 @@ export default {
this.updateTaxi();
this.handlePassengerActions();
// this.checkCollisions(); // Temporär deaktiviert für freie Fahrt
this.checkCollisions();
this.render();
// Minimap zeichnen
@@ -493,44 +521,249 @@ export default {
},
checkCollisions() {
// Prüfe Straßenkollisionen
if (!this.isTaxiOnRoad()) {
this.taxi.speed = 0;
this.score = Math.max(0, this.score - 2);
// Prüfe Straßenkollisionen nur wenn das Spiel nicht pausiert ist
if (!this.isPaused && !this.isTaxiOnRoad()) {
this.handleCrash();
}
// Prüfe Hindernisse
this.obstacles.forEach(obstacle => {
if (this.checkCollision(this.taxi, obstacle)) {
this.taxi.speed = 0;
this.score = Math.max(0, this.score - 5);
// 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
// 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);
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;
// Fokus zurück auf Canvas setzen
this.$nextTick(() => {
if (this.canvas) {
this.canvas.focus();
}
});
},
resetTaxiPosition() {
// Taxi in der Mitte des Screens platzieren
this.taxi.x = 200 - this.taxi.width/2;
this.taxi.y = 200 - 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() {
const tileSize = this.tiles.size;
const cols = 8;
const rows = 8;
// Bestimme welches Tile das Taxi gerade belegt
const tileCol = Math.floor(this.taxi.x / tileSize);
const tileRow = Math.floor(this.taxi.y / tileSize);
// Prüfe ob das Taxi innerhalb der Canvas-Grenzen ist
if (tileCol < 0 || tileCol >= cols || tileRow < 0 || tileRow >= rows) {
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;
}
// Bestimme Tile-Typ
const tileType = this.getTileType(tileRow, tileCol, rows, cols);
// Konvertiere Taxi-Position zu relativen Koordinaten innerhalb des Tiles
const relativeX = (this.taxi.x - tileCol * tileSize) / tileSize;
const relativeY = (this.taxi.y - tileRow * tileSize) / tileSize;
// Hole das aktuelle Tile und dessen Polygone
const tileType = this.getCurrentTileType();
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
const originalTileSize = streetCoordinates.getOriginalTileSize(); // 640px
const currentTileSize = 400; // Aktuelle Render-Größe
// Prüfe ob das Taxi auf der Straße ist
return streetCoordinates.isPointDriveable(relativeX, relativeY, tileType, 1);
// 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) {
@@ -577,7 +810,7 @@ export default {
if (this.taxi.image) {
// Zeichne Taxi-Bild
console.log('Zeichne Taxi-Bild:', this.taxi.image, 'Position:', this.taxi.x, this.taxi.y);
// 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,
@@ -587,7 +820,7 @@ export default {
);
} else {
// Fallback: Zeichne blaues Rechteck wenn Bild nicht geladen
console.log('Taxi-Bild nicht geladen, zeichne Fallback-Rechteck');
// 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);
}
@@ -609,9 +842,12 @@ export default {
this.currentTile.col >= 0 && this.currentTile.col < cols) {
const tileType = mapData[this.currentTile.row][this.currentTile.col];
if (tileType) {
// Zeichne Straßenregionen für das aktuelle Tile
streetCoordinates.drawDriveableRegions(this.ctx, tileType, tileSize, 0, 0);
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]) {
@@ -622,9 +858,10 @@ export default {
} 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, tileType, tileSize, 0, 0);
streetCoordinates.drawDriveableRegions(this.ctx, streetTileType, tileSize, 0, 0);
// Zeichne Tile-Overlay falls verfügbar
if (this.tiles.images[tileType]) {
@@ -670,6 +907,7 @@ export default {
togglePause() {
this.isPaused = !this.isPaused;
this.showPauseOverlay = this.isPaused;
},
restartLevel() {
@@ -726,7 +964,7 @@ export default {
loadTaxiImage() {
const img = new Image();
img.onload = () => {
console.log('Taxi-Bild erfolgreich geladen:', img);
// console.log('Taxi-Bild erfolgreich geladen:', img);
this.taxi.image = img;
};
img.onerror = (error) => {
@@ -734,7 +972,7 @@ export default {
console.log('Versuche Pfad:', '/images/taxi/taxi.svg');
};
img.src = '/images/taxi/taxi.svg';
console.log('Lade Taxi-Bild von:', '/images/taxi/taxi.svg');
// console.log('Lade Taxi-Bild von:', '/images/taxi/taxi.svg');
},
async loadMaps() {
@@ -745,8 +983,8 @@ export default {
// 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;
console.log('Geladene Map:', this.currentMap);
// Canvas-Größe an geladene Map anpassen
this.updateCanvasSize();
@@ -766,7 +1004,7 @@ export default {
const selectedMap = this.maps.find(map => map.id === this.selectedMapId);
if (selectedMap) {
this.currentMap = selectedMap;
console.log('Gewechselt zu Map:', selectedMap);
// console.log('Gewechselt zu Map:', selectedMap);
// Canvas-Größe an neue Map anpassen
this.updateCanvasSize();