diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index 8a132dd..fd10073 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -243,6 +243,9 @@ export default { trafficLightStates: {}, lastTrafficLightTick: 0, redLightViolations: 0, + redLightLastCount: {}, + lastMinimapDraw: 0, + minimapDrawInterval: 120, maps: [], // Geladene Maps aus der Datenbank currentMap: null, // Aktuell verwendete Map selectedMapId: null, // ID der ausgewählten Map @@ -256,6 +259,8 @@ export default { selectedStreetName: null ,motorStopTimeout: null ,houseNumbers: {} + ,prevTaxiX: 250 + ,prevTaxiY: 250 } }, computed: { @@ -728,8 +733,12 @@ export default { this.checkCollisions(); this.render(); - // Minimap zeichnen - this.drawMinimap(); + // Minimap zeichnen (gedrosselt) + const nowTs = Date.now(); + if (nowTs - this.lastMinimapDraw >= this.minimapDrawInterval) { + this.drawMinimap(); + this.lastMinimapDraw = nowTs; + } this.gameLoop = requestAnimationFrame(this.update); }, @@ -784,6 +793,9 @@ export default { this.lastSteerChange = currentTime; } + // Vorherige Position merken für Richtungs-/Grenzprüfungen + this.prevTaxiX = this.taxi.x; + this.prevTaxiY = this.taxi.y; // Aktualisiere Position (+20% Geschwindigkeit) this.taxi.x += Math.cos(this.taxi.angle) * this.taxi.speed * 0.12; this.taxi.y += Math.sin(this.taxi.angle) * this.taxi.speed * 0.12; @@ -860,6 +872,10 @@ export default { if (!this.isPaused && !this.isTaxiOnRoad()) { this.handleCrash(); } + // Rotlichtverstöße prüfen + if (!this.isPaused) { + this.checkRedLightViolation(); + } // Prüfe Hindernisse nur wenn das Spiel nicht pausiert ist if (!this.isPaused) { @@ -870,6 +886,84 @@ export default { }); } }, + // Prüft Überfahren der (virtuell verdoppelten) Haltelinie aus der straßenzugewandten Seite + checkRedLightViolation() { + if (!this.currentMap || !this.currentMap.mapData) return; + const tileSize = this.tiles.size; + const tileType = this.getCurrentTileType(); + const approaches = this.getTileApproaches(tileType); + if (!approaches.top && !approaches.right && !approaches.bottom && !approaches.left) return; + if (!this.getTrafficLightFor(this.currentTile.col + (this.currentMap.offsetX||0), this.currentTile.row + (this.currentMap.offsetY||0))) return; + + const phase = this.getTrafficLightPhase(this.currentTile.col + (this.currentMap.offsetX||0), this.currentTile.row + (this.currentMap.offsetY||0)); + const rects = this.getVirtualStopLineRects(tileSize, approaches); + + // Einseitige Prüfung je Richtung und Phasen-Logik + // Zu prüfende Ecken laut Vorgabe: + // Horizontal: links unten (BL) und rechts oben (TR) müssen rot sein, um Verstoß zu zählen + // Vertikal: rechts unten (BR) und links oben (TL) müssen rot sein + // Diagonale Synchronisierung: TL/BR sind vertikal gekoppelt, TR/BL horizontal + const isHorRed = (phase === 2 || phase === 3); + const isVerRed = (phase === 0 || phase === 1); + + let violated = false; + + // Von welcher Seite kommt das Taxi? Delta der Mittelpunkt-Position + const prevCX = this.prevTaxiX + this.taxi.width / 2; + const prevCY = this.prevTaxiY + this.taxi.height / 2; + const currCX = this.taxi.x + this.taxi.width / 2; + const currCY = this.taxi.y + this.taxi.height / 2; + const vx = currCX - prevCX; + const vy = currCY - prevCY; + + // Top-Band: von oben nach unten (prev oben, curr unten), Eintritt über obere Bandkante y0 + if (rects.top && approaches.top) { + const x0 = rects.top.x, x1 = rects.top.x + rects.top.width; + const y0 = rects.top.y, y1 = rects.top.y + rects.top.height; + const inX = (prevCX >= x0 && prevCX <= x1) || (currCX >= x0 && currCX <= x1); + if (vy > 0 && inX && prevCY < y0 && currCY >= y0) { + if (isVerRed) violated = true; + } + } + // Bottom-Band: von unten nach oben (prev unten, curr oben), Eintritt über untere Bandkante y1 + if (rects.bottom && approaches.bottom) { + const x0 = rects.bottom.x, x1 = rects.bottom.x + rects.bottom.width; + const y0 = rects.bottom.y, y1 = rects.bottom.y + rects.bottom.height; + const inX = (prevCX >= x0 && prevCX <= x1) || (currCX >= x0 && currCX <= x1); + if (vy < 0 && inX && prevCY > y1 && currCY <= y1) { + if (isVerRed) violated = true; + } + } + // Left-Band: von links nach rechts (prev links, curr rechts), Eintritt über linke Bandkante x0 + if (rects.left && approaches.left) { + const x0 = rects.left.x, x1 = rects.left.x + rects.left.width; + const y0 = rects.left.y, y1 = rects.left.y + rects.left.height; + const inY = (prevCY >= y0 && prevCY <= y1) || (currCY >= y0 && currCY <= y1); + if (vx > 0 && inY && prevCX < x0 && currCX >= x0) { + if (isHorRed) violated = true; + } + } + // Right-Band: von rechts nach links (prev rechts, curr links), Eintritt über rechte Bandkante x1 + if (rects.right && approaches.right) { + const x0 = rects.right.x, x1 = rects.right.x + rects.right.width; + const y0 = rects.right.y, y1 = rects.right.y + rects.right.height; + const inY = (prevCY >= y0 && prevCY <= y1) || (currCY >= y0 && currCY <= y1); + if (vx < 0 && inY && prevCX > x1 && currCX <= x1) { + if (isHorRed) violated = true; + } + } + + if (violated) { + // Entprellen: pro Tile nur einmal pro tatsächlichem Übertritt zählen + const key = `${this.currentTile.col},${this.currentTile.row}`; + const now = Date.now(); + const last = this.redLightLastCount[key] || 0; + if (now - last > 500) { // 0.5s Sperrzeit + this.redLightLastCount[key] = now; + this.redLightViolations += 1; + } + } + }, handleCrash() { // Verhindere mehrfache Crashes in kurzer Zeit @@ -1239,23 +1333,23 @@ export default { 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); + const drawByAxis = (axis, x, y) => { + // axis: 'H' (horizontal) oder 'V' (vertikal) + let img = imgRed; + if (axis === 'H') { + // Horizontal: Phase 0=grün, 1=gelb, 2=rot, 3=rot-gelb + if (phase === 0) img = imgGreen; else if (phase === 1) img = imgYellow; else if (phase === 2) img = imgRed; else img = imgRedYellow; + } else { + // Vertikal: Gegenphase: 0=rot, 1=rot-gelb, 2=grün, 3=gelb + 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); + }; + // Diagonale Synchronisation: TL/BR vertikal, TR/BL horizontal + if (showTL) drawByAxis('V', leftX, topY); + if (showTR) drawByAxis('H', rightX, topY); + if (showBL) drawByAxis('H', leftX, bottomY); + if (showBR) drawByAxis('V', rightX, bottomY); } } this.drawStreetNamesOnMainCanvas(this.ctx, tileSize, tileType, absCol, absRow); @@ -1395,6 +1489,24 @@ export default { if (approaches.right) ctx.fillRect(size - m, (size - width) / 2 - 30, thickness, width); ctx.restore(); }, + // Liefert die virtuellen (breit verdoppelten) Haltelinienrechtecke pro Seite, ohne zu zeichnen + getVirtualStopLineRects(size, approaches = { top:true, right:true, bottom:true, left:true }) { + const margin = 120; + const extra = 30; + const m = margin + extra; + const thickness = 5 * 2; // doppelte Breite für Erkennung + const width = 60; // Länge der Linie unverändert + const rects = {}; + // Oben + if (approaches.top) rects.top = { x: (size - width) / 2 - 30, y: m - thickness, width: width, height: thickness }; + // Unten + if (approaches.bottom) rects.bottom = { x: (size - width) / 2 + 30, y: size - m, width: width, height: thickness }; + // Links + if (approaches.left) rects.left = { x: m - thickness, y: (size - width) / 2 + 30, width: thickness, height: width }; + // Rechts + if (approaches.right) rects.right = { x: size - m, y: (size - width) / 2 - 30, width: thickness, height: width }; + return rects; + }, // Liefert, von welchen Seiten eine Straße an dieses Tile anbindet getTileApproaches(tileType) { switch (tileType) {