diff --git a/backend/app.js b/backend/app.js index 58880ab..f10d657 100644 --- a/backend/app.js +++ b/backend/app.js @@ -13,6 +13,8 @@ import falukantRouter from './routers/falukantRouter.js'; import friendshipRouter from './routers/friendshipRouter.js'; import blogRouter from './routers/blogRouter.js'; import match3Router from './routers/match3Router.js'; +import taxiRouter from './routers/taxiRouter.js'; +import taxiMapRouter from './routers/taxiMapRouter.js'; import cors from 'cors'; import './jobs/sessionCleanup.js'; @@ -39,6 +41,8 @@ app.use('/api/navigation', navigationRouter); app.use('/api/settings', settingsRouter); app.use('/api/admin', adminRouter); app.use('/api/match3', match3Router); +app.use('/api/taxi', taxiRouter); +app.use('/api/taxi-maps', taxiMapRouter); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); app.use('/api/contact', contactRouter); app.use('/api/socialnetwork', socialnetworkRouter); diff --git a/backend/controllers/navigationController.js b/backend/controllers/navigationController.js index a3b152c..40186cb 100644 --- a/backend/controllers/navigationController.js +++ b/backend/controllers/navigationController.js @@ -174,6 +174,10 @@ const menuStructure = { match3: { visible: ["all"], path: "/minigames/match3" + }, + taxi: { + visible: ["all"], + path: "/minigames/taxi" } } }, @@ -274,6 +278,10 @@ const menuStructure = { match3: { visible: ["mainadmin", "match3"], path: "/admin/minigames/match3" + }, + taxiTools: { + visible: ["mainadmin", "taxi"], + path: "/admin/minigames/taxi-tools" } } } diff --git a/backend/controllers/taxiController.js b/backend/controllers/taxiController.js new file mode 100644 index 0000000..9f6b36a --- /dev/null +++ b/backend/controllers/taxiController.js @@ -0,0 +1,144 @@ +import TaxiService from '../services/taxiService.js'; + +function extractHashedUserId(req) { + return req.headers?.userid; +} + +class TaxiController { + constructor() { + this.taxiService = new TaxiService(); + } + + // Spielstand laden + async getGameState(req, res) { + try { + const hashedUserId = extractHashedUserId(req); + const gameState = await this.taxiService.getGameState(hashedUserId); + res.json({ success: true, data: gameState }); + } catch (error) { + console.error('Error getting taxi game state:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden des Spielstands' }); + } + } + + // Spielstand speichern + async saveGameState(req, res) { + try { + const userId = extractHashedUserId(req); + const { level, score, money, passengersDelivered, fuel } = req.body; + + const gameState = await this.taxiService.saveGameState(userId, { + level, + score, + money, + passengersDelivered, + fuel + }); + + res.json({ success: true, data: gameState }); + } catch (error) { + console.error('Error saving taxi game state:', error); + res.status(500).json({ success: false, message: 'Fehler beim Speichern des Spielstands' }); + } + } + + // Level-Statistiken abrufen + async getLevelStats(req, res) { + try { + const userId = extractHashedUserId(req); + const { level } = req.params; + + const stats = await this.taxiService.getLevelStats(userId, parseInt(level)); + res.json({ success: true, data: stats }); + } catch (error) { + console.error('Error getting level stats:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Level-Statistiken' }); + } + } + + // Bestenliste abrufen + async getLeaderboard(req, res) { + try { + const { type = 'score', limit = 10 } = req.query; + const leaderboard = await this.taxiService.getLeaderboard(type, parseInt(limit)); + res.json({ success: true, data: leaderboard }); + } catch (error) { + console.error('Error getting leaderboard:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Bestenliste' }); + } + } + + // Spiel beenden und Punkte verarbeiten + async finishGame(req, res) { + try { + const userId = extractHashedUserId(req); + const { finalScore, finalMoney, passengersDelivered, level } = req.body; + + const result = await this.taxiService.finishGame(userId, { + finalScore, + finalMoney, + passengersDelivered, + level + }); + + res.json({ success: true, data: result }); + } catch (error) { + console.error('Error finishing game:', error); + res.status(500).json({ success: false, message: 'Fehler beim Beenden des Spiels' }); + } + } + + // Level freischalten + async unlockLevel(req, res) { + try { + const userId = extractHashedUserId(req); + const { level } = req.body; + + const result = await this.taxiService.unlockLevel(userId, level); + res.json({ success: true, data: result }); + } catch (error) { + console.error('Error unlocking level:', error); + res.status(500).json({ success: false, message: 'Fehler beim Freischalten des Levels' }); + } + } + + // Spieler-Statistiken abrufen + async getPlayerStats(req, res) { + try { + const userId = extractHashedUserId(req); + const stats = await this.taxiService.getPlayerStats(userId); + res.json({ success: true, data: stats }); + } catch (error) { + console.error('Error getting player stats:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Spieler-Statistiken' }); + } + } + + // Level zurücksetzen + async resetLevel(req, res) { + try { + const userId = extractHashedUserId(req); + const { level } = req.body; + + const result = await this.taxiService.resetLevel(userId, level); + res.json({ success: true, data: result }); + } catch (error) { + console.error('Error resetting level:', error); + res.status(500).json({ success: false, message: 'Fehler beim Zurücksetzen des Levels' }); + } + } + + // Alle Spielstände zurücksetzen + async resetAllProgress(req, res) { + try { + const userId = extractHashedUserId(req); + const result = await this.taxiService.resetAllProgress(userId); + res.json({ success: true, data: result }); + } catch (error) { + console.error('Error resetting all progress:', error); + res.status(500).json({ success: false, message: 'Fehler beim Zurücksetzen aller Fortschritte' }); + } + } +} + +export default TaxiController; diff --git a/backend/controllers/taxiMapController.js b/backend/controllers/taxiMapController.js new file mode 100644 index 0000000..136b2ca --- /dev/null +++ b/backend/controllers/taxiMapController.js @@ -0,0 +1,144 @@ +import TaxiMapService from '../services/taxiMapService.js'; + +class TaxiMapController { + constructor() { + this.taxiMapService = new TaxiMapService(); + + // Bind all methods to the class instance + this.getMapTypes = this.getMapTypes.bind(this); + this.getMaps = this.getMaps.bind(this); + this.getMapById = this.getMapById.bind(this); + this.getMapByPosition = this.getMapByPosition.bind(this); + this.getDefaultMap = this.getDefaultMap.bind(this); + this.createMap = this.createMap.bind(this); + this.updateMap = this.updateMap.bind(this); + this.deleteMap = this.deleteMap.bind(this); + this.setDefaultMap = this.setDefaultMap.bind(this); + } + + async getMapTypes(req, res) { + try { + const mapTypes = await this.taxiMapService.getMapTypes(); + res.json({ success: true, data: mapTypes }); + } catch (error) { + console.error('Error getting map types:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Map-Typen' }); + } + } + + async getMaps(req, res) { + try { + const maps = await this.taxiMapService.getMaps(); + res.json({ success: true, data: maps }); + } catch (error) { + console.error('Error getting maps:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Maps' }); + } + } + + async getMapById(req, res) { + try { + const { mapId } = req.params; + const map = await this.taxiMapService.getMapById(mapId); + + if (!map) { + return res.status(404).json({ success: false, message: 'Map nicht gefunden' }); + } + + res.json({ success: true, data: map }); + } catch (error) { + console.error('Error getting map by ID:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Map' }); + } + } + + async getMapByPosition(req, res) { + try { + const { positionX, positionY } = req.params; + const map = await this.taxiMapService.getMapByPosition( + parseInt(positionX), + parseInt(positionY) + ); + + if (!map) { + return res.status(404).json({ success: false, message: 'Map an Position nicht gefunden' }); + } + + res.json({ success: true, data: map }); + } catch (error) { + console.error('Error getting map by position:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Map' }); + } + } + + async getDefaultMap(req, res) { + try { + const map = await this.taxiMapService.getDefaultMap(); + + if (!map) { + return res.status(404).json({ success: false, message: 'Keine Standard-Map gefunden' }); + } + + res.json({ success: true, data: map }); + } catch (error) { + console.error('Error getting default map:', error); + res.status(500).json({ success: false, message: 'Fehler beim Laden der Standard-Map' }); + } + } + + async createMap(req, res) { + try { + const mapData = req.body; + const map = await this.taxiMapService.createMap(mapData); + res.status(201).json({ success: true, data: map }); + } catch (error) { + console.error('Error creating map:', error); + res.status(500).json({ success: false, message: 'Fehler beim Erstellen der Map' }); + } + } + + async updateMap(req, res) { + try { + const { mapId } = req.params; + const updateData = req.body; + const map = await this.taxiMapService.updateMap(mapId, updateData); + res.json({ success: true, data: map }); + } catch (error) { + console.error('Error updating map:', error); + if (error.message === 'Map not found') { + return res.status(404).json({ success: false, message: 'Map nicht gefunden' }); + } + res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Map' }); + } + } + + async deleteMap(req, res) { + try { + const { mapId } = req.params; + await this.taxiMapService.deleteMap(mapId); + res.json({ success: true, message: 'Map erfolgreich gelöscht' }); + } catch (error) { + console.error('Error deleting map:', error); + if (error.message === 'Map not found') { + return res.status(404).json({ success: false, message: 'Map nicht gefunden' }); + } + res.status(500).json({ success: false, message: 'Fehler beim Löschen der Map' }); + } + } + + async setDefaultMap(req, res) { + try { + const { mapId } = req.params; + await this.taxiMapService.setDefaultMap(mapId); + res.json({ success: true, message: 'Standard-Map erfolgreich gesetzt' }); + } catch (error) { + console.error('Error setting default map:', error); + if (error.message === 'Map not found') { + return res.status(404).json({ success: false, message: 'Map nicht gefunden' }); + } + res.status(500).json({ success: false, message: 'Fehler beim Setzen der Standard-Map' }); + } + } +} + +export default TaxiMapController; diff --git a/backend/models/associations.js b/backend/models/associations.js index 3173f69..7c83d75 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -102,6 +102,10 @@ import Match3Level from './match3/level.js'; import Objective from './match3/objective.js'; import UserProgress from './match3/userProgress.js'; import UserLevelProgress from './match3/userLevelProgress.js'; +import TaxiGameState from './taxi/taxiGameState.js'; +import TaxiLevelStats from './taxi/taxiLevelStats.js'; +import TaxiMapType from './taxi/taxiMapType.js'; +import TaxiMap from './taxi/taxiMap.js'; export default function setupAssociations() { // RoomType 1:n Room @@ -786,4 +790,15 @@ export default function setupAssociations() { UserLevelProgress.belongsTo(UserProgress, { foreignKey: 'userProgressId', as: 'userProgress' }); Match3Level.hasMany(UserLevelProgress, { foreignKey: 'levelId', as: 'userLevelProgress' }); UserLevelProgress.belongsTo(Match3Level, { foreignKey: 'levelId', as: 'level' }); + + // Taxi Game associations + TaxiGameState.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + User.hasOne(TaxiGameState, { foreignKey: 'userId', as: 'taxiGameState' }); + + TaxiLevelStats.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + User.hasMany(TaxiLevelStats, { foreignKey: 'userId', as: 'taxiLevelStats' }); + + // Taxi Map associations + TaxiMap.belongsTo(TaxiMapType, { foreignKey: 'mapTypeId', as: 'mapType' }); + TaxiMapType.hasMany(TaxiMap, { foreignKey: 'mapTypeId', as: 'maps' }); } diff --git a/backend/models/index.js b/backend/models/index.js index f51a9e0..b8641d8 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -95,6 +95,9 @@ import Match3Objective from './match3/objective.js'; import Match3UserProgress from './match3/userProgress.js'; import Match3UserLevelProgress from './match3/userLevelProgress.js'; +// — Taxi Minigame — +import { TaxiGameState, TaxiLevelStats } from './taxi/index.js'; + // — Politische Ämter (Politics) — import PoliticalOfficeType from './falukant/type/political_office_type.js'; import PoliticalOfficeRequirement from './falukant/predefine/political_office_prerequisite.js'; @@ -230,6 +233,10 @@ const models = { Match3Objective, Match3UserProgress, Match3UserLevelProgress, + + // Taxi Minigame + TaxiGameState, + TaxiLevelStats, }; export default models; diff --git a/backend/models/taxi/index.js b/backend/models/taxi/index.js new file mode 100644 index 0000000..078e608 --- /dev/null +++ b/backend/models/taxi/index.js @@ -0,0 +1,6 @@ +import TaxiGameState from './taxiGameState.js'; +import TaxiLevelStats from './taxiLevelStats.js'; +import TaxiMapType from './taxiMapType.js'; +import TaxiMap from './taxiMap.js'; + +export { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap }; diff --git a/backend/models/taxi/taxiGameState.js b/backend/models/taxi/taxiGameState.js new file mode 100644 index 0000000..4bbca38 --- /dev/null +++ b/backend/models/taxi/taxiGameState.js @@ -0,0 +1,75 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiGameState = sequelize.define('TaxiGameState', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false + }, + currentLevel: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1 + }, + totalScore: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + totalMoney: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + totalPassengersDelivered: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + unlockedLevels: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: [1] + }, + achievements: { + type: DataTypes.JSON, + allowNull: false, + defaultValue: [] + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } +}, { + tableName: 'taxi_game_states', + schema: 'taxi', + timestamps: true, + indexes: [ + { + unique: true, + fields: ['user_id'] + }, + { + fields: ['total_score'] + }, + { + fields: ['total_money'] + }, + { + fields: ['total_passengers_delivered'] + } + ] +}); + +export default TaxiGameState; diff --git a/backend/models/taxi/taxiLevelStats.js b/backend/models/taxi/taxiLevelStats.js new file mode 100644 index 0000000..4977138 --- /dev/null +++ b/backend/models/taxi/taxiLevelStats.js @@ -0,0 +1,79 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiLevelStats = sequelize.define('TaxiLevelStats', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false + }, + level: { + type: DataTypes.INTEGER, + allowNull: false + }, + bestScore: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + bestMoney: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + bestPassengersDelivered: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + timesPlayed: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + completed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + }, + playTime: { + type: DataTypes.INTEGER, + allowNull: true, + comment: 'Play time in seconds' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } +}, { + tableName: 'taxi_level_stats', + schema: 'taxi', + timestamps: true, + indexes: [ + { + unique: true, + fields: ['user_id', 'level'] + }, + { + fields: ['level'] + }, + { + fields: ['best_score'] + }, + { + fields: ['completed'] + } + ] +}); + +export default TaxiLevelStats; diff --git a/backend/models/taxi/taxiMap.js b/backend/models/taxi/taxiMap.js new file mode 100644 index 0000000..90fa641 --- /dev/null +++ b/backend/models/taxi/taxiMap.js @@ -0,0 +1,101 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiMap = sequelize.define('TaxiMap', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING(100), + allowNull: false + }, + description: { + type: DataTypes.TEXT, + allowNull: true + }, + width: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 8, + comment: 'Map width in tiles' + }, + height: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 8, + comment: 'Map height in tiles' + }, + tileSize: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 50, + comment: 'Size of each tile in pixels' + }, + mapTypeId: { + type: DataTypes.INTEGER, + allowNull: false, + comment: 'Reference to TaxiMapType' + }, + mapData: { + type: DataTypes.JSON, + allowNull: false, + comment: '2D array of map type IDs for each tile position' + }, + positionX: { + type: DataTypes.INTEGER, + allowNull: false, + comment: 'X position as continuous integer (1, 2, 3, ...)' + }, + positionY: { + type: DataTypes.INTEGER, + allowNull: false, + comment: 'Y position as continuous integer (1, 2, 3, ...)' + }, + isActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, + isDefault: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + comment: 'Whether this is the default map for new games' + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } +}, { + tableName: 'taxi_maps', + schema: 'taxi', + timestamps: true, + indexes: [ + { + fields: ['name'] + }, + { + fields: ['is_active'] + }, + { + fields: ['is_default'] + }, + { + fields: ['position_x', 'position_y'] + }, + { + unique: true, + fields: ['position_x', 'position_y'] + } + ] +}); + +export default TaxiMap; diff --git a/backend/models/taxi/taxiMapType.js b/backend/models/taxi/taxiMapType.js new file mode 100644 index 0000000..3701947 --- /dev/null +++ b/backend/models/taxi/taxiMapType.js @@ -0,0 +1,57 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiMapType = sequelize.define('TaxiMapType', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.STRING(50), + allowNull: false, + unique: true + }, + description: { + type: DataTypes.TEXT, + allowNull: true + }, + tileType: { + type: DataTypes.STRING(50), + allowNull: false, + comment: 'Type of tile: cornerBottomLeft, cornerBottomRight, cornerTopLeft, cornerTopRight, horizontal, vertical, cross, fuelHorizontal, fuelVertical, tLeft, tRight, tUp, tDown' + }, + isActive: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } +}, { + tableName: 'taxi_map_types', + schema: 'taxi', + timestamps: true, + indexes: [ + { + unique: true, + fields: ['name'] + }, + { + fields: ['tile_type'] + }, + { + fields: ['is_active'] + } + ] +}); + +export default TaxiMapType; diff --git a/backend/routers/taxiMapRouter.js b/backend/routers/taxiMapRouter.js new file mode 100644 index 0000000..3c9ebc0 --- /dev/null +++ b/backend/routers/taxiMapRouter.js @@ -0,0 +1,26 @@ +import express from 'express'; +import TaxiMapController from '../controllers/taxiMapController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); +const taxiMapController = new TaxiMapController(); + +// All routes require authentication +router.use(authenticate); + +// Map types routes +router.get('/map-types', (req, res) => taxiMapController.getMapTypes(req, res)); + +// Maps routes +router.get('/maps', (req, res) => taxiMapController.getMaps(req, res)); +router.get('/maps/default', (req, res) => taxiMapController.getDefaultMap(req, res)); +router.get('/maps/position/:positionX/:positionY', (req, res) => taxiMapController.getMapByPosition(req, res)); +router.get('/maps/:mapId', (req, res) => taxiMapController.getMapById(req, res)); + +// Map management routes (admin only - you might want to add admin middleware) +router.post('/maps', (req, res) => taxiMapController.createMap(req, res)); +router.put('/maps/:mapId', (req, res) => taxiMapController.updateMap(req, res)); +router.delete('/maps/:mapId', (req, res) => taxiMapController.deleteMap(req, res)); +router.post('/maps/:mapId/set-default', (req, res) => taxiMapController.setDefaultMap(req, res)); + +export default router; diff --git a/backend/routers/taxiRouter.js b/backend/routers/taxiRouter.js new file mode 100644 index 0000000..4cabd0e --- /dev/null +++ b/backend/routers/taxiRouter.js @@ -0,0 +1,30 @@ +import express from 'express'; +import TaxiController from '../controllers/taxiController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); +const taxiController = new TaxiController(); + +// Alle Routen erfordern Authentifizierung +router.use(authenticate); + +// Spielstand-Routen +router.get('/game-state', (req, res) => taxiController.getGameState(req, res)); +router.post('/game-state', (req, res) => taxiController.saveGameState(req, res)); + +// Level-Routen +router.get('/level/:level/stats', (req, res) => taxiController.getLevelStats(req, res)); +router.post('/level/unlock', (req, res) => taxiController.unlockLevel(req, res)); +router.post('/level/reset', (req, res) => taxiController.resetLevel(req, res)); + +// Spiel-Routen +router.post('/finish', (req, res) => taxiController.finishGame(req, res)); + +// Statistik-Routen +router.get('/leaderboard', (req, res) => taxiController.getLeaderboard(req, res)); +router.get('/player-stats', (req, res) => taxiController.getPlayerStats(req, res)); + +// Reset-Routen +router.post('/reset-all', (req, res) => taxiController.resetAllProgress(req, res)); + +export default router; diff --git a/backend/services/taxiMapService.js b/backend/services/taxiMapService.js new file mode 100644 index 0000000..66ff785 --- /dev/null +++ b/backend/services/taxiMapService.js @@ -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; diff --git a/backend/services/taxiService.js b/backend/services/taxiService.js new file mode 100644 index 0000000..d62a9fa --- /dev/null +++ b/backend/services/taxiService.js @@ -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; diff --git a/backend/utils/initializeTaxi.js b/backend/utils/initializeTaxi.js new file mode 100644 index 0000000..a0111b8 --- /dev/null +++ b/backend/utils/initializeTaxi.js @@ -0,0 +1,29 @@ +// initializeTaxi.js + +import TaxiMapService from '../services/taxiMapService.js'; + +const initializeTaxi = async () => { + try { + console.log('Initializing Taxi game data...'); + + const taxiMapService = new TaxiMapService(); + + // Initialisiere Map-Typen + console.log('Initializing taxi map types...'); + await taxiMapService.initializeMapTypes(); + + // Prüfe ob bereits eine Standard-Map existiert + const existingDefaultMap = await taxiMapService.getDefaultMap(); + if (!existingDefaultMap) { + console.log('Creating default taxi map...'); + await taxiMapService.createDefaultMap(); + } + + console.log('Taxi game initialization complete.'); + } catch (error) { + console.error('Error initializing Taxi game:', error); + throw error; + } +}; + +export default initializeTaxi; diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index befb75d..a414ece 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -40,6 +40,7 @@ const createSchemas = async () => { await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_log'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS chat'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS match3'); + await sequelize.query('CREATE SCHEMA IF NOT EXISTS taxi'); }; const initializeDatabase = async () => { diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index 2873aa5..ba204ab 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -13,6 +13,7 @@ import initializeForum from './initializeForum.js'; import initializeChat from './initializeChat.js'; import initializeMatch3Data from './initializeMatch3.js'; import updateExistingMatch3Levels from './updateExistingMatch3Levels.js'; +import initializeTaxi from './initializeTaxi.js'; // Normale Synchronisation (nur bei STAGE=dev Schema-Updates) const syncDatabase = async () => { @@ -70,6 +71,9 @@ const syncDatabase = async () => { console.log("Updating existing Match3 levels..."); await updateExistingMatch3Levels(); + console.log("Initializing Taxi..."); + await initializeTaxi(); + console.log('Database synchronization complete.'); } catch (error) { console.error('Unable to synchronize the database:', error); @@ -85,19 +89,7 @@ const syncDatabaseForDeployment = async () => { console.log('✅ Deployment-Modus: Schema-Updates sind immer aktiviert'); console.log("Initializing database schemas..."); - // Nur Schemas erstellen, keine Model-Synchronisation - const { sequelize } = await import('./sequelize.js'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS community'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS type'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS service'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS forum'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_data'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_type'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_predefine'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_log'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS chat'); - await sequelize.query('CREATE SCHEMA IF NOT EXISTS match3'); + await initializeDatabase(); console.log("Synchronizing models with schema updates..."); await syncModelsAlways(models); @@ -137,6 +129,9 @@ const syncDatabaseForDeployment = async () => { console.log("Updating existing Match3 levels..."); await updateExistingMatch3Levels(); + console.log("Initializing Taxi..."); + await initializeTaxi(); + console.log('Database synchronization for deployment complete.'); } catch (error) { console.error('Unable to synchronize the database for deployment:', error); diff --git a/frontend/public/images/taxi/car_blue.svg b/frontend/public/images/taxi/car_blue.svg new file mode 100644 index 0000000..ad23f00 --- /dev/null +++ b/frontend/public/images/taxi/car_blue.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/car_green.svg b/frontend/public/images/taxi/car_green.svg new file mode 100644 index 0000000..53f132b --- /dev/null +++ b/frontend/public/images/taxi/car_green.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/car_pink.svg b/frontend/public/images/taxi/car_pink.svg new file mode 100644 index 0000000..edf29db --- /dev/null +++ b/frontend/public/images/taxi/car_pink.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/car_red.svg b/frontend/public/images/taxi/car_red.svg new file mode 100644 index 0000000..b66ac65 --- /dev/null +++ b/frontend/public/images/taxi/car_red.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/car_turquise.svg b/frontend/public/images/taxi/car_turquise.svg new file mode 100644 index 0000000..e2bfb17 --- /dev/null +++ b/frontend/public/images/taxi/car_turquise.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/cornerbottomleft.svg b/frontend/public/images/taxi/cornerbottomleft.svg new file mode 100644 index 0000000..b2ac2f8 --- /dev/null +++ b/frontend/public/images/taxi/cornerbottomleft.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/cornerbottomright.svg b/frontend/public/images/taxi/cornerbottomright.svg new file mode 100644 index 0000000..3caf709 --- /dev/null +++ b/frontend/public/images/taxi/cornerbottomright.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/cornertopleft.svg b/frontend/public/images/taxi/cornertopleft.svg new file mode 100644 index 0000000..1f19fdc --- /dev/null +++ b/frontend/public/images/taxi/cornertopleft.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/cornertopright.svg b/frontend/public/images/taxi/cornertopright.svg new file mode 100644 index 0000000..30b63bc --- /dev/null +++ b/frontend/public/images/taxi/cornertopright.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/cross.svg b/frontend/public/images/taxi/cross.svg new file mode 100644 index 0000000..c170c32 --- /dev/null +++ b/frontend/public/images/taxi/cross.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/fuelhorizontal.svg b/frontend/public/images/taxi/fuelhorizontal.svg new file mode 100644 index 0000000..0052b66 --- /dev/null +++ b/frontend/public/images/taxi/fuelhorizontal.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + 58,2 + + + + + 47,3 + + + + diff --git a/frontend/public/images/taxi/fuelvertical.svg b/frontend/public/images/taxi/fuelvertical.svg new file mode 100644 index 0000000..f859a73 --- /dev/null +++ b/frontend/public/images/taxi/fuelvertical.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 58,2 + + + + + 47,3 + + + + + + diff --git a/frontend/public/images/taxi/horizontal.svg b/frontend/public/images/taxi/horizontal.svg new file mode 100644 index 0000000..fd0368c --- /dev/null +++ b/frontend/public/images/taxi/horizontal.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + diff --git a/frontend/public/images/taxi/house.svg b/frontend/public/images/taxi/house.svg new file mode 100644 index 0000000..6211cfd --- /dev/null +++ b/frontend/public/images/taxi/house.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-cornerbottomleft.svg b/frontend/public/images/taxi/map-cornerbottomleft.svg new file mode 100644 index 0000000..cdf2a12 --- /dev/null +++ b/frontend/public/images/taxi/map-cornerbottomleft.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-cornerbottomright.svg b/frontend/public/images/taxi/map-cornerbottomright.svg new file mode 100644 index 0000000..585774d --- /dev/null +++ b/frontend/public/images/taxi/map-cornerbottomright.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-cornertopleft.svg b/frontend/public/images/taxi/map-cornertopleft.svg new file mode 100644 index 0000000..276171d --- /dev/null +++ b/frontend/public/images/taxi/map-cornertopleft.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-cornertopright.svg b/frontend/public/images/taxi/map-cornertopright.svg new file mode 100644 index 0000000..8eec9d0 --- /dev/null +++ b/frontend/public/images/taxi/map-cornertopright.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-cross.svg b/frontend/public/images/taxi/map-cross.svg new file mode 100644 index 0000000..9cc8c40 --- /dev/null +++ b/frontend/public/images/taxi/map-cross.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-fuelhorizontal.svg b/frontend/public/images/taxi/map-fuelhorizontal.svg new file mode 100644 index 0000000..c7cee41 --- /dev/null +++ b/frontend/public/images/taxi/map-fuelhorizontal.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-fuelvertical.svg b/frontend/public/images/taxi/map-fuelvertical.svg new file mode 100644 index 0000000..362906d --- /dev/null +++ b/frontend/public/images/taxi/map-fuelvertical.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-horizontal.svg b/frontend/public/images/taxi/map-horizontal.svg new file mode 100644 index 0000000..0b62bd5 --- /dev/null +++ b/frontend/public/images/taxi/map-horizontal.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-tdown.svg b/frontend/public/images/taxi/map-tdown.svg new file mode 100644 index 0000000..1d54a70 --- /dev/null +++ b/frontend/public/images/taxi/map-tdown.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-tleft.svg b/frontend/public/images/taxi/map-tleft.svg new file mode 100644 index 0000000..1c136bd --- /dev/null +++ b/frontend/public/images/taxi/map-tleft.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-tright.svg b/frontend/public/images/taxi/map-tright.svg new file mode 100644 index 0000000..c73bb59 --- /dev/null +++ b/frontend/public/images/taxi/map-tright.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-tup.svg b/frontend/public/images/taxi/map-tup.svg new file mode 100644 index 0000000..5099236 --- /dev/null +++ b/frontend/public/images/taxi/map-tup.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/map-vertical.svg b/frontend/public/images/taxi/map-vertical.svg new file mode 100644 index 0000000..2992653 --- /dev/null +++ b/frontend/public/images/taxi/map-vertical.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/person.svg b/frontend/public/images/taxi/person.svg new file mode 100644 index 0000000..2367894 --- /dev/null +++ b/frontend/public/images/taxi/person.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/radar.svg b/frontend/public/images/taxi/radar.svg new file mode 100644 index 0000000..41fd26e --- /dev/null +++ b/frontend/public/images/taxi/radar.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/redlight.svg b/frontend/public/images/taxi/redlight.svg new file mode 100644 index 0000000..662aa04 --- /dev/null +++ b/frontend/public/images/taxi/redlight.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/taxi.svg b/frontend/public/images/taxi/taxi.svg new file mode 100644 index 0000000..ae998ff --- /dev/null +++ b/frontend/public/images/taxi/taxi.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/tdown.svg b/frontend/public/images/taxi/tdown.svg new file mode 100644 index 0000000..a2dd0c8 --- /dev/null +++ b/frontend/public/images/taxi/tdown.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/ticket.svg b/frontend/public/images/taxi/ticket.svg new file mode 100644 index 0000000..be0911f --- /dev/null +++ b/frontend/public/images/taxi/ticket.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/tleft.svg b/frontend/public/images/taxi/tleft.svg new file mode 100644 index 0000000..442fb5f --- /dev/null +++ b/frontend/public/images/taxi/tleft.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/trafficlight-green.svg b/frontend/public/images/taxi/trafficlight-green.svg new file mode 100644 index 0000000..12ac636 --- /dev/null +++ b/frontend/public/images/taxi/trafficlight-green.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/trafficlight-red.svg b/frontend/public/images/taxi/trafficlight-red.svg new file mode 100644 index 0000000..a12028f --- /dev/null +++ b/frontend/public/images/taxi/trafficlight-red.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/trafficlight-redyellow.svg b/frontend/public/images/taxi/trafficlight-redyellow.svg new file mode 100644 index 0000000..553181e --- /dev/null +++ b/frontend/public/images/taxi/trafficlight-redyellow.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/trafficlight-yellow.svg b/frontend/public/images/taxi/trafficlight-yellow.svg new file mode 100644 index 0000000..cd1d1e7 --- /dev/null +++ b/frontend/public/images/taxi/trafficlight-yellow.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/tright.svg b/frontend/public/images/taxi/tright.svg new file mode 100644 index 0000000..4d8eb8b --- /dev/null +++ b/frontend/public/images/taxi/tright.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/tup.svg b/frontend/public/images/taxi/tup.svg new file mode 100644 index 0000000..aa7c57c --- /dev/null +++ b/frontend/public/images/taxi/tup.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + diff --git a/frontend/public/images/taxi/vertical.svg b/frontend/public/images/taxi/vertical.svg new file mode 100644 index 0000000..f4ce03b --- /dev/null +++ b/frontend/public/images/taxi/vertical.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + diff --git a/frontend/src/data/streetCoordinates.json b/frontend/src/data/streetCoordinates.json new file mode 100644 index 0000000..d2f2316 --- /dev/null +++ b/frontend/src/data/streetCoordinates.json @@ -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} + ] + ] + } + } +} diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index 76ff4e5..0c4c66b 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -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" + } } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/de/minigames.json b/frontend/src/i18n/locales/de/minigames.json index a524129..75c2876 100644 --- a/frontend/src/i18n/locales/de/minigames.json +++ b/frontend/src/i18n/locales/de/minigames.json @@ -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" } } } diff --git a/frontend/src/i18n/locales/de/navigation.json b/frontend/src/i18n/locales/de/navigation.json index 5244793..30cfc1d 100644 --- a/frontend/src/i18n/locales/de/navigation.json +++ b/frontend/src/i18n/locales/de/navigation.json @@ -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" }, diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index d53f2a4..42f6055 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -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" + } } } } \ No newline at end of file diff --git a/frontend/src/i18n/locales/en/minigames.json b/frontend/src/i18n/locales/en/minigames.json index 05be013..3bf11ad 100644 --- a/frontend/src/i18n/locales/en/minigames.json +++ b/frontend/src/i18n/locales/en/minigames.json @@ -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" } } } diff --git a/frontend/src/i18n/locales/en/navigation.json b/frontend/src/i18n/locales/en/navigation.json index 46c4f33..43d7c24 100644 --- a/frontend/src/i18n/locales/en/navigation.json +++ b/frontend/src/i18n/locales/en/navigation.json @@ -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" }, diff --git a/frontend/src/router/adminRoutes.js b/frontend/src/router/adminRoutes.js index 1841705..bd2547e 100644 --- a/frontend/src/router/adminRoutes.js +++ b/frontend/src/router/adminRoutes.js @@ -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 } } ]; diff --git a/frontend/src/router/minigamesRoutes.js b/frontend/src/router/minigamesRoutes.js index 492d303..e78f7c6 100644 --- a/frontend/src/router/minigamesRoutes.js +++ b/frontend/src/router/minigamesRoutes.js @@ -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 } } ]; diff --git a/frontend/src/utils/streetCoordinates.js b/frontend/src/utils/streetCoordinates.js new file mode 100644 index 0000000..7ef18f8 --- /dev/null +++ b/frontend/src/utils/streetCoordinates.js @@ -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; diff --git a/frontend/src/views/admin/TaxiToolsView.vue b/frontend/src/views/admin/TaxiToolsView.vue new file mode 100644 index 0000000..1be8455 --- /dev/null +++ b/frontend/src/views/admin/TaxiToolsView.vue @@ -0,0 +1,1093 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/minigames/MinigamesView.vue b/frontend/src/views/minigames/MinigamesView.vue deleted file mode 100644 index dd339d8..0000000 --- a/frontend/src/views/minigames/MinigamesView.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue new file mode 100644 index 0000000..b1de570 --- /dev/null +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -0,0 +1,1135 @@ + + + + +