diff --git a/backend/models/taxi/taxiMapTile.js b/backend/models/taxi/taxiMapTile.js index e3f61c5..9fa24d2 100644 --- a/backend/models/taxi/taxiMapTile.js +++ b/backend/models/taxi/taxiMapTile.js @@ -23,6 +23,12 @@ const TaxiMapTile = sequelize.define('TaxiMapTile', { type: DataTypes.STRING(50), allowNull: false, }, + trafficLight: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + comment: 'Whether this tile has a traffic light' + }, meta: { type: DataTypes.JSON, allowNull: true, @@ -36,6 +42,7 @@ const TaxiMapTile = sequelize.define('TaxiMapTile', { { unique: true, fields: ['map_id','x','y'] }, { fields: ['map_id'] }, { fields: ['tile_type'] }, + { fields: ['traffic_light'] }, ] }); diff --git a/backend/models/type/user_right.js b/backend/models/type/user_right.js index 3cf4741..ffa2450 100644 --- a/backend/models/type/user_right.js +++ b/backend/models/type/user_right.js @@ -7,7 +7,7 @@ const UserRightType = sequelize.define('user_right_type', { allowNull: false } }, { - tableName: 'user_right', + tableName: 'user_right_type', schema: 'type', underscored: true }); diff --git a/backend/services/taxiMapService.js b/backend/services/taxiMapService.js index dc38bb7..97f1101 100644 --- a/backend/services/taxiMapService.js +++ b/backend/services/taxiMapService.js @@ -209,7 +209,9 @@ class TaxiMapService extends BaseService { where: { mapId, x, y }, defaults: { mapId, x, y, tileType, meta: meta || null } }); - await row.update({ tileType, meta: meta || null }); + // trafficLight kann in meta.trafficLight oder künftig in eigener Spalte liegen + const trafficLight = !!(meta && meta.trafficLight); + await row.update({ tileType, meta: meta && Object.keys(meta).length ? meta : null, trafficLight }); } return { success: true }; } diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js index 03ac962..afd4338 100644 --- a/backend/utils/sequelize.js +++ b/backend/utils/sequelize.js @@ -45,8 +45,10 @@ const createSchemas = async () => { const initializeDatabase = async () => { await createSchemas(); - const { default: models } = await import('../models/index.js'); - await syncModels(models); + // Modelle nur laden, aber an dieser Stelle NICHT syncen. + // Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral + // über syncModelsWithUpdates()/syncModelsAlways gesteuert. + await import('../models/index.js'); }; const syncModels = async (models) => { diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js index ba204ab..e6324f6 100644 --- a/backend/utils/syncDatabase.js +++ b/backend/utils/syncDatabase.js @@ -1,6 +1,6 @@ // syncDatabase.js -import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways } from './sequelize.js'; +import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js'; import initializeTypes from './initializeTypes.js'; import initializeSettings from './initializeSettings.js'; import initializeUserRights from './initializeUserRights.js'; @@ -33,11 +33,32 @@ const syncDatabase = async () => { console.log("Initializing database schemas..."); await initializeDatabase(); - console.log("Synchronizing models..."); - await syncModelsWithUpdates(models); + // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt + console.log("Pre-ensure Taxi columns (traffic_light) ..."); + 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' AND column_name = 'traffic_light' + ) THEN + ALTER TABLE taxi.taxi_map_tile + ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false; + END IF; + END + $$; + `); + console.log("✅ traffic_light-Spalte ist vorhanden"); + } catch (e) { + console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); + } console.log("Setting up associations..."); - setupAssociations(); + setupAssociations(); + + console.log("Synchronizing models..."); + await syncModelsWithUpdates(models); console.log("Initializing settings..."); await initializeSettings(); @@ -91,11 +112,32 @@ const syncDatabaseForDeployment = async () => { console.log("Initializing database schemas..."); await initializeDatabase(); - console.log("Synchronizing models with schema updates..."); - await syncModelsAlways(models); + // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt + console.log("Pre-ensure Taxi columns (traffic_light) ..."); + 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' AND column_name = 'traffic_light' + ) THEN + ALTER TABLE taxi.taxi_map_tile + ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false; + END IF; + END + $$; + `); + console.log("✅ traffic_light-Spalte ist vorhanden"); + } catch (e) { + console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); + } console.log("Setting up associations..."); - setupAssociations(); + setupAssociations(); + + console.log("Synchronizing models with schema updates..."); + await syncModelsAlways(models); console.log("Initializing settings..."); await initializeSettings(); diff --git a/frontend/public/images/taxi/passenger1.png b/frontend/public/images/taxi/passenger1.png new file mode 100644 index 0000000..352dd54 Binary files /dev/null and b/frontend/public/images/taxi/passenger1.png differ diff --git a/frontend/public/images/taxi/passenger2.png b/frontend/public/images/taxi/passenger2.png new file mode 100644 index 0000000..16a8580 Binary files /dev/null and b/frontend/public/images/taxi/passenger2.png differ diff --git a/frontend/public/images/taxi/passenger3.png b/frontend/public/images/taxi/passenger3.png new file mode 100644 index 0000000..775e0db Binary files /dev/null and b/frontend/public/images/taxi/passenger3.png differ diff --git a/frontend/public/images/taxi/passenger4.png b/frontend/public/images/taxi/passenger4.png new file mode 100644 index 0000000..9fcfa1a Binary files /dev/null and b/frontend/public/images/taxi/passenger4.png differ diff --git a/frontend/public/images/taxi/passenger5.png b/frontend/public/images/taxi/passenger5.png new file mode 100644 index 0000000..0b155d4 Binary files /dev/null and b/frontend/public/images/taxi/passenger5.png differ diff --git a/frontend/public/images/taxi/passenger6.png b/frontend/public/images/taxi/passenger6.png new file mode 100644 index 0000000..0e1684a Binary files /dev/null and b/frontend/public/images/taxi/passenger6.png differ diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 3a89e87..7fd2244 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -356,8 +356,8 @@ }, "gifts": { "Gold Coin": "Goldmünze", - "Silk Scarf": "Seidenschale", - "Exotic Perfume": "Exotisches Parfum", + "Silk Scarf": "Seidenschal", + "Exotic Perfume": "Exotisches Parfüm", "Crystal Pendant": "Kristallanhänger", "Leather Journal": "Lederjournal", "Fine Wine": "Feiner Wein", diff --git a/frontend/src/views/admin/TaxiToolsView.vue b/frontend/src/views/admin/TaxiToolsView.vue index 30e642f..6970947 100644 --- a/frontend/src/views/admin/TaxiToolsView.vue +++ b/frontend/src/views/admin/TaxiToolsView.vue @@ -98,6 +98,7 @@
+ Ampel @@ -178,6 +179,11 @@ +
+ +
@@ -249,6 +255,7 @@ :alt="getCellAtPosition(x, y).tileType" class="tile-image" /> + Ampel
+
+ +
@@ -795,6 +807,9 @@ export default { // Neue Struktur: Mapping corner -> rotation this.boardCells[key].extraHouses = { ...t.meta.houses }; } + if (t.meta && t.meta.trafficLight) { + this.boardCells[key].extraTrafficLight = true; + } } // Straßennamen aus tileStreets übernehmen if (Array.isArray(map.tileStreets)) { @@ -867,6 +882,13 @@ export default { this.computeAllowedHouseCorners(); }, + toggleTrafficLight() { + if (!this.selectedCellKey) return; + const cell = this.boardCells[this.selectedCellKey] || { x: 0, y: 0, tileType: null }; + const updated = { ...cell, extraTrafficLight: !cell.extraTrafficLight }; + this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated); + }, + startPlaceHouse(rotationDeg) { // Neuer Flow: Rotation wählen, nachdem Ecke gewählt wurde if (!this.selectedCellKey || !this.pendingCorner) return; @@ -1155,7 +1177,10 @@ export default { x: c.x, y: c.y, tileType: c.tileType, - meta: c.extraHouses ? { houses: { ...c.extraHouses } } : null + meta: { + ...(c.extraHouses ? { houses: { ...c.extraHouses } } : {}), + ...(c.extraTrafficLight ? { trafficLight: true } : {}) + } })); const tileStreetNames = this.collectTileStreetNames(); const tileHouses = this.collectTileHouses(); @@ -1478,10 +1503,15 @@ export default { .house-square.corner-ro { position: absolute; top: 3px; right: 3px; } .house-square.corner-lu { position: absolute; bottom: 3px; left: 3px; } .house-square.corner-ru { position: absolute; bottom: 3px; right: 3px; } +.traffic-overlay { position: absolute; width: 25px; height: 25px; left: 50%; top: 50%; transform: translate(-50%, -50%); pointer-events: none; } .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; } +.trafficlight-row { margin-top: 10px; } +.trafficlight-btn { padding: 4px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer; } +.trafficlight-icon { width: 40px; height: 40px; } + .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; } diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 282eb42..8a132dd 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -66,6 +66,10 @@
+ + 🚦 + {{ redLightViolations }} + ⏱️ {{ taxi.speed * 5 }} km/h @@ -236,6 +240,9 @@ export default { images: {} }, houseImage: null, + trafficLightStates: {}, + lastTrafficLightTick: 0, + redLightViolations: 0, maps: [], // Geladene Maps aus der Datenbank currentMap: null, // Aktuell verwendete Map selectedMapId: null, // ID der ausgewählten Map @@ -293,11 +300,43 @@ export default { this.setupEventListeners(); await this.initializeMotorSound(); this.setupAudioUnlockHandlers(); + this.lastTrafficLightTick = Date.now(); }, beforeUnmount() { this.cleanup(); }, methods: { + // Ampelschaltung: sekündliche Phasen-Updates; pro Tile ein State + updateTrafficLights() { + const now = Date.now(); + if (now - this.lastTrafficLightTick < 1000) return; + this.lastTrafficLightTick = now; + if (!this.currentMap || !Array.isArray(this.currentMap.tiles)) return; + const tiles = this.currentMap.tiles.filter(t => t.trafficLight || (t.meta && t.meta.trafficLight)); + for (const t of tiles) { + const key = `${t.x},${t.y}`; + let st = this.trafficLightStates[key]; + if (!st) { + // initialisieren: Phase starten, zufällige Grün/Rot-Dauern 5..15s + const r = (min,max)=> Math.floor(min + Math.random()*(max-min+1)); + st = { + phase: 0, // 0:Hgrün/Vrot, 1:Hgelb/Vrotgelb, 2:Hrot/Vgrün, 3:Hrotgelb/Vgelb + remaining: r(5,15), // Sekunden bis zur nächsten Umschaltung + hDur: r(5,15), // Dauer H-grün (Phase 0) + vDur: r(5,15) // Dauer V-grün (Phase 2) + }; + this.trafficLightStates[key] = st; + } + st.remaining -= 1; + if (st.remaining <= 0) { + // Phase vorwärts schalten, Zwischenphasen 1s + if (st.phase === 0) { st.phase = 1; st.remaining = 1; } + else if (st.phase === 1) { st.phase = 2; st.remaining = st.vDur; } + else if (st.phase === 2) { st.phase = 3; st.remaining = 1; } + else { st.phase = 0; st.remaining = st.hDur; } + } + } + }, // Hilfsfunktion: Liefert laufende Nummer für gegebenen Straßennamen getStreetNumberByName(name) { const item = this.streetLegend.find(it => it.name === name); @@ -681,6 +720,9 @@ export default { return; } + // Ampelschaltung tick + this.updateTrafficLights(); + this.updateTaxi(); this.handlePassengerActions(); this.checkCollisions(); @@ -1152,8 +1194,70 @@ export default { // Häuser zeichnen (aus tileHouses) const absCol = this.currentTile.col + (this.currentMap.offsetX || 0); const absRow = this.currentTile.row + (this.currentMap.offsetY || 0); + // Haltelinien (falls Ampel vorhanden) – 120px vom Rand, 5px dick, 40px breit + if (this.getTrafficLightFor(absCol, absRow)) { + const approaches = this.getTileApproaches(tileType); + this.drawStopLinesOnMainCanvas(this.ctx, tileSize, approaches); + } this.drawHousesOnMainCanvas(this.ctx, tileSize, absCol, absRow); // Straßennamen auf Haupt-Canvas zeichnen + // Ampeln an jeder Ecke des Tiles (über den Häusern, unter den Namen) + if (this.getTrafficLightFor(absCol, absRow)) { + const phase = this.getTrafficLightPhase(absCol, absRow); // 0,1,2,3 + // Phase-Mapping je Ecke laut Vorgabe: + // 0: H grün / V rot + // 1: H gelb / V rot-gelb + // 2: H rot / V grün + // 3: H rot-gelb / V gelb + const imgGreen = this.tiles.images['trafficlight-green']; + const imgYellow = this.tiles.images['trafficlight-yellow']; + const imgRed = this.tiles.images['trafficlight-red']; + const imgRedYellow = this.tiles.images['trafficlight-redyellow']; + { + const sTL = tileSize * 0.16; // um 20% kleiner als zuvor + // Positionierung nach Vorgabe: + // X: 180px vom linken bzw. rechten Rand, dann je 60px zum näheren Rand und 5px zurück + let leftX = 180; + let rightX = tileSize - 180 - sTL; + leftX = Math.max(0, leftX - 60); + rightX = Math.min(tileSize - sTL, rightX + 60); + leftX = Math.min(tileSize - sTL, Math.max(0, leftX + 5)); + rightX = Math.min(tileSize - sTL, Math.max(0, rightX - 5)); + // Y: oben korrekt, unten 60px weiter nach oben + const topY = 180 - sTL; + const bottomY = tileSize - 180 - 60; + // Sichtbarkeit pro Seite bestimmen und auf Ecken mappen + const ap = this.getTileApproaches(tileType); + let showTL = false, showTR = false, showBL = false, showBR = false; + if (ap.top) { showTL = true; showTR = true; } + if (ap.bottom) { showBL = true; showBR = true; } + if (ap.left) { showTL = true; showBL = true; } + if (ap.right) { showTR = true; showBR = true; } + // Tile-spezifische Ausnahmen (z.B. T-Kreuzungen) + const hide = this.getCornerHidesForTile(tileType); + if (hide.TL) showTL = false; + if (hide.TR) showTR = false; + if (hide.BL) showBL = false; + if (hide.BR) showBR = false; + const drawCorner = (corner, x, y) => { + let img = imgRed; + if (corner === 'top') { + if (phase === 0) img = imgGreen; else if (phase === 1) img = imgYellow; else if (phase === 2) img = imgRed; else img = imgRedYellow; + } else if (corner === 'bottom') { + if (phase === 0) img = imgRed; else if (phase === 1) img = imgRedYellow; else if (phase === 2) img = imgGreen; else img = imgYellow; + } else if (corner === 'left') { + if (phase === 0) img = imgGreen; else if (phase === 1) img = imgYellow; else if (phase === 2) img = imgRed; else img = imgRedYellow; + } else if (corner === 'right') { + if (phase === 0) img = imgRed; else if (phase === 1) img = imgRedYellow; else if (phase === 2) img = imgGreen; else img = imgYellow; + } + if (img && img.complete) this.ctx.drawImage(img, x, y, sTL, sTL); + }; + if (showTL) drawCorner('left', leftX, topY); + if (showTR) drawCorner('right', rightX, topY); + if (showBL) drawCorner('left', leftX, bottomY); + if (showBR) drawCorner('right', rightX, bottomY); + } + } this.drawStreetNamesOnMainCanvas(this.ctx, tileSize, tileType, absCol, absRow); } } @@ -1172,6 +1276,18 @@ export default { } } }, + getTrafficLightFor(col, row) { + // Prüfe Tiles-Array, ob trafficLight-Spalte oder meta.trafficLight gesetzt ist + if (!this.currentMap || !Array.isArray(this.currentMap.tiles)) return false; + const found = this.currentMap.tiles.find(t => t.x === col && t.y === row && (t.trafficLight === true || (t.meta && t.meta.trafficLight))); + return !!found; + }, + getTrafficLightPhase(col, row) { + const key = `${col},${row}`; + const st = this.trafficLightStates[key]; + if (!st) return 0; // default + return st.phase || 0; + }, drawStreetNamesOnMainCanvas(ctx, size, tileType, col, row) { if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return; @@ -1260,6 +1376,55 @@ export default { ctx.restore(); } }, + // Haltelinien: 120px vom Rand, 5px dick, 40px breit, an allen vier Zufahrten + drawStopLinesOnMainCanvas(ctx, size, approaches = { top:true, right:true, bottom:true, left:true }) { + const margin = 120; // Basis-Abstand vom Rand + const extra = 30; // 10px zurück (von +40 auf +30) + const m = margin + extra; + const thickness = 5; + const width = 60; // +20px breiter (vorher 40) + ctx.save(); + ctx.fillStyle = '#ffffff'; + // Oben (horizontale Linie, zentriert) + if (approaches.top) ctx.fillRect((size - width) / 2 - 30, m - thickness, width, thickness); + // Unten + if (approaches.bottom) ctx.fillRect((size - width) / 2 + 30, size - m, width, thickness); + // Links (vertikale Linie, zentriert) + if (approaches.left) ctx.fillRect(m - thickness, (size - width) / 2 + 30, thickness, width); + // Rechts + if (approaches.right) ctx.fillRect(size - m, (size - width) / 2 - 30, thickness, width); + ctx.restore(); + }, + // Liefert, von welchen Seiten eine Straße an dieses Tile anbindet + getTileApproaches(tileType) { + switch (tileType) { + case 'horizontal': return { top:false, right:true, bottom:false, left:true }; + case 'vertical': return { top:true, right:false, bottom:true, left:false }; + case 'cross': return { top:true, right:true, bottom:true, left:true }; + case 'cornertopleft': return { top:true, right:false, bottom:false, left:true }; + case 'cornertopright': return { top:true, right:true, bottom:false, left:false }; + case 'cornerbottomleft': return { top:false, right:false, bottom:true, left:true }; + case 'cornerbottomright': return { top:false, right:true, bottom:true, left:false }; + case 'tup': return { top:true, right:true, bottom:false, left:true }; // T-oben: unten gesperrt + case 'tdown': return { top:false, right:true, bottom:true, left:true }; // T-unten: oben gesperrt + case 'tleft': return { top:true, right:false, bottom:true, left:true }; // T-links: rechts gesperrt + case 'tright': return { top:true, right:true, bottom:true, left:false }; // T-rechts: links gesperrt + case 'fuelhorizontal': return { top:false, right:true, bottom:false, left:true }; + case 'fuelvertical': return { top:true, right:false, bottom:true, left:false }; + default: return { top:true, right:true, bottom:true, left:true }; + } + }, + // Ecken-spezifische Ausblendungen je Tiletyp (zusätzlich zu Seiten-Logik) + getCornerHidesForTile(tileType) { + // TL=oben links, TR=oben rechts, BL=unten links, BR=unten rechts + switch (tileType) { + case 'tdown': return { TL: true, TR: false, BL: false, BR: false }; // oben gesperrt -> oben links weg + case 'tup': return { TL: false, TR: false, BL: false, BR: true }; // unten gesperrt -> unten rechts weg + case 'tleft': return { TL: false, TR: true, BL: false, BR: false }; // rechts gesperrt -> oben rechts weg + case 'tright': return { TL: false, TR: false, BL: true, BR: false }; // links gesperrt -> unten links weg + default: return { TL: false, TR: false, BL: false, BR: false }; + } + }, getTileType(row, col, rows, cols) { // Ecken @@ -1410,6 +1575,19 @@ export default { img.src = `/images/taxi/${tileName}.svg`; this.tiles.images[tileName] = img; } + + // Lade Ampel-Icon (zentral, 50% Größe) + const red = new Image(); + red.src = '/images/taxi/redlight.svg'; + this.tiles.images['redlight-svg'] = red; + + // Lade Trafficlight-Zustände (für großes Tile an den Ecken) + const tlStates = ['trafficlight-red','trafficlight-yellow','trafficlight-redyellow','trafficlight-green']; + for (const key of tlStates) { + const img = new Image(); + img.src = `/images/taxi/${key}.svg`; + this.tiles.images[key] = img; + } }, loadTaxiImage() { @@ -1539,6 +1717,15 @@ export default { // Straßennamen-Nummern zeichnen, basierend auf tileStreets (nur einmal pro Name) this.drawStreetNumbersOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow, drawnNames); + + // Ampel im Minimap zentriert (50% Größe) + if (this.getTrafficLightFor(absCol, absRow)) { + const img = this.tiles.images['redlight-svg']; + if (img && img.complete) { + const s = tileSize * 0.3; // 60% der bisherigen 50%-Größe + ctx.drawImage(img, x + (tileSize - s) / 2, y + (tileSize - s) / 2, s, s); + } + } } } } @@ -1748,7 +1935,36 @@ export default { if (t.h && t.v) { addEdge(keyH, keyV); } } if (nodes.size === 0) continue; - let startKey = null; for (const k of nodes.keys()) { if ((adj.get(k)?.size || 0) === 1) { startKey = k; break; } } if (!startKey) startKey = Array.from(nodes.keys()).sort()[0]; + // Startknoten deterministisch wählen: + // 1) bevorzuge Endpunkte (Grad=1) + // 2) bevorzuge horizontale Segmente (axis==='h') und wähle den mit kleinstem x (links) + // 3) sonst vertikale Segmente (axis==='v') mit kleinstem y (oben) + const nodeEntries = Array.from(nodes.entries()); + const deg = (k) => (adj.get(k)?.size || 0); + let candidates = nodeEntries.filter(([k]) => deg(k) === 1); + if (candidates.length === 0) candidates = nodeEntries; + let horiz = candidates.filter(([, n]) => n.axis === 'h'); + let startPair = null; + if (horiz.length > 0) { + // Links-vorne nimmt Vorrang (kleinstes x, dann kleinstes y) + startPair = horiz.reduce((best, curr) => { + if (!best) return curr; + const bn = best[1], cn = curr[1]; + if (cn.x < bn.x) return curr; + if (cn.x === bn.x && cn.y < bn.y) return curr; + return best; + }, null); + } else { + const vert = candidates.filter(([, n]) => n.axis === 'v'); + startPair = vert.reduce((best, curr) => { + if (!best) return curr; + const bn = best[1], cn = curr[1]; + if (cn.y < bn.y) return curr; + if (cn.y === bn.y && cn.x < bn.x) return curr; + return best; + }, null) || candidates[0]; + } + let startKey = startPair ? startPair[0] : (nodeEntries[0] && nodeEntries[0][0]); const visited = new Set(); let odd = 1, even = 2; let prev = null; let curr = startKey; let currOddSide = (nodes.get(curr).axis === 'h') ? 'top' : 'left'; while (curr) { visited.add(curr); @@ -1773,8 +1989,15 @@ export default { }); if (!housesFiltered.length) { prev = curr; curr = next || null; continue; } const orderCorners = (list) => { - if (axis === 'h') { const movingRight = dir.dx > 0 || (dir.dx === 0 && !prev); const seq = movingRight ? ['lo','ro','lu','ru'] : ['ro','lo','ru','lu']; return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); } - else { const movingDown = dir.dy > 0 || (dir.dy === 0 && !prev); const seq = movingDown ? ['lo','lu','ro','ru'] : ['lu','lo','ru','ro']; return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); } + if (axis === 'h') { + // Erzwinge Nummerierung beginnend von links nach rechts + const seq = ['lo','ro','lu','ru']; + return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); + } else { + const movingDown = dir.dy > 0 || (dir.dy === 0 && !prev); + const seq = movingDown ? ['lo','lu','ro','ru'] : ['lu','lo','ru','ro']; + return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); + } }; const oddCorners = (axis === 'h') ? (currOddSide === 'top' ? ['lo','ro'] : ['lu','ru']) : (currOddSide === 'left' ? ['lo','lu'] : ['ro','ru']); const evenCorners = (axis === 'h') ? (currOddSide === 'top' ? ['lu','ru'] : ['lo','ro']) : (currOddSide === 'left' ? ['ro','ru'] : ['lo','lu']); @@ -1854,6 +2077,10 @@ export default { font-weight: 500; } +.redlight-counter { display: inline-flex; align-items: center; gap: 4px; margin-right: 8px; } +.redlight-icon { font-size: 10pt; } +.redlight-value { font-weight: 700; min-width: 16px; text-align: right; } + .tacho-icon { font-size: 10pt; }