Änderung: Hinzufügung des Taxi-Minispiels und zugehöriger Funktionen

Änderungen:
- Integration des Taxi-Minispiels mit neuen Routen und Komponenten im Backend und Frontend.
- Erstellung von Modellen und Datenbank-Schemas für das Taxi-Spiel, einschließlich TaxiGameState, TaxiLevelStats und TaxiMap.
- Erweiterung der Navigationsstruktur und der Benutzeroberfläche, um das Taxi-Spiel und die zugehörigen Tools zu unterstützen.
- Aktualisierung der Übersetzungen für das Taxi-Minispiel in Deutsch und Englisch.

Diese Anpassungen erweitern die Funktionalität der Anwendung um ein neues Minispiel und verbessern die Benutzererfahrung durch neue Features und Inhalte.
This commit is contained in:
Torsten Schulz (local)
2025-09-15 17:59:42 +02:00
parent 4699488ce1
commit f230849a5c
72 changed files with 7698 additions and 133 deletions

View File

@@ -0,0 +1,323 @@
{
"tileSize": 640,
"tiles": {
"cornerBottomRight": {
"regions": [
[
{"x": 0, "y": 0},
{"x": 0, "y": 1},
{"x": 0.375, "y": 1},
{"x": 0.375, "y": 0.427},
{"x": 0.38, "y": 0.409},
{"x": 0.389, "y": 0.397},
{"x": 0.4, "y": 0.388},
{"x": 0.408, "y": 0.397},
{"x": 0.417, "y": 0.38},
{"x": 0.434, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0}
],
[
{"x": 0.625, "y": 1},
{"x": 0.625, "y": 0.663},
{"x": 0.629, "y": 0.651},
{"x": 0.632, "y": 0.647},
{"x": 0.634, "y": 0.642},
{"x": 0.641, "y": 0.636},
{"x": 0.648, "y": 0.632},
{"x": 0.656, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1}
]
]
},
"cornerBottomLeft": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 0.575, "y": 0.375},
{"x": 0.588, "y": 0.38},
{"x": 0.6, "y": 0.386},
{"x": 0.611, "y": 0.395},
{"x": 0.619, "y": 0.406},
{"x": 0.625, "y": 0.422},
{"x": 0.625, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 0, "y": 0.625},
{"x": 0.336, "y": 0.625},
{"x": 0.35, "y": 0.629},
{"x": 0.359, "y": 0.636},
{"x": 0.366, "y": 0.642},
{"x": 0.373, "y": 0.651},
{"x": 0.375, "y": 0.659},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1}
]
]
},
"cornerTopLeft": {
"regions": [
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 0.339},
{"x": 0.372, "y": 0.353},
{"x": 0.366, "y": 0.363},
{"x": 0.361, "y": 0.367},
{"x": 0.356, "y": 0.37},
{"x": 0.348, "y": 0.373},
{"x": 0.336, "y": 0.375},
{"x": 0, "y": 0.375},
{"x": 0, "y": 0}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 0.583},
{"x": 0.62, "y": 0.594},
{"x": 0.615, "y": 0.605},
{"x": 0.605, "y": 0.614},
{"x": 0.594, "y": 0.621},
{"x": 0.584, "y": 0.625},
{"x": 0, "y": 0.625},
{"x": 0, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0}
]
]
},
"cornerTopRight": {
"regions": [
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 0.583},
{"x": 0.38, "y": 0.594},
{"x": 0.384, "y": 0.605},
{"x": 0.395, "y": 0.614},
{"x": 0.406, "y": 0.621},
{"x": 0.416, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1},
{"x": 0, "y": 1},
{"x": 0, "y": 0}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 0.339},
{"x": 0.628, "y": 0.353},
{"x": 0.634, "y": 0.363},
{"x": 0.639, "y": 0.367},
{"x": 0.644, "y": 0.37},
{"x": 0.652, "y": 0.373},
{"x": 0.664, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0}
]
]
},
"horizontal": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 0, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1},
{"x": 0, "y": 1}
]
]
},
"vertical": {
"regions": [
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1},
{"x": 0, "y": 0}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0}
]
]
},
"cross": {
"regions": [
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 0.375},
{"x": 0, "y": 0.375},
{"x": 0, "y": 0}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0}
],
[
{"x": 0.375, "y": 1},
{"x": 0.375, "y": 0.625},
{"x": 0, "y": 0.625},
{"x": 0, "y": 1}
],
[
{"x": 0.625, "y": 1},
{"x": 0.625, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1}
]
]
},
"fuelHorizontal": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 0.075, "y": 0.375},
{"x": 0.384, "y": 0.195},
{"x": 0.615, "y": 0.195},
{"x": 0.925, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 0.25, "y": 0.375},
{"x": 0.384, "y": 0.299},
{"x": 0.615, "y": 0.299},
{"x": 0.75, "y": 0.375},
{"x": 0.25, "y": 0.375}
],
[
{"x": 0, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1},
{"x": 0, "y": 1}
]
]
},
"fuelVertical": {
"regions": [
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 0.075},
{"x": 0.805, "y": 0.384},
{"x": 0.805, "y": 0.615},
{"x": 0.625, "y": 0.925},
{"x": 0.625, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0}
],
[
{"x": 0.625, "y": 0.25},
{"x": 0.701, "y": 0.384},
{"x": 0.701, "y": 0.615},
{"x": 0.625, "y": 0.75},
{"x": 0.625, "y": 0.25}
],
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1},
{"x": 0, "y": 0}
]
]
},
"tLeft": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 0.375, "y": 0.375},
{"x": 0.375, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 0, "y": 0.625},
{"x": 0.375, "y": 0.625},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 1},
{"x": 1, "y": 1},
{"x": 1, "y": 0}
]
]
},
"tRight": {
"regions": [
[
{"x": 0.375, "y": 0},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1},
{"x": 0, "y": 0}
],
[
{"x": 0.625, "y": 0},
{"x": 0.625, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0}
],
[
{"x": 0.625, "y": 1},
{"x": 0.625, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1}
]
]
},
"tUp": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 0.375, "y": 0.375},
{"x": 0.375, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 1, "y": 0.375},
{"x": 0.625, "y": 0.375},
{"x": 0.625, "y": 0},
{"x": 1, "y": 0}
],
[
{"x": 0, "y": 0.625},
{"x": 1, "y": 0.625},
{"x": 1, "y": 1},
{"x": 0, "y": 1}
]
]
},
"tDown": {
"regions": [
[
{"x": 0, "y": 0.375},
{"x": 1, "y": 0.375},
{"x": 1, "y": 0},
{"x": 0, "y": 0}
],
[
{"x": 0, "y": 0.625},
{"x": 0.375, "y": 0.625},
{"x": 0.375, "y": 1},
{"x": 0, "y": 1}
],
[
{"x": 1, "y": 0.625},
{"x": 0.625, "y": 0.625},
{"x": 0.625, "y": 1},
{"x": 1, "y": 1}
]
]
}
}
}

