diff --git a/frontend/public/images/taxi/house_small.png b/frontend/public/images/taxi/house_small.png new file mode 100644 index 0000000..25e9ae1 Binary files /dev/null and b/frontend/public/images/taxi/house_small.png differ diff --git a/frontend/src/views/admin/TaxiToolsView.vue b/frontend/src/views/admin/TaxiToolsView.vue index b372679..8fcb706 100644 --- a/frontend/src/views/admin/TaxiToolsView.vue +++ b/frontend/src/views/admin/TaxiToolsView.vue @@ -88,6 +88,16 @@ :alt="getCellAtPosition(x, y).tileType" class="tile-image" /> +
+
+
+
+
@@ -139,7 +149,26 @@

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

-
+
+
+ +
+
Haus
+
+
+
+
+
+
+
+
@@ -210,6 +239,16 @@ :alt="getCellAtPosition(x, y).tileType" class="tile-image" /> +
+
+
+
+
@@ -261,7 +300,36 @@

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

-
+
+
+ +
+
Haus
+
+
+
+
+
+
+
+
@@ -289,6 +357,26 @@ import SimpleTabs from '../../components/SimpleTabs.vue'; import MessageDialog from '../../dialogues/standard/MessageDialog.vue'; import apiClient from '../../utils/axios.js'; +// Matrix: erlaubte Haus-Tür-Richtungen je Tile-Typ und Ecke +// Richtungen: bottom, right, top, left +const HOUSE_TYPE_MATRIX = { + horizontal: { lo: ['bottom'], ro: ['bottom'], lu: ['top'], ru: ['top'] }, + vertical: { lo: ['right'], ro: ['left'], lu: ['right'], ru: ['left'] }, + cross: { lo: ['bottom', 'right'], ro: ['bottom', 'left'], lu: ['top', 'right'], ru: ['top', 'left'] }, + cornertopleft: { lo: ['bottom', 'right'], ro: ['left'], lu: ['top'], ru: [] }, + cornertopright: { lo: ['right'], ro: ['bottom', 'right'], lu: [], ru: ['top'] }, + cornerbottomleft: { lo: ['bottom'], ro: [], lu: ['top', 'right'], ru: ['left'] }, + cornerbottomright: { lo: [], ro: ['bottom'], lu: ['right'], ru: ['top', 'left'] }, + tup: { lo: ['bottom', 'right'], ro: ['bottom', 'left'], lu: ['top'], ru: ['top'] }, + tdown: { lo: ['bottom'], ro: ['bottom'], lu: ['top', 'right'], ru: ['top', 'left'] }, + tleft: { lo: ['bottom', 'right'], ro: ['left'], lu: ['top', 'right'], ru: ['left'] }, + tright: { lo: ['right'], ro: ['bottom', 'left'], lu: ['right'], ru: ['top', 'left'] }, + fuelhorizontal: { lo: [], ro: [], lu: ['top'], ru: ['top'] }, + fuelvertical: { lo: ['right'], ro: [], lu: ['right'], ru: [] } +}; + +const DIRECTION_TO_ROTATION = { bottom: 0, right: 90, top: 180, left: 270 }; + export default { name: 'AdminTaxiToolsView', components: { @@ -325,6 +413,9 @@ export default { ], selectedCellKey: null, selectedTileType: null + ,pendingHouse: null + ,allowedHouseCorners: [] + ,pendingCorner: null }; }, computed: { @@ -353,8 +444,8 @@ export default { return this.supportsHorizontal(type) || this.supportsVertical(type); }, showExtraElementsPanel() { - // Gleiche Logik wie Straßenpanel – nur sichtbar, wenn eine Zelle selektiert ist - return !!this.selectedCell; + // Nur sichtbar, wenn eine Zelle selektiert ist UND ein Tile-Typ gesetzt ist + return !!(this.selectedCell && this.selectedCell.tileType); }, supportsHorizontalName() { if (!this.selectedCell) return false; @@ -413,6 +504,15 @@ export default { gridTemplateRows: `repeat(${this.boardHeight}, 50px)`, gap: '2px' }; + }, + + // Verfügbare Haus-Tür-Richtungen anhand ausgewählter Ecke und Tile-Typ + availableHouseDirections() { + if (!this.pendingCorner || !this.selectedCell || !this.selectedCell.tileType) return []; + const entry = HOUSE_TYPE_MATRIX[this.selectedCell.tileType]; + if (!entry) return []; + const dirs = entry[this.pendingCorner] || []; + return Array.isArray(dirs) ? dirs : []; } }, @@ -440,6 +540,18 @@ export default { }, methods: { + directionToRotation(dir) { + return DIRECTION_TO_ROTATION[dir] ?? 0; + }, + doorCssClass(dir) { + switch (dir) { + case 'bottom': return 'door-bottom'; + case 'right': return 'door-right'; + case 'top': return 'door-top'; + case 'left': return 'door-left'; + default: return 'door-bottom'; + } + }, 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']; @@ -661,6 +773,16 @@ export default { for (const t of map.tiles) { const key = `${t.x},${t.y}`; this.boardCells[key] = { tileType: t.tileType, x: t.x, y: t.y }; + if (t.meta && t.meta.house) { + // Legacy Einzel-Haus zu neuer Struktur migrieren + const corner = t.meta.house.corner || 'lo'; + const rotation = t.meta.house.rotation || 0; + this.boardCells[key].extraHouses = { [corner]: rotation }; + } + if (t.meta && t.meta.houses) { + // Neue Struktur: Mapping corner -> rotation + this.boardCells[key].extraHouses = { ...t.meta.houses }; + } } // Straßennamen aus tileStreets übernehmen if (Array.isArray(map.tileStreets)) { @@ -688,10 +810,14 @@ export default { }, selectCell(key) { - // Wähle Position aus (Platzhalter) + // Wähle Position aus this.selectedCellKey = this.selectedCellKey === key ? null : key; - // Setze Tile-Auswahl zurück + // Reset temporärer Zustände this.selectedTileType = null; + this.pendingHouse = null; + this.pendingCorner = null; + // Erlaubte Ecken vorberechnen + this.computeAllowedHouseCorners(); }, selectTileType(tileType) { @@ -725,6 +851,46 @@ export default { // Erweitere das Board um benachbarte leere Zellen falls nötig this.expandBoardIfNeeded(x, y, tileType); + // Erlaubte Haus-Ecken aktualisieren + this.computeAllowedHouseCorners(); + }, + + startPlaceHouse(rotationDeg) { + // Neuer Flow: Rotation wählen, nachdem Ecke gewählt wurde + if (!this.selectedCellKey || !this.pendingCorner) return; + const cell = this.boardCells[this.selectedCellKey]; + if (!cell || !cell.tileType) return; + const existing = cell.extraHouses || {}; + const next = { ...existing, [this.pendingCorner]: rotationDeg }; + const updated = { ...cell, extraHouses: next }; + this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated); + this.pendingCorner = null; + }, + confirmHouseCorner(corner) { + if (!this.selectedCellKey) return; + const cell = this.boardCells[this.selectedCellKey]; + if (!cell || !cell.tileType) return; + this.pendingCorner = corner; + }, + computeAllowedHouseCorners() { + if (!this.selectedCell || !this.selectedCell.tileType) { + this.allowedHouseCorners = []; + return; + } + this.allowedHouseCorners = this.getAllowedHouseCorners(this.selectedCell.tileType); + }, + getAllowedHouseCorners(tileType) { + // lo=links oben, ro=rechts oben, lu=links unten, ru=rechts unten + switch (tileType) { + case 'cornerbottomright': return ['ro','lu','ru']; + case 'cornerbottomleft': return ['lo','lu','ru']; + case 'cornertopright': return ['lo','ro','ru']; + case 'cornertopleft': return ['lo','ro','lu']; + case 'fuelhorizontal': return ['ru','lu']; + case 'fuelvertical': return ['lo','lu']; + default: // horizontal, vertical, cross, tup, tdown, tleft, tright + return ['lo','ro','lu','ru']; + } }, expandBoardIfNeeded(x, y, tileType) { @@ -864,6 +1030,25 @@ export default { return { tileType: null, x, y }; }, + doorClass(deg) { + switch (deg) { + case 0: return 'door-bottom'; + case 90: return 'door-right'; + case 180: return 'door-top'; + case 270: return 'door-left'; + default: return 'door-bottom'; + } + }, + cornerClass(corner) { + switch (corner) { + case 'lo': return 'corner-lo'; + case 'ro': return 'corner-ro'; + case 'lu': return 'corner-lu'; + case 'ru': return 'corner-ru'; + default: return 'corner-lo'; + } + }, + range(start, end) { const result = []; for (let i = start; i <= end; i++) { @@ -954,7 +1139,12 @@ export default { // 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 })); + .map(c => ({ + x: c.x, + y: c.y, + tileType: c.tileType, + meta: c.extraHouses ? { houses: { ...c.extraHouses } } : null + })); const tileStreetNames = this.collectTileStreetNames(); const mapData = { ...this.mapForm, @@ -1238,6 +1428,39 @@ export default { color: #F9A22C; font-size: 18px; } +.extra-grid { display: flex; gap: 8px; align-items: center; } +.extra-option { + width: 20px; + height: 20px; + border: 1px solid #ddd; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + background: #fff; + cursor: pointer; + transition: transform 0.15s ease; +} +.extra-option:hover { transform: scale(1.05); } +.house-square { width: 10px; height: 10px; background: #d0d0d0; border: 1px solid #aaa; position: relative; } +.house-door { position: absolute; background: #000; } +.door-top { top: -1px; left: calc(50% - 2.5px); width: 5px; height: 2px; } +.door-bottom { bottom: -1px; left: calc(50% - 2.5px); width: 5px; height: 2px; } +.door-left { left: -1px; top: calc(50% - 2.5px); width: 2px; height: 5px; } +.door-right { right: -1px; top: calc(50% - 2.5px); width: 2px; height: 5px; } +.house-overlay { position: absolute; inset: 0; pointer-events: none; } +.house-square.corner-lo { position: absolute; top: 1px; left: 1px; } +.house-square.corner-ro { position: absolute; top: 1px; right: 1px; } +.house-square.corner-lu { position: absolute; bottom: 1px; left: 1px; } +.house-square.corner-ru { position: absolute; bottom: 1px; right: 1px; } +.corner-chooser { margin-top: 8px; display: flex; gap: 6px; } +.corner-btn { padding: 3px; border: 1px solid #ccc; border-radius: 4px; background: #f7f7f7; cursor: pointer; } +.corner-btn:hover { background: #eee; } + +.corner-preview { width: 18px; height: 18px; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; } +.corner-preview .q { width: 100%; height: 100%; background: #000; display: block; } +.corner-preview .q.active { background: #d60000; } + .panel-body.placeholder { border: 2px dashed #ddd; border-radius: 8px;