Änderung: Erweiterung der Taxi-Map-Logik und Verbesserung der Benutzeroberfläche
Änderungen: - Hinzufügung neuer Modelle für TaxiMapTile, TaxiStreetName und TaxiMapTileStreet zur Unterstützung der Tile- und Straßennamen-Logik. - Anpassung der TaxiMap- und TaxiMapService-Logik zur Verwaltung von Tiles und Straßennamen. - Implementierung von Methoden zur Upsert-Logik für Tiles und Straßennamen in der TaxiMapService. - Verbesserung der Benutzeroberfläche in TaxiToolsView.vue zur Anzeige und Bearbeitung von Straßennamen und zusätzlichen Elementen. Diese Anpassungen verbessern die Funktionalität und Benutzererfahrung im Taxi-Minispiel erheblich, indem sie eine detailliertere Verwaltung von Karten und Straßennamen ermöglichen.
This commit is contained in:
@@ -106,6 +106,9 @@ import TaxiGameState from './taxi/taxiGameState.js';
|
|||||||
import TaxiLevelStats from './taxi/taxiLevelStats.js';
|
import TaxiLevelStats from './taxi/taxiLevelStats.js';
|
||||||
import TaxiMapType from './taxi/taxiMapType.js';
|
import TaxiMapType from './taxi/taxiMapType.js';
|
||||||
import TaxiMap from './taxi/taxiMap.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() {
|
export default function setupAssociations() {
|
||||||
// RoomType 1:n Room
|
// RoomType 1:n Room
|
||||||
@@ -801,4 +804,15 @@ export default function setupAssociations() {
|
|||||||
// Taxi Map associations
|
// Taxi Map associations
|
||||||
TaxiMap.belongsTo(TaxiMapType, { foreignKey: 'mapTypeId', as: 'mapType' });
|
TaxiMap.belongsTo(TaxiMapType, { foreignKey: 'mapTypeId', as: 'mapType' });
|
||||||
TaxiMapType.hasMany(TaxiMap, { foreignKey: 'mapTypeId', as: 'maps' });
|
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' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,18 @@ const ChatRight = sequelize.define('ChatRight', {
|
|||||||
id: {
|
id: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
autoIncrement: true},
|
autoIncrement: true
|
||||||
|
},
|
||||||
tr: {
|
tr: {
|
||||||
type: DataTypes.STRING(32),
|
type: DataTypes.STRING(32),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true}}, {
|
unique: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
schema: 'chat',
|
schema: 'chat',
|
||||||
tableName: 'rights',
|
tableName: 'rights',
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
underscored: true});
|
underscored: true
|
||||||
|
});
|
||||||
|
|
||||||
export default ChatRight;
|
export default ChatRight;
|
||||||
|
|||||||
@@ -96,9 +96,7 @@ import Match3UserProgress from './match3/userProgress.js';
|
|||||||
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
import Match3UserLevelProgress from './match3/userLevelProgress.js';
|
||||||
|
|
||||||
// — Taxi Minigame —
|
// — Taxi Minigame —
|
||||||
import { TaxiGameState, TaxiLevelStats } from './taxi/index.js';
|
import { TaxiGameState, TaxiLevelStats, TaxiMapType, TaxiMap, TaxiMapTile, TaxiStreetName, TaxiMapTileStreet } from './taxi/index.js';
|
||||||
import TaxiMap from './taxi/taxiMap.js';
|
|
||||||
import TaxiMapType from './taxi/taxiMapType.js';
|
|
||||||
|
|
||||||
// — Politische Ämter (Politics) —
|
// — Politische Ämter (Politics) —
|
||||||
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
import PoliticalOfficeType from './falukant/type/political_office_type.js';
|
||||||
@@ -241,6 +239,9 @@ const models = {
|
|||||||
TaxiLevelStats,
|
TaxiLevelStats,
|
||||||
TaxiMap,
|
TaxiMap,
|
||||||
TaxiMapType,
|
TaxiMapType,
|
||||||
|
TaxiMapTile,
|
||||||
|
TaxiStreetName,
|
||||||
|
TaxiMapTileStreet,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default models;
|
export default models;
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ import TaxiGameState from './taxiGameState.js';
|
|||||||
import TaxiLevelStats from './taxiLevelStats.js';
|
import TaxiLevelStats from './taxiLevelStats.js';
|
||||||
import TaxiMapType from './taxiMapType.js';
|
import TaxiMapType from './taxiMapType.js';
|
||||||
import TaxiMap from './taxiMap.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 };
|
||||||
|
|||||||
@@ -41,20 +41,12 @@ const TaxiGameState = sequelize.define('TaxiGameState', {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: []
|
defaultValue: []
|
||||||
},
|
},
|
||||||
createdAt: {
|
// createdAt/updatedAt via timestamps
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'taxi_game_state',
|
tableName: 'taxi_game_state',
|
||||||
schema: 'taxi',
|
schema: 'taxi',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
@@ -45,20 +45,12 @@ const TaxiLevelStats = sequelize.define('TaxiLevelStats', {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
comment: 'Play time in seconds'
|
comment: 'Play time in seconds'
|
||||||
},
|
},
|
||||||
createdAt: {
|
// createdAt/updatedAt via timestamps
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'taxi_level_stat',
|
tableName: 'taxi_level_stat',
|
||||||
schema: 'taxi',
|
schema: 'taxi',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ const TaxiMap = sequelize.define('TaxiMap', {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
comment: 'Reference to TaxiMapType'
|
comment: 'Reference to TaxiMapType'
|
||||||
},
|
},
|
||||||
mapData: {
|
|
||||||
type: DataTypes.JSON,
|
|
||||||
allowNull: false,
|
|
||||||
comment: '2D array of map type IDs for each tile position'
|
|
||||||
},
|
|
||||||
isActive: {
|
isActive: {
|
||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
@@ -54,20 +49,12 @@ const TaxiMap = sequelize.define('TaxiMap', {
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
comment: 'Whether this is the default map for new games'
|
comment: 'Whether this is the default map for new games'
|
||||||
},
|
},
|
||||||
createdAt: {
|
// createdAt/updatedAt via timestamps
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'taxi_map',
|
tableName: 'taxi_map',
|
||||||
schema: 'taxi',
|
schema: 'taxi',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
fields: ['name']
|
fields: ['name']
|
||||||
|
|||||||
44
backend/models/taxi/taxiMapTile.js
Normal file
44
backend/models/taxi/taxiMapTile.js
Normal file
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
45
backend/models/taxi/taxiMapTileStreet.js
Normal file
45
backend/models/taxi/taxiMapTileStreet.js
Normal file
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
@@ -26,20 +26,12 @@ const TaxiMapType = sequelize.define('TaxiMapType', {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
defaultValue: true
|
defaultValue: true
|
||||||
},
|
},
|
||||||
createdAt: {
|
// createdAt/updatedAt via timestamps
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
tableName: 'taxi_map_type',
|
tableName: 'taxi_map_type',
|
||||||
schema: 'taxi',
|
schema: 'taxi',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
underscored: true,
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|||||||
24
backend/models/taxi/taxiStreetName.js
Normal file
24
backend/models/taxi/taxiStreetName.js
Normal file
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import BaseService from './BaseService.js';
|
import BaseService from './BaseService.js';
|
||||||
import TaxiMap from '../models/taxi/taxiMap.js';
|
import TaxiMap from '../models/taxi/taxiMap.js';
|
||||||
import TaxiMapType from '../models/taxi/taxiMapType.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 {
|
class TaxiMapService extends BaseService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -30,10 +33,18 @@ class TaxiMapService extends BaseService {
|
|||||||
try {
|
try {
|
||||||
const maps = await TaxiMap.findAll({
|
const maps = await TaxiMap.findAll({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
include: [{
|
include: [
|
||||||
model: TaxiMapType,
|
{ model: TaxiMapType, as: 'mapType' },
|
||||||
as: 'mapType'
|
{ model: TaxiMapTile, as: 'tiles' },
|
||||||
}],
|
{
|
||||||
|
model: TaxiMapTileStreet,
|
||||||
|
as: 'tileStreets',
|
||||||
|
include: [
|
||||||
|
{ model: TaxiStreetName, as: 'streetNameH' },
|
||||||
|
{ model: TaxiStreetName, as: 'streetNameV' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
order: [['name', 'ASC']]
|
order: [['name', 'ASC']]
|
||||||
});
|
});
|
||||||
return maps;
|
return maps;
|
||||||
@@ -53,10 +64,18 @@ class TaxiMapService extends BaseService {
|
|||||||
id: mapId,
|
id: mapId,
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
include: [{
|
include: [
|
||||||
model: TaxiMapType,
|
{ model: TaxiMapType, as: 'mapType' },
|
||||||
as: 'mapType'
|
{ model: TaxiMapTile, as: 'tiles' },
|
||||||
}]
|
{
|
||||||
|
model: TaxiMapTileStreet,
|
||||||
|
as: 'tileStreets',
|
||||||
|
include: [
|
||||||
|
{ model: TaxiStreetName, as: 'streetNameH' },
|
||||||
|
{ model: TaxiStreetName, as: 'streetNameV' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -93,8 +112,18 @@ class TaxiMapService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
async createMap(mapData) {
|
async createMap(mapData) {
|
||||||
try {
|
try {
|
||||||
const map = await TaxiMap.create(mapData);
|
const { tiles, tileStreetNames, ...mapFields } = mapData;
|
||||||
return map;
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Error creating map:', error);
|
console.error('Error creating map:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -106,7 +135,8 @@ class TaxiMapService extends BaseService {
|
|||||||
*/
|
*/
|
||||||
async updateMap(mapId, updateData) {
|
async updateMap(mapId, updateData) {
|
||||||
try {
|
try {
|
||||||
const [updatedRowsCount] = await TaxiMap.update(updateData, {
|
const { tiles, tileStreetNames, ...mapFields } = updateData;
|
||||||
|
const [updatedRowsCount] = await TaxiMap.update(mapFields, {
|
||||||
where: { id: mapId }
|
where: { id: mapId }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +144,13 @@ class TaxiMapService extends BaseService {
|
|||||||
throw new Error('Map not found');
|
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);
|
return await this.getMapById(mapId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating map:', 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)
|
* Löscht eine Map (soft delete)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// initializeTaxi.js
|
// initializeTaxi.js
|
||||||
|
|
||||||
import TaxiMapService from '../services/taxiMapService.js';
|
import TaxiMapService from '../services/taxiMapService.js';
|
||||||
|
import { sequelize } from './sequelize.js';
|
||||||
|
|
||||||
const initializeTaxi = async () => {
|
const initializeTaxi = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -8,6 +9,85 @@ const initializeTaxi = async () => {
|
|||||||
|
|
||||||
const taxiMapService = new TaxiMapService();
|
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
|
// Initialisiere Map-Typen
|
||||||
console.log('Initializing taxi map types...');
|
console.log('Initializing taxi map types...');
|
||||||
await taxiMapService.initializeMapTypes();
|
await taxiMapService.initializeMapTypes();
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const checkSchemaUpdates = async (models) => {
|
|||||||
const schemas = [
|
const schemas = [
|
||||||
'community', 'logs', 'type', 'service', 'forum',
|
'community', 'logs', 'type', 'service', 'forum',
|
||||||
'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log',
|
'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log',
|
||||||
'chat', 'match3'
|
'chat', 'match3', 'taxi'
|
||||||
];
|
];
|
||||||
|
|
||||||
let needsUpdate = false;
|
let needsUpdate = false;
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ class MotorSound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (!this.gainNode) return;
|
if (!this.gainNode || !this.gainNode.context) return;
|
||||||
this.gainNode.disconnect(this.context.destination);
|
try {
|
||||||
|
this.gainNode.disconnect(this.context.destination);
|
||||||
|
} catch (_) {
|
||||||
|
// bereits disconnected
|
||||||
|
}
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,13 @@
|
|||||||
"mapType": "Map-Typ",
|
"mapType": "Map-Typ",
|
||||||
"mapLayout": "Map-Layout",
|
"mapLayout": "Map-Layout",
|
||||||
"tilePalette": "Tile-Palette",
|
"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",
|
"position": "Position",
|
||||||
"fillAllRoads": "Alle Straßen",
|
"fillAllRoads": "Alle Straßen",
|
||||||
"clearAll": "Alle löschen",
|
"clearAll": "Alle löschen",
|
||||||
|
|||||||
@@ -211,6 +211,13 @@
|
|||||||
"mapType": "Map Type",
|
"mapType": "Map Type",
|
||||||
"mapLayout": "Map Layout",
|
"mapLayout": "Map Layout",
|
||||||
"tilePalette": "Tile Palette",
|
"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",
|
"position": "Position",
|
||||||
"fillAllRoads": "All Roads",
|
"fillAllRoads": "All Roads",
|
||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Match3Game from '../views/minigames/Match3Game.vue';
|
// Lazy-Loading verhindert Circular Imports/HMR-Probleme
|
||||||
import TaxiGame from '../views/minigames/TaxiGame.vue';
|
const Match3Game = () => import('../views/minigames/Match3Game.vue');
|
||||||
|
const TaxiGame = () => import('../views/minigames/TaxiGame.vue');
|
||||||
|
|
||||||
const minigamesRoutes = [
|
const minigamesRoutes = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tile-Palette (rechts) -->
|
<!-- Tile-Palette (rechts) -->
|
||||||
<div class="tile-palette">
|
<div class="tile-palette tool-panel">
|
||||||
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
||||||
<div class="tile-grid">
|
<div class="tile-grid">
|
||||||
<div
|
<div
|
||||||
@@ -113,6 +113,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Straßennamen (drittes Panel) -->
|
||||||
|
<div v-if="showStreetNamePanel" class="tool-panel street-names">
|
||||||
|
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group-mini" v-if="supportsHorizontalName">
|
||||||
|
<label class="arrow-label">↔</label>
|
||||||
|
<input type="text" :value="selectedCell?.streetNameH || ''"
|
||||||
|
@input="onChangeStreetName('h', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group-mini" v-if="supportsVerticalName">
|
||||||
|
<label class="arrow-label">↕</label>
|
||||||
|
<input type="text" :value="selectedCell?.streetNameV || ''"
|
||||||
|
@input="onChangeStreetName('v', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-mini" v-if="showContinueOtherButton">
|
||||||
|
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
|
||||||
|
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zusätzliche Elemente (viertes Panel) -->
|
||||||
|
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
|
||||||
|
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
|
||||||
|
<div class="panel-body placeholder">—</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,7 +216,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tile-Palette (rechts) -->
|
<!-- Tile-Palette (rechts) -->
|
||||||
<div class="tile-palette">
|
<div class="tile-palette tool-panel">
|
||||||
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
<h4>{{ $t('admin.taxiTools.mapEditor.tilePalette') }}</h4>
|
||||||
<div class="tile-grid">
|
<div class="tile-grid">
|
||||||
<div
|
<div
|
||||||
@@ -207,6 +235,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Straßennamen (drittes Panel) -->
|
||||||
|
<div v-if="showStreetNamePanel" class="tool-panel street-names">
|
||||||
|
<h4>{{ $t('admin.taxiTools.mapEditor.streetNames') }}</h4>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="form-group-mini" v-if="supportsHorizontalName">
|
||||||
|
<label class="arrow-label">↔</label>
|
||||||
|
<input type="text" :value="selectedCell?.streetNameH || ''"
|
||||||
|
@input="onChangeStreetName('h', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group-mini" v-if="supportsVerticalName">
|
||||||
|
<label class="arrow-label">↕</label>
|
||||||
|
<input type="text" :value="selectedCell?.streetNameV || ''"
|
||||||
|
@input="onChangeStreetName('v', $event.target.value)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-mini" v-if="showContinueOtherButton">
|
||||||
|
<button type="button" class="btn btn-primary mini" @click="continueStreetNames">
|
||||||
|
{{ $t('admin.taxiTools.mapEditor.continueOther') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zusätzliche Elemente (viertes Panel) -->
|
||||||
|
<div v-if="showExtraElementsPanel" class="tool-panel extra-elements">
|
||||||
|
<h4>{{ $t('admin.taxiTools.mapEditor.extraElements') }}</h4>
|
||||||
|
<div class="panel-body placeholder">—</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -254,7 +310,7 @@ export default {
|
|||||||
mapForm: {
|
mapForm: {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
mapData: []
|
// mapData entfernt – Tiles werden separat gesendet
|
||||||
},
|
},
|
||||||
boardCells: {}, // { "x,y": { tileType: "horizontal", x: 0, y: 0 } }
|
boardCells: {}, // { "x,y": { tileType: "horizontal", x: 0, y: 0 } }
|
||||||
availableTileTypes: [
|
availableTileTypes: [
|
||||||
@@ -286,6 +342,38 @@ export default {
|
|||||||
if (Object.keys(this.boardCells).length === 0) return 0;
|
if (Object.keys(this.boardCells).length === 0) return 0;
|
||||||
return this.maxY - this.minY + 1;
|
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() {
|
minX() {
|
||||||
if (Object.keys(this.boardCells).length === 0) return 0;
|
if (Object.keys(this.boardCells).length === 0) return 0;
|
||||||
@@ -352,6 +440,170 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
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: corner<xxx>right, horizontal, tdown, tup, cross, fuelhorizontal, sowie corner<xxx>left 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<xxx>, vertical, tleft, tright, cross, fuelvertical, cornerup<xxx>
|
||||||
|
// Abschluss bei Richtung down: cornerup<xxx> 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() {
|
async loadMaps() {
|
||||||
try {
|
try {
|
||||||
const user = this.$store.getters.user;
|
const user = this.$store.getters.user;
|
||||||
@@ -402,11 +654,27 @@ export default {
|
|||||||
this.mapForm = {
|
this.mapForm = {
|
||||||
name: map.name,
|
name: map.name,
|
||||||
description: map.description,
|
description: map.description,
|
||||||
mapData: map.mapData || []
|
|
||||||
};
|
};
|
||||||
|
// Falls Tiles aus Backend mitkommen, Board daraus erstellen
|
||||||
if (map.mapData && map.mapData.length > 0) {
|
if (Array.isArray(map.tiles) && map.tiles.length > 0) {
|
||||||
this.createBoardFromData(map.mapData);
|
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 {
|
} else {
|
||||||
this.initializeBoard();
|
this.initializeBoard();
|
||||||
}
|
}
|
||||||
@@ -433,9 +701,8 @@ export default {
|
|||||||
// Wenn eine Position ausgewählt ist, platziere das Tile
|
// Wenn eine Position ausgewählt ist, platziere das Tile
|
||||||
if (this.selectedCellKey !== null) {
|
if (this.selectedCellKey !== null) {
|
||||||
this.placeTile(this.selectedCellKey, tileType);
|
this.placeTile(this.selectedCellKey, tileType);
|
||||||
// Setze Auswahl zurück nach dem Platzieren
|
// Nach dem Setzen bleibt die Zelle ausgewählt
|
||||||
this.selectedCellKey = null;
|
// Tile-Typ-Auswahl bleibt erhalten, um mehrere gleiche Tiles zu setzen
|
||||||
this.selectedTileType = null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -453,6 +720,9 @@ export default {
|
|||||||
y: y
|
y: y
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Nach dem Setzen: Zelle ausgewählt lassen
|
||||||
|
this.selectedCellKey = cellKey;
|
||||||
|
|
||||||
// Erweitere das Board um benachbarte leere Zellen falls nötig
|
// Erweitere das Board um benachbarte leere Zellen falls nötig
|
||||||
this.expandBoardIfNeeded(x, y, tileType);
|
this.expandBoardIfNeeded(x, y, tileType);
|
||||||
},
|
},
|
||||||
@@ -681,13 +951,19 @@ export default {
|
|||||||
|
|
||||||
async saveMap() {
|
async saveMap() {
|
||||||
try {
|
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 = {
|
const mapData = {
|
||||||
...this.mapForm,
|
...this.mapForm,
|
||||||
width: this.boardWidth,
|
width: this.boardWidth,
|
||||||
height: this.boardHeight,
|
height: this.boardHeight,
|
||||||
tileSize: 50,
|
tileSize: 50,
|
||||||
mapTypeId: 1, // Standard Map-Typ
|
mapTypeId: 1,
|
||||||
mapData: this.generateMapData()
|
tiles,
|
||||||
|
tileStreetNames
|
||||||
};
|
};
|
||||||
|
|
||||||
let savedMap;
|
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) {
|
async deleteMap(mapId) {
|
||||||
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
if (confirm('Möchtest du diese Map wirklich löschen?')) {
|
||||||
try {
|
try {
|
||||||
@@ -941,6 +1229,44 @@ export default {
|
|||||||
flex: 0 0 300px;
|
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 {
|
.tile-palette h4 {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #F9A22C;
|
color: #F9A22C;
|
||||||
@@ -953,7 +1279,7 @@ export default {
|
|||||||
grid-template-rows: repeat(4, 50px);
|
grid-template-rows: repeat(4, 50px);
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
padding: 10px;
|
padding: 6px; /* weniger Innenabstand, damit oben/unten kompakter */
|
||||||
background: #f9f9f9;
|
background: #f9f9f9;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,22 @@
|
|||||||
<li>Vermeide Kollisionen mit anderen Fahrzeugen</li>
|
<li>Vermeide Kollisionen mit anderen Fahrzeugen</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Straßenname-Legende -->
|
||||||
|
<div class="game-objectives" v-if="streetLegend.length">
|
||||||
|
<h4>Straßennamen</h4>
|
||||||
|
<ul>
|
||||||
|
<li
|
||||||
|
v-for="item in streetLegend"
|
||||||
|
:key="item.num"
|
||||||
|
class="legend-street-item"
|
||||||
|
:class="{ selected: selectedStreetName === item.name }"
|
||||||
|
@click="onSelectStreet(item.name)"
|
||||||
|
>
|
||||||
|
{{ item.num }}: {{ item.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spiel-Canvas mit Tacho -->
|
<!-- Spiel-Canvas mit Tacho -->
|
||||||
@@ -228,7 +244,40 @@ export default {
|
|||||||
keys: {},
|
keys: {},
|
||||||
motorSound: null,
|
motorSound: null,
|
||||||
audioContext: 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() {
|
async mounted() {
|
||||||
@@ -245,6 +294,74 @@ export default {
|
|||||||
this.cleanup();
|
this.cleanup();
|
||||||
},
|
},
|
||||||
methods: {
|
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)
|
// Mapping zwischen Spiel-Tile-Namen (lowercase) und streetCoordinates.json (camelCase)
|
||||||
mapTileTypeToStreetCoordinates(tileType) {
|
mapTileTypeToStreetCoordinates(tileType) {
|
||||||
const mapping = {
|
const mapping = {
|
||||||
@@ -256,7 +373,11 @@ export default {
|
|||||||
'vertical': 'vertical',
|
'vertical': 'vertical',
|
||||||
'cross': 'cross',
|
'cross': 'cross',
|
||||||
'fuelhorizontal': 'fuelHorizontal',
|
'fuelhorizontal': 'fuelHorizontal',
|
||||||
'fuelvertical': 'fuelVertical'
|
'fuelvertical': 'fuelVertical',
|
||||||
|
'tleft': 'tLeft',
|
||||||
|
'tright': 'tRight',
|
||||||
|
'tup': 'tUp',
|
||||||
|
'tdown': 'tDown'
|
||||||
};
|
};
|
||||||
return mapping[tileType] || tileType;
|
return mapping[tileType] || tileType;
|
||||||
},
|
},
|
||||||
@@ -588,9 +709,9 @@ export default {
|
|||||||
this.lastSteerChange = currentTime;
|
this.lastSteerChange = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiere Position (1/4 der ursprünglichen Bewegung)
|
// Aktualisiere Position (+20% Geschwindigkeit)
|
||||||
this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.1;
|
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.1;
|
this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.12;
|
||||||
|
|
||||||
// Aktualisiere aktuelle Tile-Position
|
// Aktualisiere aktuelle Tile-Position
|
||||||
this.updateCurrentTile();
|
this.updateCurrentTile();
|
||||||
@@ -995,9 +1116,12 @@ export default {
|
|||||||
if (this.tiles.images[tileType]) {
|
if (this.tiles.images[tileType]) {
|
||||||
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
|
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
|
// Fallback: Zeichne Standard-Tile wenn keine Map geladen ist
|
||||||
const tileType = 'cornertopleft'; // Standard-Tile
|
const tileType = 'cornertopleft'; // Standard-Tile
|
||||||
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
const streetTileType = this.mapTileTypeToStreetCoordinates(tileType);
|
||||||
@@ -1009,9 +1133,54 @@ export default {
|
|||||||
if (this.tiles.images[tileType]) {
|
if (this.tiles.images[tileType]) {
|
||||||
this.ctx.drawImage(this.tiles.images[tileType], 0, 0, tileSize, tileSize);
|
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) {
|
getTileType(row, col, rows, cols) {
|
||||||
// Ecken
|
// Ecken
|
||||||
if (row === 0 && col === 0) return 'cornertopleft';
|
if (row === 0 && col === 0) return 'cornertopleft';
|
||||||
@@ -1133,12 +1302,14 @@ export default {
|
|||||||
async loadTiles() {
|
async loadTiles() {
|
||||||
const tileNames = [
|
const tileNames = [
|
||||||
'cornerbottomleft', 'cornerbottomright', 'cornertopleft', 'cornertopright',
|
'cornerbottomleft', 'cornerbottomright', 'cornertopleft', 'cornertopright',
|
||||||
'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical'
|
'cross', 'fuelhorizontal', 'fuelvertical', 'horizontal', 'vertical',
|
||||||
|
'tleft', 'tright', 'tup', 'tdown'
|
||||||
];
|
];
|
||||||
|
|
||||||
const mapTileNames = [
|
const mapTileNames = [
|
||||||
'map-cornerbottomleft', 'map-cornerbottomright', 'map-cornertopleft', 'map-cornertopright',
|
'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
|
// Lade normale Tiles
|
||||||
@@ -1173,12 +1344,11 @@ export default {
|
|||||||
async loadMaps() {
|
async loadMaps() {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/api/taxi-maps/maps');
|
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
|
// Verwende die erste verfügbare Map als Standard
|
||||||
if (this.maps.length > 0) {
|
if (this.maps.length > 0) {
|
||||||
this.currentMap = this.maps[0];
|
this.currentMap = this.maps[0];
|
||||||
this.selectedMap = this.maps[0]; // Auch selectedMap setzen
|
|
||||||
this.selectedMapId = this.maps[0].id;
|
this.selectedMapId = this.maps[0].id;
|
||||||
|
|
||||||
// Canvas-Größe an geladene Map anpassen
|
// Canvas-Größe an geladene Map anpassen
|
||||||
@@ -1198,7 +1368,7 @@ export default {
|
|||||||
// Wechsle zur ausgewählten Map
|
// Wechsle zur ausgewählten Map
|
||||||
const selectedMap = this.maps.find(map => map.id === this.selectedMapId);
|
const selectedMap = this.maps.find(map => map.id === this.selectedMapId);
|
||||||
if (selectedMap) {
|
if (selectedMap) {
|
||||||
this.currentMap = selectedMap;
|
this.currentMap = this.buildMapDataFromTiles({ ...selectedMap });
|
||||||
// console.log('Gewechselt zu Map:', selectedMap);
|
// console.log('Gewechselt zu Map:', selectedMap);
|
||||||
|
|
||||||
// Canvas-Größe an neue Map anpassen
|
// Canvas-Größe an neue Map anpassen
|
||||||
@@ -1248,6 +1418,9 @@ export default {
|
|||||||
const offsetX = (canvas.width - cols * tileSize) / 2;
|
const offsetX = (canvas.width - cols * tileSize) / 2;
|
||||||
const offsetY = (canvas.height - rows * 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 row = 0; row < rows; row++) {
|
||||||
for (let col = 0; col < cols; col++) {
|
for (let col = 0; col < cols; col++) {
|
||||||
const x = offsetX + col * tileSize;
|
const x = offsetX + col * tileSize;
|
||||||
@@ -1261,6 +1434,14 @@ export default {
|
|||||||
if (this.tiles.images[mapTileType]) {
|
if (this.tiles.images[mapTileType]) {
|
||||||
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
|
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]) {
|
if (this.tiles.images[mapTileType]) {
|
||||||
ctx.drawImage(this.tiles.images[mapTileType], x, y, tileSize, tileSize);
|
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.moveTo(taxiX, taxiY);
|
||||||
ctx.lineTo(endX, endY);
|
ctx.lineTo(endX, endY);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
},
|
||||||
},
|
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;
|
||||||
|
|
||||||
computed: {
|
const wantH = entry.streetNameH?.name === this.selectedStreetName;
|
||||||
toggleIcon() {
|
const wantV = entry.streetNameV?.name === this.selectedStreetName;
|
||||||
return this.isStatsExpanded ? '−' : '+';
|
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;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legend-street-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.legend-street-item.selected {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user