View File

@@ -192,6 +192,34 @@
"totalUsers": "Gesamtanzahl Benutzer",
"genderDistribution": "Geschlechterverteilung",
"ageDistribution": "Altersverteilung"
},
"taxiTools": {
"title": "Taxi-Tools",
"description": "Verwalte Taxi-Maps, Level und Konfigurationen",
"mapEditor": {
"title": "Map bearbeiten",
"availableMaps": "Verfügbare Maps: {count}",
"newMap": "Neue Map erstellen",
"mapFormat": "{name} (Position: {x},{y})",
"mapName": "Map-Name",
"mapDescription": "Beschreibung",
"mapWidth": "Breite",
"mapHeight": "Höhe",
"tileSize": "Tile-Größe",
"positionX": "X-Position",
"positionY": "Y-Position",
"mapType": "Map-Typ",
"mapLayout": "Map-Layout",
"tilePalette": "Tile-Palette",
"position": "Position",
"fillAllRoads": "Alle Straßen",
"clearAll": "Alle löschen",
"generateRandom": "Zufällig generieren",
"delete": "Löschen",
"update": "Aktualisieren",
"cancel": "Abbrechen",
"create": "Erstellen"
}
}
}
}

View File

@@ -33,6 +33,37 @@
"totalStars": "Gesamtsterne",
"levelsCompleted": "Abgeschlossene Level",
"restartCampaign": "Kampagne neu starten"
},
"taxi": {
"title": "Taxi Simulator",
"description": "Fahre Passagiere durch die Stadt und verdiene Geld!",
"gameStats": "Spiel-Statistiken",
"score": "Punkte",
"money": "Geld",
"passengers": "Passagiere",
"currentLevel": "Aktueller Level",
"level": "Level",
"fuel": "Treibstoff",
"fuelLeft": "Verbleibender Treibstoff",
"restartLevel": "Level neu starten",
"pause": "Pause",
"resume": "Weiterspielen",
"paused": "Spiel pausiert",
"levelComplete": "Level abgeschlossen!",
"levelScore": "Level-Punktzahl",
"moneyEarned": "Verdientes Geld",
"passengersDelivered": "Beförderte Passagiere",
"nextLevel": "Nächster Level",
"campaignComplete": "Kampagne abgeschlossen!",
"totalScore": "Gesamtpunktzahl",
"totalMoney": "Gesamtgeld",
"levelsCompleted": "Abgeschlossene Level",
"restartCampaign": "Kampagne neu starten",
"pickupPassenger": "Passagier aufnehmen",
"deliverPassenger": "Passagier abliefern",
"refuel": "Tanken",
"startEngine": "Motor starten",
"stopEngine": "Motor stoppen"
}
}
}

