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 @@
-
@@ -188,7 +216,7 @@
-
@@ -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;
+}