Ä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,269 @@
import BaseService from './BaseService.js';
import TaxiMap from '../models/taxi/taxiMap.js';
import TaxiMapType from '../models/taxi/taxiMapType.js';
class TaxiMapService extends BaseService {
constructor() {
super();
}
/**
* Holt alle verfügbaren Map-Typen
*/
async getMapTypes() {
try {
const mapTypes = await TaxiMapType.findAll({
where: { isActive: true },
order: [['name', 'ASC']]
});
return mapTypes;
} catch (error) {
console.error('Error getting map types:', error);
throw error;
}
}
/**
* Holt alle verfügbaren Maps
*/
async getMaps() {
try {
const maps = await TaxiMap.findAll({
where: { isActive: true },
include: [{
model: TaxiMapType,
as: 'mapType'
}],
order: [['positionY', 'ASC'], ['positionX', 'ASC']]
});
return maps;
} catch (error) {
console.error('Error getting maps:', error);
throw error;
}
}
/**
* Holt eine spezifische Map
*/
async getMapById(mapId) {
try {
const map = await TaxiMap.findOne({
where: {
id: mapId,
isActive: true
},
include: [{
model: TaxiMapType,
as: 'mapType'
}]
});
return map;
} catch (error) {
console.error('Error getting map by ID:', error);
throw error;
}
}
/**
* Holt eine Map nach Position
*/
async getMapByPosition(positionX, positionY) {
try {
const map = await TaxiMap.findOne({
where: {
positionX: positionX,
positionY: positionY,
isActive: true
},
include: [{
model: TaxiMapType,
as: 'mapType'
}]
});
return map;
} catch (error) {
console.error('Error getting map by position:', error);
throw error;
}
}
/**
* Holt die Standard-Map
*/
async getDefaultMap() {
try {
const map = await TaxiMap.findOne({
where: {
isDefault: true,
isActive: true
},
include: [{
model: TaxiMapType,
as: 'mapType'
}]
});
return map;
} catch (error) {
console.error('Error getting default map:', error);
throw error;
}
}
/**
* Erstellt eine neue Map
*/
async createMap(mapData) {
try {
const map = await TaxiMap.create(mapData);
return map;
} catch (error) {
console.error('Error creating map:', error);
throw error;
}
}
/**
* Aktualisiert eine Map
*/
async updateMap(mapId, updateData) {
try {
const [updatedRowsCount] = await TaxiMap.update(updateData, {
where: { id: mapId }
});
if (updatedRowsCount === 0) {
throw new Error('Map not found');
}
return await this.getMapById(mapId);
} catch (error) {
console.error('Error updating map:', error);
throw error;
}
}
/**
* Löscht eine Map (soft delete)
*/
async deleteMap(mapId) {
try {
const [updatedRowsCount] = await TaxiMap.update(
{ isActive: false },
{ where: { id: mapId } }
);
if (updatedRowsCount === 0) {
throw new Error('Map not found');
}
return { success: true };
} catch (error) {
console.error('Error deleting map:', error);
throw error;
}
}
/**
* Setzt eine Map als Standard
*/
async setDefaultMap(mapId) {
try {
// Entferne Standard-Status von allen anderen Maps
await TaxiMap.update(
{ isDefault: false },
{ where: { isDefault: true } }
);
// Setze neue Standard-Map
const [updatedRowsCount] = await TaxiMap.update(
{ isDefault: true },
{ where: { id: mapId } }
);
if (updatedRowsCount === 0) {
throw new Error('Map not found');
}
return { success: true };
} catch (error) {
console.error('Error setting default map:', error);
throw error;
}
}
/**
* Initialisiert Standard-Map-Typen
*/
async initializeMapTypes() {
try {
const mapTypes = [
{ name: 'Corner Bottom Left', tileType: 'cornerBottomLeft', description: 'Bottom left corner tile' },
{ name: 'Corner Bottom Right', tileType: 'cornerBottomRight', description: 'Bottom right corner tile' },
{ name: 'Corner Top Left', tileType: 'cornerTopLeft', description: 'Top left corner tile' },
{ name: 'Corner Top Right', tileType: 'cornerTopRight', description: 'Top right corner tile' },
{ name: 'Horizontal', tileType: 'horizontal', description: 'Horizontal road tile' },
{ name: 'Vertical', tileType: 'vertical', description: 'Vertical road tile' },
{ name: 'Cross', tileType: 'cross', description: 'Cross intersection tile' },
{ name: 'Fuel Horizontal', tileType: 'fuelHorizontal', description: 'Horizontal road with fuel station' },
{ name: 'Fuel Vertical', tileType: 'fuelVertical', description: 'Vertical road with fuel station' },
{ name: 'T-Left', tileType: 'tLeft', description: 'T-junction facing left' },
{ name: 'T-Right', tileType: 'tRight', description: 'T-junction facing right' },
{ name: 'T-Up', tileType: 'tUp', description: 'T-junction facing up' },
{ name: 'T-Down', tileType: 'tDown', description: 'T-junction facing down' }
];
for (const mapType of mapTypes) {
await TaxiMapType.findOrCreate({
where: { name: mapType.name },
defaults: mapType
});
}
console.log('Taxi map types initialized');
} catch (error) {
console.error('Error initializing map types:', error);
throw error;
}
}
/**
* Erstellt eine Standard-Map
*/
async createDefaultMap() {
try {
// 8x8 Standard-Map mit verschiedenen Tile-Typen
const mapData = [
['cornerTopLeft', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'cornerTopRight'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['vertical', 'cross', 'cross', 'cross', 'cross', 'cross', 'cross', 'vertical'],
['cornerBottomLeft', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'horizontal', 'cornerBottomRight']
];
const map = await TaxiMap.create({
name: 'Standard City Map',
description: 'A standard 8x8 city map with roads and intersections',
width: 8,
height: 8,
tileSize: 50,
mapTypeId: 1, // Assuming first map type
mapData: mapData,
positionX: 1,
positionY: 1,
isDefault: true,
isActive: true
});
return map;
} catch (error) {
console.error('Error creating default map:', error);
throw error;
}
}
}
export default TaxiMapService;

View File

@@ -0,0 +1,407 @@
import BaseService from './BaseService.js';
import TaxiGameState from '../models/taxi/taxiGameState.js';
import TaxiLevelStats from '../models/taxi/taxiLevelStats.js';
import User from '../models/community/user.js';
class TaxiService extends BaseService {
constructor() {
super();
}
// Hilfsmethode: Konvertiere hashedId zu userId
async getUserIdFromHashedId(hashedUserId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
return user.id;
}
// Spielstand abrufen
async getGameState(hashedUserId) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
let gameState = await TaxiGameState.findOne({
where: { userId }
});
if (!gameState) {
// Erstelle neuen Spielstand
gameState = await TaxiGameState.create({
userId,
currentLevel: 1,
totalScore: 0,
totalMoney: 0,
totalPassengersDelivered: 0,
unlockedLevels: [1],
achievements: []
});
}
return {
currentLevel: gameState.currentLevel,
totalScore: gameState.totalScore,
totalMoney: gameState.totalMoney,
totalPassengersDelivered: gameState.totalPassengersDelivered,
unlockedLevels: gameState.unlockedLevels,
achievements: gameState.achievements
};
} catch (error) {
console.error('Error getting taxi game state:', error);
throw error;
}
}
// Spielstand speichern
async saveGameState(hashedUserId, gameData) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const { level, score, money, passengersDelivered, fuel } = gameData;
const [gameState, created] = await TaxiGameState.findOrCreate({
where: { userId },
defaults: {
userId,
currentLevel: level || 1,
totalScore: score || 0,
totalMoney: money || 0,
totalPassengersDelivered: passengersDelivered || 0,
unlockedLevels: [1],
achievements: []
}
});
if (!created) {
// Aktualisiere bestehenden Spielstand
await gameState.update({
currentLevel: Math.max(gameState.currentLevel, level || 1),
totalScore: Math.max(gameState.totalScore, score || 0),
totalMoney: Math.max(gameState.totalMoney, money || 0),
totalPassengersDelivered: Math.max(gameState.totalPassengersDelivered, passengersDelivered || 0)
});
}
return {
currentLevel: gameState.currentLevel,
totalScore: gameState.totalScore,
totalMoney: gameState.totalMoney,
totalPassengersDelivered: gameState.totalPassengersDelivered,
unlockedLevels: gameState.unlockedLevels,
achievements: gameState.achievements
};
} catch (error) {
console.error('Error saving taxi game state:', error);
throw error;
}
}
// Level-Statistiken abrufen
async getLevelStats(hashedUserId, level) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const stats = await TaxiLevelStats.findOne({
where: { userId, level }
});
if (!stats) {
return {
level,
bestScore: 0,
bestMoney: 0,
bestPassengersDelivered: 0,
timesPlayed: 0,
completed: false
};
}
return {
level: stats.level,
bestScore: stats.bestScore,
bestMoney: stats.bestMoney,
bestPassengersDelivered: stats.bestPassengersDelivered,
timesPlayed: stats.timesPlayed,
completed: stats.completed
};
} catch (error) {
console.error('Error getting level stats:', error);
throw error;
}
}
// Bestenliste abrufen
async getLeaderboard(type = 'score', limit = 10) {
try {
let orderBy;
switch (type) {
case 'money':
orderBy = [['totalMoney', 'DESC']];
break;
case 'passengers':
orderBy = [['totalPassengersDelivered', 'DESC']];
break;
case 'level':
orderBy = [['currentLevel', 'DESC']];
break;
default:
orderBy = [['totalScore', 'DESC']];
}
const leaderboard = await TaxiGameState.findAll({
order: orderBy,
limit: parseInt(limit),
include: [{
model: User,
attributes: ['username', 'id']
}]
});
return leaderboard.map((entry, index) => ({
rank: index + 1,
username: entry.User.username,
userId: entry.User.id,
score: entry.totalScore,
money: entry.totalMoney,
passengers: entry.totalPassengersDelivered,
level: entry.currentLevel
}));
} catch (error) {
console.error('Error getting leaderboard:', error);
throw error;
}
}
// Spiel beenden und Punkte verarbeiten
async finishGame(hashedUserId, gameData) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const { finalScore, finalMoney, passengersDelivered, level } = gameData;
// Aktualisiere Spielstand
const gameState = await this.saveGameState(userId, {
level,
score: finalScore,
money: finalMoney,
passengersDelivered
});
// Aktualisiere Level-Statistiken
await this.updateLevelStats(hashedUserId, level, {
score: finalScore,
money: finalMoney,
passengersDelivered
});
// Prüfe auf neue freigeschaltete Level
const newUnlockedLevels = await this.checkUnlockedLevels(hashedUserId, level);
// Prüfe auf neue Erfolge
const newAchievements = await this.checkAchievements(hashedUserId, gameState);
return {
gameState,
newUnlockedLevels,
newAchievements
};
} catch (error) {
console.error('Error finishing game:', error);
throw error;
}
}
// Level freischalten
async unlockLevel(hashedUserId, level) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const gameState = await TaxiGameState.findOne({
where: { userId }
});
if (!gameState) {
throw new Error('Spielstand nicht gefunden');
}
const unlockedLevels = [...gameState.unlockedLevels];
if (!unlockedLevels.includes(level)) {
unlockedLevels.push(level);
unlockedLevels.sort((a, b) => a - b);
await gameState.update({
unlockedLevels
});
}
return { unlockedLevels };
} catch (error) {
console.error('Error unlocking level:', error);
throw error;
}
}
// Spieler-Statistiken abrufen
async getPlayerStats(hashedUserId) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const gameState = await this.getGameState(hashedUserId);
const levelStats = await TaxiLevelStats.findAll({
where: { userId },
order: [['level', 'ASC']]
});
const totalLevelsPlayed = levelStats.length;
const completedLevels = levelStats.filter(stat => stat.completed).length;
const totalPlayTime = levelStats.reduce((sum, stat) => sum + (stat.playTime || 0), 0);
return {
...gameState,
totalLevelsPlayed,
completedLevels,
totalPlayTime,
levelStats: levelStats.map(stat => ({
level: stat.level,
bestScore: stat.bestScore,
bestMoney: stat.bestMoney,
bestPassengersDelivered: stat.bestPassengersDelivered,
timesPlayed: stat.timesPlayed,
completed: stat.completed
}))
};
} catch (error) {
console.error('Error getting player stats:', error);
throw error;
}
}
// Level zurücksetzen
async resetLevel(hashedUserId, level) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
await TaxiLevelStats.destroy({
where: { userId, level }
});
return { success: true };
} catch (error) {
console.error('Error resetting level:', error);
throw error;
}
}
// Alle Spielstände zurücksetzen
async resetAllProgress(hashedUserId) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
await TaxiGameState.destroy({
where: { userId }
});
await TaxiLevelStats.destroy({
where: { userId }
});
return { success: true };
} catch (error) {
console.error('Error resetting all progress:', error);
throw error;
}
}
// Hilfsmethoden
async updateLevelStats(hashedUserId, level, stats) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const { score, money, passengersDelivered } = stats;
const [levelStat, created] = await TaxiLevelStats.findOrCreate({
where: { userId, level },
defaults: {
userId,
level,
bestScore: score,
bestMoney: money,
bestPassengersDelivered: passengersDelivered,
timesPlayed: 1,
completed: true
}
});
if (!created) {
const updates = {
timesPlayed: levelStat.timesPlayed + 1,
completed: true
};
if (score > levelStat.bestScore) updates.bestScore = score;
if (money > levelStat.bestMoney) updates.bestMoney = money;
if (passengersDelivered > levelStat.bestPassengersDelivered) {
updates.bestPassengersDelivered = passengersDelivered;
}
await levelStat.update(updates);
}
} catch (error) {
console.error('Error updating level stats:', error);
throw error;
}
}
async checkUnlockedLevels(hashedUserId, currentLevel) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const gameState = await TaxiGameState.findOne({
where: { userId }
});
if (!gameState) return [];
const newUnlockedLevels = [];
const nextLevel = currentLevel + 1;
if (!gameState.unlockedLevels.includes(nextLevel)) {
newUnlockedLevels.push(nextLevel);
await this.unlockLevel(hashedUserId, nextLevel);
}
return newUnlockedLevels;
} catch (error) {
console.error('Error checking unlocked levels:', error);
return [];
}
}
async checkAchievements(hashedUserId, gameState) {
try {
const userId = await this.getUserIdFromHashedId(hashedUserId);
const achievements = [];
const currentAchievements = gameState.achievements || [];
// Beispiel-Erfolge
if (gameState.totalScore >= 1000 && !currentAchievements.includes('score_1000')) {
achievements.push('score_1000');
}
if (gameState.totalPassengersDelivered >= 50 && !currentAchievements.includes('passengers_50')) {
achievements.push('passengers_50');
}
if (gameState.currentLevel >= 10 && !currentAchievements.includes('level_10')) {
achievements.push('level_10');
}
if (achievements.length > 0) {
const newAchievements = [...currentAchievements, ...achievements];
await TaxiGameState.update(
{ achievements: newAchievements },
{ where: { userId } }
);
}
return achievements;
} catch (error) {
console.error('Error checking achievements:', error);
return [];
}
}
}
export default TaxiService;