View File

@@ -30,7 +30,8 @@
}
},
"m-minigames": {
"match3": "Match 3 - Juwelen"
"match3": "Match 3 - Juwelen",
"taxi": "Taxi Simulator"
},
"m-settings": {
"homepage": "Startseite",
@@ -60,7 +61,8 @@
},
"minigames": "Minispiele",
"m-minigames": {
"match3": "Match3 Level"
"match3": "Match3 Level",
"taxiTools": "Taxi-Tools"
},
"chatrooms": "Chaträume"
},

View File

@@ -192,6 +192,34 @@
"totalUsers": "Total Users",
"genderDistribution": "Gender Distribution",
"ageDistribution": "Age Distribution"
},
"taxiTools": {
"title": "Taxi Tools",
"description": "Manage Taxi maps, levels and configurations",
"mapEditor": {
"title": "Edit Map",
"availableMaps": "Available Maps: {count}",
"newMap": "Create New Map",
"mapFormat": "{name} (Position: {x},{y})",
"mapName": "Map Name",
"mapDescription": "Description",
"mapWidth": "Width",
"mapHeight": "Height",
"tileSize": "Tile Size",
"positionX": "X Position",
"positionY": "Y Position",
"mapType": "Map Type",
"mapLayout": "Map Layout",
"tilePalette": "Tile Palette",
"position": "Position",
"fillAllRoads": "All Roads",
"clearAll": "Clear All",
"generateRandom": "Generate Random",
"delete": "Delete",
"update": "Update",
"cancel": "Cancel",
"create": "Create"
}
}
}
}

View File

@@ -33,6 +33,37 @@
"totalStars": "Total Stars",
"levelsCompleted": "Levels Completed",
"restartCampaign": "Restart Campaign"
},
"taxi": {
"title": "Taxi Simulator",
"description": "Drive passengers through the city and earn money!",
"gameStats": "Game Statistics",
"score": "Score",
"money": "Money",
"passengers": "Passengers",
"currentLevel": "Current Level",
"level": "Level",
"fuel": "Fuel",
"fuelLeft": "Fuel Left",
"restartLevel": "Restart Level",
"pause": "Pause",
"resume": "Resume",
"paused": "Game Paused",
"levelComplete": "Level Complete!",
"levelScore": "Level Score",
"moneyEarned": "Money Earned",
"passengersDelivered": "Passengers Delivered",
"nextLevel": "Next Level",
"campaignComplete": "Campaign Complete!",
"totalScore": "Total Score",
"totalMoney": "Total Money",
"levelsCompleted": "Levels Completed",
"restartCampaign": "Restart Campaign",
"pickupPassenger": "Pick up Passenger",
"deliverPassenger": "Deliver Passenger",
"refuel": "Refuel",
"startEngine": "Start Engine",
"stopEngine": "Stop Engine"
}
}
}

