diff --git a/backend/models/associations.js b/backend/models/associations.js index 7c83d75..350d460 100644 --- a/backend/models/associations.js +++ b/backend/models/associations.js @@ -106,6 +106,9 @@ import TaxiGameState from './taxi/taxiGameState.js'; import TaxiLevelStats from './taxi/taxiLevelStats.js'; import TaxiMapType from './taxi/taxiMapType.js'; import TaxiMap from './taxi/taxiMap.js'; +import TaxiMapTile from './taxi/taxiMapTile.js'; +import TaxiStreetName from './taxi/taxiStreetName.js'; +import TaxiMapTileStreet from './taxi/taxiMapTileStreet.js'; export default function setupAssociations() { // RoomType 1:n Room @@ -801,4 +804,15 @@ export default function setupAssociations() { // Taxi Map associations TaxiMap.belongsTo(TaxiMapType, { foreignKey: 'mapTypeId', as: 'mapType' }); TaxiMapType.hasMany(TaxiMap, { foreignKey: 'mapTypeId', as: 'maps' }); + + // Tiles + TaxiMap.hasMany(TaxiMapTile, { foreignKey: 'mapId', as: 'tiles' }); + TaxiMapTile.belongsTo(TaxiMap, { foreignKey: 'mapId', as: 'map' }); + + // Street name models + TaxiMapTileStreet.belongsTo(TaxiMap, { foreignKey: 'map_id', as: 'map' }); + TaxiMap.hasMany(TaxiMapTileStreet, { foreignKey: 'map_id', as: 'tileStreets' }); + + TaxiMapTileStreet.belongsTo(TaxiStreetName, { foreignKey: 'street_name_h_id', as: 'streetNameH' }); + TaxiMapTileStreet.belongsTo(TaxiStreetName, { foreignKey: 'street_name_v_id', as: 'streetNameV' }); } diff --git a/backend/models/chat/rights.js b/backend/models/chat/rights.js index 939a07d..26089b2 100644 --- a/backend/models/chat/rights.js +++ b/backend/models/chat/rights.js @@ -5,14 +5,18 @@ const ChatRight = sequelize.define('ChatRight', { id: { type: DataTypes.INTEGER, primaryKey: true, - autoIncrement: true}, + autoIncrement: true + }, tr: { type: DataTypes.STRING(32), allowNull: false, - unique: true}}, { + unique: true + } +}, { schema: 'chat', tableName: 'rights', timestamps: false, - underscored: true}); + underscored: true +}); export default ChatRight; diff --git a/backend/models/index.js b/backend/models/index.js index fa81a33..3db1b1f 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -96,9 +96,7 @@ import Match3UserProgress from './match3/userProgress.js'; import Match3UserLevelProgress from './match3/userLevelProgress.js'; // — Taxi Minigame — -import { TaxiGameState, TaxiLevelStats } from './taxi/index.js'; -import TaxiMap from './taxi/taxiMap.js'; -import TaxiMapType from './taxi/taxiMapType.js'; +import { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap, TaxiMapTile, TaxiStreetName, TaxiMapTileStreet } from './taxi/index.js'; // — Politische Ämter (Politics) — import PoliticalOfficeType from './falukant/type/political_office_type.js'; @@ -241,6 +239,9 @@ const models = { TaxiLevelStats, TaxiMap, TaxiMapType, + TaxiMapTile, + TaxiStreetName, + TaxiMapTileStreet, }; export default models; diff --git a/backend/models/taxi/index.js b/backend/models/taxi/index.js index 078e608..a846b52 100644 --- a/backend/models/taxi/index.js +++ b/backend/models/taxi/index.js @@ -2,5 +2,8 @@ import TaxiGameState from './taxiGameState.js'; import TaxiLevelStats from './taxiLevelStats.js'; import TaxiMapType from './taxiMapType.js'; import TaxiMap from './taxiMap.js'; +import TaxiMapTile from './taxiMapTile.js'; +import TaxiStreetName from './taxiStreetName.js'; +import TaxiMapTileStreet from './taxiMapTileStreet.js'; -export { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap }; +export { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap, TaxiMapTile, TaxiStreetName, TaxiMapTileStreet }; diff --git a/backend/models/taxi/taxiGameState.js b/backend/models/taxi/taxiGameState.js index 0991d96..4ed061f 100644 --- a/backend/models/taxi/taxiGameState.js +++ b/backend/models/taxi/taxiGameState.js @@ -41,20 +41,12 @@ const TaxiGameState = sequelize.define('TaxiGameState', { allowNull: false, defaultValue: [] }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW - } + // createdAt/updatedAt via timestamps }, { tableName: 'taxi_game_state', schema: 'taxi', timestamps: true, + underscored: true, indexes: [ { unique: true, diff --git a/backend/models/taxi/taxiLevelStats.js b/backend/models/taxi/taxiLevelStats.js index c23da16..23bd34d 100644 --- a/backend/models/taxi/taxiLevelStats.js +++ b/backend/models/taxi/taxiLevelStats.js @@ -45,20 +45,12 @@ const TaxiLevelStats = sequelize.define('TaxiLevelStats', { 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 - } + // createdAt/updatedAt via timestamps }, { tableName: 'taxi_level_stat', schema: 'taxi', timestamps: true, + underscored: true, indexes: [ { unique: true, diff --git a/backend/models/taxi/taxiMap.js b/backend/models/taxi/taxiMap.js index f9f7aff..a7914cb 100644 --- a/backend/models/taxi/taxiMap.js +++ b/backend/models/taxi/taxiMap.js @@ -38,11 +38,6 @@ const TaxiMap = sequelize.define('TaxiMap', { allowNull: false, comment: 'Reference to TaxiMapType' }, - mapData: { - type: DataTypes.JSON, - allowNull: false, - comment: '2D array of map type IDs for each tile position' - }, isActive: { type: DataTypes.BOOLEAN, allowNull: false, @@ -54,20 +49,12 @@ const TaxiMap = sequelize.define('TaxiMap', { 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 - } + // createdAt/updatedAt via timestamps }, { tableName: 'taxi_map', schema: 'taxi', timestamps: true, + underscored: true, indexes: [ { fields: ['name'] diff --git a/backend/models/taxi/taxiMapTile.js b/backend/models/taxi/taxiMapTile.js new file mode 100644 index 0000000..e3f61c5 --- /dev/null +++ b/backend/models/taxi/taxiMapTile.js @@ -0,0 +1,44 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiMapTile = sequelize.define('TaxiMapTile', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + mapId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + x: { + type: DataTypes.INTEGER, + allowNull: false, + }, + y: { + type: DataTypes.INTEGER, + allowNull: false, + }, + tileType: { + type: DataTypes.STRING(50), + allowNull: false, + }, + meta: { + type: DataTypes.JSON, + allowNull: true, + } +}, { + tableName: 'taxi_map_tile', + schema: 'taxi', + timestamps: true, + underscored: true, + indexes: [ + { unique: true, fields: ['map_id','x','y'] }, + { fields: ['map_id'] }, + { fields: ['tile_type'] }, + ] +}); + +export default TaxiMapTile; + + diff --git a/backend/models/taxi/taxiMapTileStreet.js b/backend/models/taxi/taxiMapTileStreet.js new file mode 100644 index 0000000..a9e1245 --- /dev/null +++ b/backend/models/taxi/taxiMapTileStreet.js @@ -0,0 +1,45 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiMapTileStreet = sequelize.define('TaxiMapTileStreet', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + mapId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + x: { + type: DataTypes.INTEGER, + allowNull: false, + }, + y: { + type: DataTypes.INTEGER, + allowNull: false, + }, + streetNameHId: { + type: DataTypes.INTEGER, + allowNull: true, + }, + streetNameVId: { + type: DataTypes.INTEGER, + allowNull: true, + } +}, { + tableName: 'taxi_map_tile_street', + schema: 'taxi', + timestamps: true, + underscored: true, + indexes: [ + { unique: true, fields: ['map_id','x','y'] }, + { fields: ['map_id'] }, + { fields: ['street_name_h_id'] }, + { fields: ['street_name_v_id'] } + ] +}); + +export default TaxiMapTileStreet; + + diff --git a/backend/models/taxi/taxiMapType.js b/backend/models/taxi/taxiMapType.js index 688d07c..efe606d 100644 --- a/backend/models/taxi/taxiMapType.js +++ b/backend/models/taxi/taxiMapType.js @@ -26,20 +26,12 @@ const TaxiMapType = sequelize.define('TaxiMapType', { allowNull: false, defaultValue: true }, - createdAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW - }, - updatedAt: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW - } + // createdAt/updatedAt via timestamps }, { tableName: 'taxi_map_type', schema: 'taxi', timestamps: true, + underscored: true, indexes: [ { unique: true, diff --git a/backend/models/taxi/taxiStreetName.js b/backend/models/taxi/taxiStreetName.js new file mode 100644 index 0000000..6d4052b --- /dev/null +++ b/backend/models/taxi/taxiStreetName.js @@ -0,0 +1,24 @@ +import { DataTypes } from 'sequelize'; +import { sequelize } from '../../utils/sequelize.js'; + +const TaxiStreetName = sequelize.define('TaxiStreetName', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING(255), + allowNull: false, + unique: true, + } +}, { + tableName: 'taxi_street_name', + schema: 'taxi', + timestamps: true, + underscored: true, +}); + +export default TaxiStreetName; + + diff --git a/backend/services/taxiMapService.js b/backend/services/taxiMapService.js index 72dec58..32851e1 100644 --- a/backend/services/taxiMapService.js +++ b/backend/services/taxiMapService.js @@ -1,6 +1,9 @@ import BaseService from './BaseService.js'; import TaxiMap from '../models/taxi/taxiMap.js'; import TaxiMapType from '../models/taxi/taxiMapType.js'; +import TaxiMapTile from '../models/taxi/taxiMapTile.js'; +import TaxiStreetName from '../models/taxi/taxiStreetName.js'; +import TaxiMapTileStreet from '../models/taxi/taxiMapTileStreet.js'; class TaxiMapService extends BaseService { constructor() { @@ -30,10 +33,18 @@ class TaxiMapService extends BaseService { try { const maps = await TaxiMap.findAll({ where: { isActive: true }, - include: [{ - model: TaxiMapType, - as: 'mapType' - }], + include: [ + { model: TaxiMapType, as: 'mapType' }, + { model: TaxiMapTile, as: 'tiles' }, + { + model: TaxiMapTileStreet, + as: 'tileStreets', + include: [ + { model: TaxiStreetName, as: 'streetNameH' }, + { model: TaxiStreetName, as: 'streetNameV' } + ] + } + ], order: [['name', 'ASC']] }); return maps; @@ -53,10 +64,18 @@ class TaxiMapService extends BaseService { id: mapId, isActive: true }, - include: [{ - model: TaxiMapType, - as: 'mapType' - }] + include: [ + { model: TaxiMapType, as: 'mapType' }, + { model: TaxiMapTile, as: 'tiles' }, + { + model: TaxiMapTileStreet, + as: 'tileStreets', + include: [ + { model: TaxiStreetName, as: 'streetNameH' }, + { model: TaxiStreetName, as: 'streetNameV' } + ] + } + ] }); return map; } catch (error) { @@ -93,8 +112,18 @@ class TaxiMapService extends BaseService { */ async createMap(mapData) { try { - const map = await TaxiMap.create(mapData); - return map; + const { tiles, tileStreetNames, ...mapFields } = mapData; + // mapData JSON ist entfernt – map erstellen ohne mapData + const map = await TaxiMap.create(mapFields); + // Tiles upsert (optional) + if (Array.isArray(tiles) && tiles.length > 0) { + await this.upsertTiles(map.id, tiles); + } + // Street names (optional) + if (tileStreetNames && Object.keys(tileStreetNames).length > 0) { + await this.upsertTileStreetNames(map.id, tileStreetNames); + } + return await this.getMapById(map.id); } catch (error) { console.error('Error creating map:', error); throw error; @@ -106,7 +135,8 @@ class TaxiMapService extends BaseService { */ async updateMap(mapId, updateData) { try { - const [updatedRowsCount] = await TaxiMap.update(updateData, { + const { tiles, tileStreetNames, ...mapFields } = updateData; + const [updatedRowsCount] = await TaxiMap.update(mapFields, { where: { id: mapId } }); @@ -114,6 +144,13 @@ class TaxiMapService extends BaseService { throw new Error('Map not found'); } + // Tiles upsert (optional) + if (Array.isArray(tiles)) { + await this.upsertTiles(mapId, tiles); + } + if (tileStreetNames && Object.keys(tileStreetNames).length > 0) { + await this.upsertTileStreetNames(mapId, tileStreetNames); + } return await this.getMapById(mapId); } catch (error) { console.error('Error updating map:', error); @@ -121,6 +158,53 @@ class TaxiMapService extends BaseService { } } + /** + * Speichert Straßennamen-Belegungen für eine Map + * @param {number} mapId + * @param {{[cellKey:string]:{streetNameH?:string, streetNameV?:string}}} tileNames + */ + async upsertTileStreetNames(mapId, tileNames) { + const entries = Object.entries(tileNames || {}); + for (const [cellKey, data] of entries) { + const [x, y] = cellKey.split(',').map(Number); + const record = { mapId, x, y }; + // Street names: findOrCreate + if (data.streetNameH) { + const [snH] = await TaxiStreetName.findOrCreate({ where: { name: data.streetNameH }, defaults: { name: data.streetNameH } }); + record.streetNameHId = snH.id; + } else { + record.streetNameHId = null; + } + if (data.streetNameV) { + const [snV] = await TaxiStreetName.findOrCreate({ where: { name: data.streetNameV }, defaults: { name: data.streetNameV } }); + record.streetNameVId = snV.id; + } else { + record.streetNameVId = null; + } + + const [row] = await TaxiMapTileStreet.findOrCreate({ where: { mapId, x, y }, defaults: record }); + await row.update(record); + } + return { success: true }; + } + + /** + * Upsert Tiles (x,y,tileType,meta?) pro Map + * Erwartet: tiles: Array<{x:number,y:number,tileType:string, meta?:object}> + */ + async upsertTiles(mapId, tiles) { + for (const tile of tiles) { + const { x, y, tileType, meta } = tile; + if (typeof x !== 'number' || typeof y !== 'number' || !tileType) continue; + const [row] = await TaxiMapTile.findOrCreate({ + where: { mapId, x, y }, + defaults: { mapId, x, y, tileType, meta: meta || null } + }); + await row.update({ tileType, meta: meta || null }); + } + return { success: true }; + } + /** * Löscht eine Map (soft delete) */ diff --git a/backend/utils/initializeTaxi.js b/backend/utils/initializeTaxi.js index a0111b8..446e4d2 100644 --- a/backend/utils/initializeTaxi.js +++ b/backend/utils/initializeTaxi.js @@ -1,6 +1,7 @@ // initializeTaxi.js import TaxiMapService from '../services/taxiMapService.js'; +import { sequelize } from './sequelize.js'; const initializeTaxi = async () => { try { @@ -8,6 +9,85 @@ const initializeTaxi = async () => { const taxiMapService = new TaxiMapService(); + // Stelle sicher, dass die neue Tabelle taxi_map_tile existiert (vor Zugriffen) + try { + await (await import('../models/taxi/taxiMapTile.js')).default.sync({ alter: true, force: false }); + console.log('✅ Tabelle taxi.taxi_map_tile ist synchronisiert'); + } catch (e) { + console.warn('⚠️ Konnte taxi_map_tile nicht synchronisieren:', e?.message || e); + } + + // Stelle sicher: timestamps-Spalten in taxi_street_name vorhanden (ältere DBs hatten evtl. kein updated_at) + try { + await sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'taxi' AND table_name = 'taxi_street_name' AND column_name = 'created_at' + ) THEN + ALTER TABLE taxi.taxi_street_name ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(); + END IF; + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'taxi' AND table_name = 'taxi_street_name' AND column_name = 'updated_at' + ) THEN + ALTER TABLE taxi.taxi_street_name ADD COLUMN updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(); + END IF; + END + $$; + `); + console.log('✅ timestamps-Spalten für taxi.taxi_street_name sichergestellt'); + } catch (e) { + console.warn('⚠️ Konnte timestamps-Spalten für taxi.taxi_street_name nicht sicherstellen:', e?.message || e); + } + + // Stelle sicher: timestamps-Spalten in taxi_map_tile_street vorhanden + try { + await sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile_street' AND column_name = 'created_at' + ) THEN + ALTER TABLE taxi.taxi_map_tile_street ADD COLUMN created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(); + END IF; + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile_street' AND column_name = 'updated_at' + ) THEN + ALTER TABLE taxi.taxi_map_tile_street ADD COLUMN updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(); + END IF; + END + $$; + `); + console.log('✅ timestamps-Spalten für taxi.taxi_map_tile_street sichergestellt'); + } catch (e) { + console.warn('⚠️ Konnte timestamps-Spalten für taxi.taxi_map_tile_street nicht sicherstellen:', e?.message || e); + } + + // Entferne veraltete Spalte taxi.taxi_map.map_data (Übergang von JSON → relational) + try { + const exists = await sequelize.query( + `SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'taxi' + AND table_name = 'taxi_map' + AND column_name = 'map_data' + ) AS exists;`, + { type: sequelize.QueryTypes.SELECT } + ); + const hasColumn = Array.isArray(exists) ? (exists[0]?.exists === true || exists[0]?.exists === 't') : false; + if (hasColumn) { + console.log('🔧 Entferne veraltete Spalte taxi.taxi_map.map_data ...'); + await sequelize.query(`ALTER TABLE taxi.taxi_map DROP COLUMN IF EXISTS map_data;`); + console.log('✅ Spalte map_data entfernt'); + } + } catch (e) { + console.warn('⚠️ Konnte Spalte map_data nicht prüfen/entfernen (nicht kritisch):', e?.message || e); + } + // Initialisiere Map-Typen console.log('Initializing taxi map types...'); await taxiMapService.initializeMapTypes(); diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index a414ece..03ac962 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -121,7 +121,7 @@ const checkSchemaUpdates = async (models) => { const schemas = [ 'community', 'logs', 'type', 'service', 'forum', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', - 'chat', 'match3' + 'chat', 'match3', 'taxi' ]; let needsUpdate = false; diff --git a/frontend/src/assets/motorSound.js b/frontend/src/assets/motorSound.js index 42e1069..db32fc4 100644 --- a/frontend/src/assets/motorSound.js +++ b/frontend/src/assets/motorSound.js @@ -55,8 +55,12 @@ class MotorSound { } stop() { - if (!this.gainNode) return; - this.gainNode.disconnect(this.context.destination); + if (!this.gainNode || !this.gainNode.context) return; + try { + this.gainNode.disconnect(this.context.destination); + } catch (_) { + // bereits disconnected + } this.isPlaying = false; } diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index 0d764c6..8d250ad 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -211,6 +211,13 @@ "mapType": "Map-Typ", "mapLayout": "Map-Layout", "tilePalette": "Tile-Palette", + "streetNames": "Straßennamen", + "extraElements": "Zusätzliche Elemente", + "streetNameHorizontal": "Straßenname (horizontal)", + "streetNameVertical": "Straßenname (vertikal)", + "continueHorizontal": "In anderer Richtung fortführen (→)", + "continueVertical": "In anderer Richtung fortführen (↓)", + "continueOther": "In anderer Richtung fortführen", "position": "Position", "fillAllRoads": "Alle Straßen", "clearAll": "Alle löschen", diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index 2d2d7b8..0caf239 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -211,6 +211,13 @@ "mapType": "Map Type", "mapLayout": "Map Layout", "tilePalette": "Tile Palette", + "streetNames": "Street names", + "extraElements": "Additional elements", + "streetNameHorizontal": "Street name (horizontal)", + "streetNameVertical": "Street name (vertical)", + "continueHorizontal": "Continue in other direction (→)", + "continueVertical": "Continue in other direction (↓)", + "continueOther": "Continue in other direction", "position": "Position", "fillAllRoads": "All Roads", "clearAll": "Clear All", diff --git a/frontend/src/router/minigamesRoutes.js b/frontend/src/router/minigamesRoutes.js index e78f7c6..26b1162 100644 --- a/frontend/src/router/minigamesRoutes.js +++ b/frontend/src/router/minigamesRoutes.js @@ -1,5 +1,6 @@ -import Match3Game from '../views/minigames/Match3Game.vue'; -import TaxiGame from '../views/minigames/TaxiGame.vue'; +// Lazy-Loading verhindert Circular Imports/HMR-Probleme +const Match3Game = () => import('../views/minigames/Match3Game.vue'); +const TaxiGame = () => import('../views/minigames/TaxiGame.vue'); const minigamesRoutes = [ { diff --git a/frontend/src/views/admin/TaxiToolsView.vue b/frontend/src/views/admin/TaxiToolsView.vue index 638cc78..b372679 100644 --- a/frontend/src/views/admin/TaxiToolsView.vue +++ b/frontend/src/views/admin/TaxiToolsView.vue @@ -94,7 +94,7 @@ -
+

{{ $t('admin.taxiTools.mapEditor.tilePalette') }}

+ + +
+

{{ $t('admin.taxiTools.mapEditor.streetNames') }}

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+

{{ $t('admin.taxiTools.mapEditor.extraElements') }}

+
+
@@ -188,7 +216,7 @@ -
+

{{ $t('admin.taxiTools.mapEditor.tilePalette') }}

+ + +
+

{{ $t('admin.taxiTools.mapEditor.streetNames') }}

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+

{{ $t('admin.taxiTools.mapEditor.extraElements') }}

+
+
@@ -254,7 +310,7 @@ export default { mapForm: { name: '', description: '', - mapData: [] + // mapData entfernt – Tiles werden separat gesendet }, boardCells: {}, // { "x,y": { tileType: "horizontal", x: 0, y: 0 } } availableTileTypes: [ @@ -286,6 +342,38 @@ export default { if (Object.keys(this.boardCells).length === 0) return 0; return this.maxY - this.minY + 1; }, + selectedCell() { + if (!this.selectedCellKey) return null; + return this.boardCells[this.selectedCellKey] || null; + }, + showStreetNamePanel() { + // Panel nur anzeigen, wenn Zelle selektiert ist und der Tile-Typ Namen unterstützt + if (!this.selectedCell) return false; + const type = this.selectedCell.tileType; + return this.supportsHorizontal(type) || this.supportsVertical(type); + }, + showExtraElementsPanel() { + // Gleiche Logik wie Straßenpanel – nur sichtbar, wenn eine Zelle selektiert ist + return !!this.selectedCell; + }, + supportsHorizontalName() { + if (!this.selectedCell) return false; + return this.supportsHorizontal(this.selectedCell.tileType); + }, + supportsVerticalName() { + if (!this.selectedCell) return false; + return this.supportsVertical(this.selectedCell.tileType); + }, + showContinueOtherButton() { + if (!this.selectedCell) return false; + const hAllowed = this.supportsHorizontalName; + const vAllowed = this.supportsVerticalName; + if (!(hAllowed && vAllowed)) return false; + const hasH = !!this.selectedCell.streetNameH; + const hasV = !!this.selectedCell.streetNameV; + // genau einer gesetzt + return (hasH && !hasV) || (!hasH && hasV); + }, minX() { if (Object.keys(this.boardCells).length === 0) return 0; @@ -352,6 +440,170 @@ export default { }, methods: { + supportsHorizontal(tileType) { + // Horizontalen Straßennamen erlauben bei: horizontal, cross, tleft, tright, tup, tdown, corns wo horizontaler Ast existiert + const allowed = ['horizontal', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelhorizontal']; + return allowed.includes(tileType); + }, + supportsVertical(tileType) { + // Vertikalen Straßennamen erlauben bei: vertical, cross, tup, tdown, etc. + const allowed = ['vertical', 'cross', 'tleft', 'tright', 'tup', 'tdown', 'cornertopleft', 'cornerbottomleft', 'cornertopright', 'cornerbottomright', 'fuelvertical']; + return allowed.includes(tileType); + }, + continueStreetNames() { + if (!this.selectedCell) return; + // Wenn nur einer der beiden Namen existiert, kopiere in das andere Feld + const hasH = !!this.selectedCell.streetNameH; + const hasV = !!this.selectedCell.streetNameV; + if (hasH && !hasV) { + const value = this.selectedCell.streetNameH; + this.onChangeStreetName('v', value); + } else if (!hasH && hasV) { + const value = this.selectedCell.streetNameV; + this.onChangeStreetName('h', value); + } + }, + onChangeStreetName(dir, value) { + if (!this.selectedCell) return; + const key = dir === 'h' ? 'streetNameH' : 'streetNameV'; + // Vue reaktiv: bestehendes Objekt erweitern + const updated = { ...(this.boardCells[this.selectedCellKey] || {}), [key]: value }; + this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated); + // Namen entlang der Achse bis zu Abschluss-Tiles fortführen + this.propagateStreetNameChain(this.selectedCellKey, dir, value); + }, + continueStreetName(dir) { + if (!this.selectedCell) return; + const name = dir === 'h' ? (this.selectedCell.streetNameH || '') : (this.selectedCell.streetNameV || ''); + if (!name) return; + this.propagateStreetNameChain(this.selectedCellKey, dir, name); + }, + + // Führt Namen entlang der Achse fort, bis ein Abschluss-Tile erreicht ist + propagateStreetNameChain(startKey, dir, name) { + const start = this.boardCells[startKey]; + if (!start || !start.tileType) return; + const { x, y } = start; + const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV'; + + const walk = (sx, sy, sign) => { + let cx = sx; + let cy = sy; + while (true) { + if (dir === 'h') cx += (sign === 'right' ? 1 : -1); else cy += (sign === 'down' ? 1 : -1); + const key = `${cx},${cy}`; + const cell = this.boardCells[key]; + if (!cell || !cell.tileType) break; + const status = this.classifyNeighbor(dir, sign, cell.tileType); + if (status === 'none') break; + const upd = { ...cell, [keyField]: name }; + this.$set ? this.$set(this.boardCells, key, upd) : (this.boardCells[key] = upd); + if (status === 'terminal') break; + } + }; + + const dirs = this.allowedDirections(start.tileType); + if (dir === 'h') { + if (dirs.right) walk(x, y, 'right'); + if (dirs.left) walk(x, y, 'left'); + } else { + if (dirs.down) walk(x, y, 'down'); + if (dirs.up) walk(x, y, 'up'); + } + }, + + classifyNeighbor(dir, sign, type) { + const isCornerRight = (t) => /corner.*right$/.test(t); + const isCornerLeft = (t) => /corner.*left$/.test(t); + const isCornerTop = (t) => /cornertop.*$/.test(t); + const isCornerBottom= (t) => /cornerbottom.*$/.test(t); + if (dir === 'h') { + // horizontal folgen können: cornerright, horizontal, tdown, tup, cross, fuelhorizontal, sowie cornerleft und tleft als Abschluss + if (sign === 'right') { + if (isCornerLeft(type) || type === 'tleft') return 'terminal'; + if (isCornerRight(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue'; + return 'none'; + } else { // left + if (isCornerRight(type) || type === 'tright') return 'terminal'; + if (isCornerLeft(type) || ['horizontal','tdown','tup','cross','fuelhorizontal'].includes(type)) return 'continue'; + return 'none'; + } + } else { + // vertikal können folgen: cornerbottom, vertical, tleft, tright, cross, fuelvertical, cornerup + // Abschluss bei Richtung down: cornerup und tup (wir setzen noch, führen aber nicht weiter) + if (sign === 'down') { + if (isCornerTop(type) || type === 'tup') return 'terminal'; + if (isCornerBottom(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue'; + return 'none'; + } else { // up + if (isCornerBottom(type) || type === 'tdown') return 'terminal'; + if (isCornerTop(type) || ['vertical','tleft','tright','cross','fuelvertical'].includes(type)) return 'continue'; + return 'none'; + } + } + }, + + allowedDirections(tileType) { + // Gibt zurück, in welche Richtungen dieses Tile Anschlüsse hat + switch (tileType) { + case 'cornertopleft': return { left: true, up: true, right: false, down: false }; + case 'cornertopright': return { right: true, up: true, left: false, down: false }; + case 'cornerbottomleft': return { left: true, down: true, right: false, up: false }; + case 'cornerbottomright': return { right: true, down: true, left: false, up: false }; + case 'horizontal': + case 'fuelhorizontal': return { left: true, right: true, up: false, down: false }; + case 'vertical': + case 'fuelvertical': return { up: true, down: true, left: false, right: false }; + case 'cross': return { left: true, right: true, up: true, down: true }; + case 'tup': return { up: true, left: true, right: true, down: false }; + case 'tdown': return { down: true, left: true, right: true, up: false }; + case 'tleft': return { left: true, up: true, down: true, right: false }; + case 'tright': return { right: true, up: true, down: true, left: false }; + default: return { left: false, right: false, up: false, down: false }; + } + }, + + propagateStreetNameFrom(startKey, dir, name) { + const keyField = dir === 'h' ? 'streetNameH' : 'streetNameV'; + const step = dir === 'h' ? { dx: 1, dy: 0 } : { dx: 0, dy: 1 }; + const oppositeStep = { dx: -step.dx, dy: -step.dy }; + // in positive Richtung fortführen + this.walkAndSet(startKey, step, dir, keyField, name); + // in negative Richtung fortführen + this.walkAndSet(startKey, oppositeStep, dir, keyField, name); + }, + + walkAndSet(startKey, step, dir, keyField, name) { + const canContinue = (tileType, dir) => { + switch (tileType) { + case 'horizontal': return dir === 'h'; + case 'vertical': return dir === 'v'; + case 'cross': return true; + case 'fuelhorizontal': return dir === 'h'; + case 'fuelvertical': return dir === 'v'; + case 'cornertopleft': + case 'cornertopright': + case 'cornerbottomleft': + case 'cornerbottomright': + return false; // Kurve blockt die Fortführung + case 'tup': return dir === 'h'; + case 'tdown': return dir === 'h'; + case 'tleft': return dir === 'v'; + case 'tright': return dir === 'v'; + default: return false; + } + }; + let [x, y] = startKey.split(',').map(Number); + while (true) { + x += step.dx; y += step.dy; + const key = `${x},${y}`; + const cell = this.boardCells[key]; + if (!cell || !cell.tileType) break; + if (!canContinue(cell.tileType, dir)) break; + const updated = { ...cell, [keyField]: name }; + this.$set ? this.$set(this.boardCells, key, updated) : (this.boardCells[key] = updated); + } + }, async loadMaps() { try { const user = this.$store.getters.user; @@ -402,11 +654,27 @@ export default { this.mapForm = { name: map.name, description: map.description, - mapData: map.mapData || [] }; - - if (map.mapData && map.mapData.length > 0) { - this.createBoardFromData(map.mapData); + // Falls Tiles aus Backend mitkommen, Board daraus erstellen + if (Array.isArray(map.tiles) && map.tiles.length > 0) { + this.boardCells = {}; + for (const t of map.tiles) { + const key = `${t.x},${t.y}`; + this.boardCells[key] = { tileType: t.tileType, x: t.x, y: t.y }; + } + // Straßennamen aus tileStreets übernehmen + if (Array.isArray(map.tileStreets)) { + for (const s of map.tileStreets) { + const key = `${s.x},${s.y}`; + if (this.boardCells[key]) { + this.boardCells[key] = { + ...this.boardCells[key], + streetNameH: s.streetNameH?.name || null, + streetNameV: s.streetNameV?.name || null + }; + } + } + } } else { this.initializeBoard(); } @@ -433,9 +701,8 @@ export default { // Wenn eine Position ausgewählt ist, platziere das Tile if (this.selectedCellKey !== null) { this.placeTile(this.selectedCellKey, tileType); - // Setze Auswahl zurück nach dem Platzieren - this.selectedCellKey = null; - this.selectedTileType = null; + // Nach dem Setzen bleibt die Zelle ausgewählt + // Tile-Typ-Auswahl bleibt erhalten, um mehrere gleiche Tiles zu setzen } }, @@ -453,6 +720,9 @@ export default { y: y }; + // Nach dem Setzen: Zelle ausgewählt lassen + this.selectedCellKey = cellKey; + // Erweitere das Board um benachbarte leere Zellen falls nötig this.expandBoardIfNeeded(x, y, tileType); }, @@ -681,13 +951,19 @@ export default { async saveMap() { try { + // Tiles in Array-Form bringen + const tiles = Object.values(this.boardCells) + .filter(c => !!c.tileType) + .map(c => ({ x: c.x, y: c.y, tileType: c.tileType })); + const tileStreetNames = this.collectTileStreetNames(); const mapData = { ...this.mapForm, width: this.boardWidth, height: this.boardHeight, tileSize: 50, - mapTypeId: 1, // Standard Map-Typ - mapData: this.generateMapData() + mapTypeId: 1, + tiles, + tileStreetNames }; let savedMap; @@ -718,6 +994,18 @@ export default { } }, + collectTileStreetNames() { + const result = {}; + for (const [key, cell] of Object.entries(this.boardCells)) { + if (!cell.tileType) continue; + const { streetNameH, streetNameV } = cell; + if (streetNameH || streetNameV) { + result[key] = { streetNameH: streetNameH || null, streetNameV: streetNameV || null }; + } + } + return result; + }, + async deleteMap(mapId) { if (confirm('Möchtest du diese Map wirklich löschen?')) { try { @@ -941,6 +1229,44 @@ export default { flex: 0 0 300px; } +/* Vereinheitlichte Tool-Panels rechts */ +.tool-panel { + flex: 0 0 220px; +} +.tool-panel h4 { + margin: 0 0 8px 0; /* weniger Abstand oben/unten */ + color: #F9A22C; + font-size: 18px; +} +.panel-body.placeholder { + border: 2px dashed #ddd; + border-radius: 8px; + padding: 8px; + color: #999; + text-align: center; +} +.form-group-mini { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} +.arrow-label { + width: 28px; + text-align: center; + font-weight: 600; +} +.form-group-mini input { + flex: 1; + padding: 6px 8px; + border: 1px solid #ccc; + border-radius: 6px; +} +.btn.mini { + padding: 6px 10px; + font-size: 12px; +} + .tile-palette h4 { margin-bottom: 15px; color: #F9A22C; @@ -953,7 +1279,7 @@ export default { grid-template-rows: repeat(4, 50px); gap: 5px; border: 2px solid #ddd; - padding: 10px; + padding: 6px; /* weniger Innenabstand, damit oben/unten kompakter */ background: #f9f9f9; border-radius: 8px; } diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 872dabb..5a0f4e3 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -43,6 +43,22 @@
  • Vermeide Kollisionen mit anderen Fahrzeugen
  • + + +
    +

    Straßennamen

    +
      +
    • + {{ item.num }}: {{ item.name }} +
    • +
    +
    @@ -228,7 +244,40 @@ export default { keys: {}, motorSound: null, audioContext: null, - audioUnlockHandler: null + audioUnlockHandler: null, + selectedStreetName: null + } + }, + computed: { + toggleIcon() { + return this.isStatsExpanded ? '−' : '+'; + }, + streetLegend() { + // Sammle alle Straßennamen aus currentMap.tileStreets und vergebe laufende Nummern + try { + const legend = []; + if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return legend; + const nameToNum = new Map(); + let counter = 1; + for (const ts of this.currentMap.tileStreets) { + if (ts.streetNameH && ts.streetNameH.name) { + const n = ts.streetNameH.name; + if (!nameToNum.has(n)) nameToNum.set(n, counter++); + } + if (ts.streetNameV && ts.streetNameV.name) { + const n = ts.streetNameV.name; + if (!nameToNum.has(n)) nameToNum.set(n, counter++); + } + } + // Sortiert nach zugewiesener Nummer + const entries = Array.from(nameToNum.entries()).sort((a,b) => a[1]-b[1]); + for (const [name, num] of entries) { + legend.push({ num, name }); + } + return legend; + } catch (e) { + return []; + } } }, async mounted() { @@ -245,6 +294,74 @@ export default { this.cleanup(); }, methods: { + // Hilfsfunktion: Liefert laufende Nummer für gegebenen Straßennamen + getStreetNumberByName(name) { + const item = this.streetLegend.find(it => it.name === name); + return item ? item.num : null; + }, + + // Zeichnet die Straßen-Nr. auf die Minimap, je nach Tile-Typ und Position (pro Name nur einmal) + drawStreetNumbersOnMinimap(ctx, x, y, size, tileType, col, row, drawnNames) { + if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return; + + // Finde etwaige StreetName-Einträge für dieses Tile + const entry = this.currentMap.tileStreets.find(ts => ts.x === col && ts.y === row); + if (!entry) return; + + ctx.save(); + const baseFontSize = Math.max(9, Math.floor(size * 0.22) - 1); + ctx.font = `${baseFontSize}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const left = x + size * 0.2; + const right = x + size * 0.8; + const top = y + size * 0.2; + const bottom = y + size * 0.8; + + const hName = entry.streetNameH?.name || null; + const vName = entry.streetNameV?.name || null; + const hNum = hName ? this.getStreetNumberByName(hName) : null; + const vNum = vName ? this.getStreetNumberByName(vName) : null; + + const t = tileType; + // Regeln gemäß Vorgabe + // Horizontal linke Seite: + const hLeftTiles = new Set(['cornerbottomleft','cornertopleft','horizontal','tdown','tup','cross','fuelhorizontal','tleft']); + // Horizontal rechte Seite: + const hRightTiles = new Set(['cornerbottomright','cornertopright','tright']); + // Vertical oben: + const vTopTiles = new Set(['cornertopleft','cornertopright','vertical','tup','tleft','tright','cross','fuelvertical']); + // Vertical unten: + const vBottomTiles = new Set(['cornerbottomleft','cornerbottomright','tdown']); + + // Zusätzliche Kanten-spezifische Regeln aus der Einleitung + // cornerbottomright: horizontal rechts, vertical unten + // cornerbottomleft: horizontal links, vertical unten + // ... ist bereits über Sets abgedeckt + + if (hNum !== null && hName && !drawnNames.has(hName)) { + const hx = hRightTiles.has(t) ? right : left; + const hy = y + size * 0.5 + 1; // 1px nach unten + const isSelH = (this.selectedStreetName && this.selectedStreetName === hName); + ctx.fillStyle = isSelH ? '#ffffff' : '#111'; + ctx.font = isSelH ? `bold ${baseFontSize}px sans-serif` : `${baseFontSize}px sans-serif`; + ctx.fillText(String(hNum), hx, hy); + drawnNames.add(hName); + } + + if (vNum !== null && vName && !drawnNames.has(vName)) { + const vx = x + size * 0.5; + const vy = vTopTiles.has(t) ? top : (vBottomTiles.has(t) ? bottom : top); + const isSelV = (this.selectedStreetName && this.selectedStreetName === vName); + ctx.fillStyle = isSelV ? '#ffffff' : '#111'; + ctx.font = isSelV ? `bold ${baseFontSize}px sans-serif` : `${baseFontSize}px sans-serif`; + ctx.fillText(String(vNum), vx, vy); + drawnNames.add(vName); + } + + ctx.restore(); + }, // Mapping zwischen Spiel-Tile-Namen (lowercase) und streetCoordinates.json (camelCase) mapTileTypeToStreetCoordinates(tileType) { const mapping = { @@ -256,7 +373,11 @@ export default { 'vertical': 'vertical', 'cross': 'cross', 'fuelhorizontal': 'fuelHorizontal', - 'fuelvertical': 'fuelVertical' + 'fuelvertical': 'fuelVertical', + 'tleft': 'tLeft', + 'tright': 'tRight', + 'tup': 'tUp', + 'tdown': 'tDown' }; return mapping[tileType] || tileType; }, @@ -588,9 +709,9 @@ export default { this.lastSteerChange = currentTime; } - // Aktualisiere Position (1/4 der ursprünglichen Bewegung) - this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.1; - this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.1; + // Aktualisiere Position (+20% Geschwindigkeit) + this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.12; + this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.12; // Aktualisiere aktuelle Tile-Position this.updateCurrentTile(); @@ -995,9 +1116,12 @@ export default { if (this.tiles.images[tileType]) { this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize); } + // Straßennummern auf Haupt-Canvas zeichnen + this.drawStreetNumbersOnMainCanvas(this.ctx, tileSize, tileType, this.currentTile.col, this.currentTile.row); } } - } else { + } + else { // Fallback: Zeichne Standard-Tile wenn keine Map geladen ist const tileType = 'cornertopleft'; // Standard-Tile const streetTileType = this.mapTileTypeToStreetCoordinates(tileType); @@ -1009,8 +1133,53 @@ export default { if (this.tiles.images[tileType]) { this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize); } + // Straßennummern auch im Fallback dezent zeichnen (Spalten/Zeilen 0) + this.drawStreetNumbersOnMainCanvas(this.ctx, tileSize, tileType, 0, 0); } }, + + drawStreetNumbersOnMainCanvas(ctx, size, tileType, col, row) { + if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return; + const absCol = col + (this.currentMap.offsetX || 0); + const absRow = row + (this.currentMap.offsetY || 0); + const entry = this.currentMap.tileStreets.find(ts => ts.x === absCol && ts.y === absRow); + if (!entry) return; + + ctx.save(); + ctx.fillStyle = 'rgba(10,10,10,0.8)'; + ctx.font = `${Math.max(11, Math.floor(size * 0.06) - 1)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const left = size * 0.2; + const right = size * 0.8; + const top = size * 0.2; + const bottom = size * 0.8; + + const hName = entry.streetNameH?.name || null; + const vName = entry.streetNameV?.name || null; + const hNum = hName ? this.getStreetNumberByName(hName) : null; + const vNum = vName ? this.getStreetNumberByName(vName) : null; + + const t = tileType; + const hLeftTiles = new Set(['cornerbottomleft','cornertopleft','horizontal','tdown','tup','cross','fuelhorizontal','tleft']); + const hRightTiles = new Set(['cornerbottomright','cornertopright','tright']); + const vTopTiles = new Set(['cornertopleft','cornertopright','vertical','tup','tleft','tright','cross','fuelvertical']); + const vBottomTiles = new Set(['cornerbottomleft','cornerbottomright','tdown']); + + if (hNum !== null) { + const hx = hRightTiles.has(t) ? right : left; + const hy = size * 0.5 + 1; // 1px nach unten + ctx.fillText(String(hNum), hx, hy); + } + if (vNum !== null) { + const vx = size * 0.5; + const vy = vTopTiles.has(t) ? top : (vBottomTiles.has(t) ? bottom : top); + ctx.fillText(String(vNum), vx, vy); + } + + ctx.restore(); + }, getTileType(row, col, rows, cols) { // Ecken @@ -1133,12 +1302,14 @@ export default { async loadTiles() { const tileNames = [ 'cornerbottomleft', 'cornerbottomright', 'cornertopleft', 'cornertopright', - 'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical' + 'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical', + 'tleft', 'tright', 'tup', 'tdown' ]; const mapTileNames = [ 'map-cornerbottomleft', 'map-cornerbottomright', 'map-cornertopleft', 'map-cornertopright', - 'map-cross', 'map-fuelhorizontal', 'map-fuelvertical', 'map-horizontal', 'map-vertical' + 'map-cross', 'map-fuelhorizontal', 'map-fuelvertical', 'map-horizontal', 'map-vertical', + 'map-tleft', 'map-tright', 'map-tup', 'map-tdown' ]; // Lade normale Tiles @@ -1173,12 +1344,11 @@ export default { async loadMaps() { try { const response = await apiClient.get('/api/taxi-maps/maps'); - this.maps = response.data.data || []; + this.maps = (response.data.data || []).map(m => this.buildMapDataFromTiles(m)); // Verwende die erste verfügbare Map als Standard if (this.maps.length > 0) { this.currentMap = this.maps[0]; - this.selectedMap = this.maps[0]; // Auch selectedMap setzen this.selectedMapId = this.maps[0].id; // Canvas-Größe an geladene Map anpassen @@ -1198,7 +1368,7 @@ export default { // Wechsle zur ausgewählten Map const selectedMap = this.maps.find(map => map.id === this.selectedMapId); if (selectedMap) { - this.currentMap = selectedMap; + this.currentMap = this.buildMapDataFromTiles({ ...selectedMap }); // console.log('Gewechselt zu Map:', selectedMap); // Canvas-Größe an neue Map anpassen @@ -1248,6 +1418,9 @@ export default { const offsetX = (canvas.width - cols * tileSize) / 2; const offsetY = (canvas.height - rows * tileSize) / 2; + // Set, um pro Straßennamen nur eine Nummer zu zeichnen + const drawnNames = new Set(); + for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const x = offsetX + col * tileSize; @@ -1261,6 +1434,14 @@ export default { if (this.tiles.images[mapTileType]) { ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize); } + + // Straßensegment der ausgewählten Straße hervorheben + const absCol = col + (this.currentMap.offsetX || 0); + const absRow = row + (this.currentMap.offsetY || 0); + this.drawStreetFillOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow); + + // Straßennamen-Nummern zeichnen, basierend auf tileStreets (nur einmal pro Name) + this.drawStreetNumbersOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow, drawnNames); } } } @@ -1289,6 +1470,8 @@ export default { if (this.tiles.images[mapTileType]) { ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize); } + + // Keine echten Namen im Fallback } } } @@ -1336,12 +1519,86 @@ export default { ctx.moveTo(taxiX, taxiY); ctx.lineTo(endX, endY); ctx.stroke(); - } - }, - - computed: { - toggleIcon() { - return this.isStatsExpanded ? '−' : '+'; + }, + buildMapDataFromTiles(map) { + if (!map || !Array.isArray(map.tiles) || map.tiles.length === 0) { + map.mapData = null; + map.offsetX = 0; + map.offsetY = 0; + return map; + } + const xs = map.tiles.map(t => t.x); + const ys = map.tiles.map(t => t.y); + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + const width = (maxX - minX + 1) || 1; + const height = (maxY - minY + 1) || 1; + const grid = Array.from({ length: height }, () => Array(width).fill(null)); + for (const t of map.tiles) { + const col = t.x - minX; + const row = t.y - minY; + if (row >= 0 && row < height && col >= 0 && col < width) { + grid[row][col] = t.tileType; + } + } + map.mapData = grid; + map.offsetX = minX; + map.offsetY = minY; + return map; + }, + onSelectStreet(name) { + this.selectedStreetName = (this.selectedStreetName === name) ? null : name; + }, + // Variante B: gesamte Straßenfläche im Tile halbtransparent rot füllen + drawStreetFillOnMinimap(ctx, x, y, size, tileType, col, row) { + if (!this.selectedStreetName || !this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return; + const entry = this.currentMap.tileStreets.find(ts => ts.x === col && ts.y === row); + if (!entry) return; + + const wantH = entry.streetNameH?.name === this.selectedStreetName; + const wantV = entry.streetNameV?.name === this.selectedStreetName; + if (!wantH && !wantV) return; + + const streetTileType = this.mapTileTypeToStreetCoordinates(tileType); + const regions = streetCoordinates.getDriveableRegions(streetTileType, size); + if (!regions || regions.length === 0) return; + + // Banddicke exakt 12px (bei Referenz 50px) skaliert auf aktuelle size + const scale = size / 50; + const bandThickness = 12 * scale; // px + + // Clip: Tile-Rechteck MINUS Hindernis-Regionen => übrig bleibt die graue Straßenfläche + ctx.save(); + ctx.beginPath(); + // 1) volles Tile + ctx.rect(x, y, size, size); + // 2) Hindernis-Regionen hinzufügen (EvenOdd) + for (const region of regions) { + if (!region || region.length === 0) continue; + ctx.moveTo(x + region[0].x, y + region[0].y); + for (let i = 1; i < region.length; i++) { + ctx.lineTo(x + region[i].x, y + region[i].y); + } + ctx.closePath(); + } + // EvenOdd erhält Fläche außerhalb der Regionen (Straße) + ctx.clip('evenodd'); + + ctx.fillStyle = '#ff4d4d'; + const marginRight = 1; // rechts 1px kürzen + const marginBottom = 1; // unten 1px kürzen + if (wantH) { + const by = y + size * 0.5 - bandThickness / 2; + ctx.fillRect(x, by, size, bandThickness); + } + if (wantV) { + const bx = x + size * 0.5 - bandThickness / 2; + ctx.fillRect(bx, y, bandThickness, size); + } + + ctx.restore(); } } } @@ -1793,4 +2050,11 @@ export default { height: auto; } } + +.legend-street-item { + cursor: pointer; +} +.legend-street-item.selected { + font-weight: 700; +}