View File

@@ -30,7 +30,8 @@
}
},
"m-minigames": {
"match3": "Match 3 - Jewels"
"match3": "Match 3 - Jewels",
"taxi": "Taxi Simulator"
},
"m-settings": {
"homepage": "Homepage",
@@ -60,7 +61,8 @@
},
"minigames": "Mini games",
"m-minigames": {
"match3": "Match3 Levels"
"match3": "Match3 Levels",
"taxiTools": "Taxi Tools"
},
"chatrooms": "Chat rooms"
},

View File

@@ -5,6 +5,7 @@ import UserRightsView from '../views/admin/UserRightsView.vue';
import ForumAdminView from '../dialogues/admin/ForumAdminView.vue';
import AdminFalukantEditUserView from '../views/admin/falukant/EditUserView.vue';
import AdminMinigamesView from '../views/admin/MinigamesView.vue';
import AdminTaxiToolsView from '../views/admin/TaxiToolsView.vue';
import AdminUsersView from '../views/admin/UsersView.vue';
import UserStatisticsView from '../views/admin/UserStatisticsView.vue';
@@ -62,6 +63,12 @@ const adminRoutes = [
name: 'AdminMinigames',
component: AdminMinigamesView,
meta: { requiresAuth: true }
},
{
path: '/admin/minigames/taxi-tools',
name: 'AdminTaxiTools',
component: AdminTaxiToolsView,
meta: { requiresAuth: true }
}
];

View File

@@ -1,4 +1,5 @@
import Match3Game from '../views/minigames/Match3Game.vue';
import TaxiGame from '../views/minigames/TaxiGame.vue';
const minigamesRoutes = [
{
@@ -6,6 +7,12 @@ const minigamesRoutes = [
name: 'Match3Game',
component: Match3Game,
meta: { requiresAuth: true }
},
{
path: '/minigames/taxi',
name: 'TaxiGame',
component: TaxiGame,
meta: { requiresAuth: true }
}
];

View File

@@ -0,0 +1,141 @@
import streetData from '../data/streetCoordinates.json';
class StreetCoordinates {
constructor() {
this.data = streetData;
}
/**
* Konvertiert relative Koordinaten (0-1) zu absoluten Koordinaten
* @param {number} relativeX - Relative X-Koordinate (0-1)
* @param {number} relativeY - Relative Y-Koordinate (0-1)
* @param {number} tileSize - Größe des Tiles in Pixeln
* @returns {Object} Absolute Koordinaten {x, y}
*/
toAbsolute(relativeX, relativeY, tileSize) {
return {
x: Math.round(relativeX * tileSize),
y: Math.round(relativeY * tileSize)
};
}
/**
* Konvertiert absolute Koordinaten zu relativen Koordinaten (0-1)
* @param {number} absoluteX - Absolute X-Koordinate
* @param {number} absoluteY - Absolute Y-Koordinate
* @param {number} tileSize - Größe des Tiles in Pixeln
* @returns {Object} Relative Koordinaten {x, y}
*/
toRelative(absoluteX, absoluteY, tileSize) {
return {
x: absoluteX / tileSize,
y: absoluteY / tileSize
};
}
/**
* Gibt die Straßenregionen für einen Tile-Typ zurück
* @param {string} tileType - Typ des Tiles (z.B. 'cornerBottomRight')
* @param {number} tileSize - Größe des Tiles in Pixeln
* @returns {Array} Array von Polygonen mit absoluten Koordinaten
*/
getDriveableRegions(tileType, tileSize) {
const tile = this.data.tiles[tileType];
if (!tile) {
console.warn(`Tile type '${tileType}' not found`);
return [];
}
return tile.regions.map(region =>
region.map(point => this.toAbsolute(point.x, point.y, tileSize))
);
}
/**
* Prüft, ob ein Punkt innerhalb der fahrbaren Bereiche liegt
* @param {number} x - X-Koordinate des Punktes
* @param {number} y - Y-Koordinate des Punktes
* @param {string} tileType - Typ des Tiles
* @param {number} tileSize - Größe des Tiles in Pixeln
* @returns {boolean} True wenn der Punkt fahrbar ist
*/
isPointDriveable(x, y, tileType, tileSize) {
const regions = this.getDriveableRegions(tileType, tileSize);
for (const region of regions) {
if (this.isPointInPolygon(x, y, region)) {
return true;
}
}
return false;
}
/**
* Prüft, ob ein Punkt innerhalb eines Polygons liegt (Ray Casting Algorithm)
* @param {number} x - X-Koordinate des Punktes
* @param {number} y - Y-Koordinate des Punktes
* @param {Array} polygon - Array von {x, y} Punkten
* @returns {boolean} True wenn der Punkt im Polygon liegt
*/
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;
}
/**
* Zeichnet die Straßenregionen auf einem Canvas
* @param {CanvasRenderingContext2D} ctx - Canvas 2D Context
* @param {string} tileType - Typ des Tiles
* @param {number} tileSize - Größe des Tiles in Pixeln
* @param {number} offsetX - X-Offset für das Tile
* @param {number} offsetY - Y-Offset für das Tile
*/
drawDriveableRegions(ctx, tileType, tileSize, offsetX = 0, offsetY = 0) {
const regions = this.getDriveableRegions(tileType, tileSize);
ctx.fillStyle = '#f0f0f0'; // Straßenfarbe
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
for (const region of regions) {
ctx.beginPath();
ctx.moveTo(region[0].x + offsetX, region[0].y + offsetY);
for (let i = 1; i < region.length; i++) {
ctx.lineTo(region[i].x + offsetX, region[i].y + offsetY);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
}
/**
* Gibt alle verfügbaren Tile-Typen zurück
* @returns {Array} Array von Tile-Typen
*/
getAvailableTileTypes() {
return Object.keys(this.data.tiles);
}
/**
* Gibt die ursprüngliche Tile-Größe zurück
* @returns {number} Ursprüngliche Tile-Größe in Pixeln
*/
getOriginalTileSize() {
return this.data.tileSize;
}
}
// Singleton-Instanz
const streetCoordinates = new StreetCoordinates();
export default streetCoordinates;

File diff suppressed because it is too large Load Diff

View File

@@ -1,116 +0,0 @@
<template>
<div class="minigames-view">
<v-container>
<v-row>
<v-col cols="12">
<h1 class="text-h3 mb-6">{{ $t('minigames.title') }}</h1>
<p class="text-body-1 mb-8">{{ $t('minigames.description') }}</p>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6" lg="4">
<v-card
class="game-card"
@click="navigateToGame('match3')"
hover
elevation="4"
>
<v-img
src="/images/icons/game.png"
height="200"
cover
class="game-image"
></v-img>
<v-card-title class="text-h5">
{{ $t('minigames.match3.title') }}
</v-card-title>
<v-card-text>
{{ $t('minigames.match3.description') }}
</v-card-text>
<v-card-actions>
<v-btn
color="primary"
variant="elevated"
@click="navigateToGame('match3')"
>
{{ $t('minigames.play') }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<!-- Platzhalter für weitere Spiele -->
<v-col cols="12" md="6" lg="4">
<v-card class="game-card coming-soon" disabled>
<v-img
src="/images/icons/coming-soon.png"
height="200"
cover
class="game-image"
></v-img>
<v-card-title class="text-h5">
{{ $t('minigames.comingSoon.title') }}
</v-card-title>
<v-card-text>
{{ $t('minigames.comingSoon.description') }}
</v-card-text>
<v-card-actions>
<v-btn disabled>
{{ $t('minigames.comingSoon') }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
export default {
name: 'MinigamesView',
methods: {
navigateToGame(game) {
this.$router.push(`/minigames/${game}`);
}
}
}
</script>
<style scoped>
.minigames-view {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px 0;
}
.game-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
border-radius: 16px;
overflow: hidden;
}
.game-card:hover:not(.coming-soon) {
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0,0,0,0.3);
}
.game-image {
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
}
.coming-soon {
opacity: 0.7;
}
h1 {
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
p {
color: rgba(255,255,255,0.9);
}
</style>

File diff suppressed because it is too large Load Diff