diff --git a/frontend/src/views/minigames/TaxiGame.vue b/frontend/src/views/minigames/TaxiGame.vue index fd10073..42bc243 100644 --- a/frontend/src/views/minigames/TaxiGame.vue +++ b/frontend/src/views/minigames/TaxiGame.vue @@ -66,13 +66,18 @@
+ ❤️ {{ vehicleCount }} + ⛽ {{ Math.round(fuel) }}% + 📷 {{ speedViolations }} 🚦 {{ redLightViolations }} - ⏱️ - {{ taxi.speed * 5 }} - km/h + + ⏱️ + {{ taxi.speed * 5 }} + km/h +
@@ -143,15 +148,7 @@ {{ $t('minigames.taxi.passengers') }} -
- {{ crashes }} - Unfälle -
-
- {{ fuel }}% - {{ $t('minigames.taxi.fuel') }} -
@@ -244,8 +241,18 @@ export default { lastTrafficLightTick: 0, redLightViolations: 0, redLightLastCount: {}, + lastViolationSound: 0, lastMinimapDraw: 0, minimapDrawInterval: 120, + radarImg: null, + activeRadar: false, + radarAtTopEdge: true, // legacy flag (nicht mehr genutzt) + radarTicketShown: false, + speedViolations: 0, + radarAxis: null, // 'H' (horizontale Messlinie: y=konstant) oder 'V' (vertikale Messlinie: x=konstant) + radarLinePos: 0, + vehicleCount: 5, + redLightSincePenalty: 0, maps: [], // Geladene Maps aus der Datenbank currentMap: null, // Aktuell verwendete Map selectedMapId: null, // ID der ausgewählten Map @@ -466,36 +473,55 @@ export default { let newRow = this.currentTile.row; let newCol = this.currentTile.col; let moved = false; + let moveDir = null; // 'right'|'left'|'down'|'up' // Nach rechts: sobald rechte Kante den Rand erreicht (>=) if (this.taxi.x + this.taxi.width >= tileSize && newCol < cols - 1) { this.taxi.x = 0; // direkt an linken Rand des neuen Tiles newCol += 1; moved = true; + moveDir = 'right'; } // Nach links: sobald linke Kante den Rand erreicht (<= 0) if (!moved && this.taxi.x <= 0 && newCol > 0) { this.taxi.x = tileSize - this.taxi.width; // direkt an rechten Rand des neuen Tiles newCol -= 1; moved = true; + moveDir = 'left'; } // Nach unten: sobald untere Kante den Rand erreicht (>=) if (!moved && this.taxi.y + this.taxi.height >= tileSize && newRow < rows - 1) { this.taxi.y = 0; // direkt an oberen Rand des neuen Tiles newRow += 1; moved = true; + moveDir = 'down'; } // Nach oben: sobald obere Kante den Rand erreicht (<= 0) if (!moved && this.taxi.y <= 0 && newRow > 0) { this.taxi.y = tileSize - this.taxi.height; // direkt an unteren Rand des neuen Tiles newRow -= 1; moved = true; + moveDir = 'up'; } // Falls Wechsel möglich war, übernehme neue Tile-Position if (moved) { this.currentTile.row = newRow; this.currentTile.col = newCol; + // Radar mit 12% Wahrscheinlichkeit aktivieren beim Betreten des neuen Tiles + this.activeRadar = Math.random() < 0.12; + // Achse und Position festlegen: Messung findet bei 150px vom Rand statt + if (this.activeRadar) { + const size = this.tiles.size; + if (moveDir === 'right') { this.radarAxis = 'V'; this.radarLinePos = 150; } + else if (moveDir === 'left') { this.radarAxis = 'V'; this.radarLinePos = size - 150; } + else if (moveDir === 'down') { this.radarAxis = 'H'; this.radarLinePos = 150; } + else if (moveDir === 'up') { this.radarAxis = 'H'; this.radarLinePos = size - 150; } + else { this.radarAxis = 'H'; this.radarLinePos = 150; } + } else { + this.radarAxis = null; + } + this.radarTicketShown = false; return; } @@ -733,6 +759,9 @@ export default { this.checkCollisions(); this.render(); + // Radar-Messung prüfen (falls aktiv) + this.checkRadarMeasurement(); + // Minimap zeichnen (gedrosselt) const nowTs = Date.now(); if (nowTs - this.lastMinimapDraw >= this.minimapDrawInterval) { @@ -961,9 +990,108 @@ export default { if (now - last > 500) { // 0.5s Sperrzeit this.redLightLastCount[key] = now; this.redLightViolations += 1; + this.playRedLightSound(); + // Drei Rotlichtverstöße => Crash-Penalty + this.redLightSincePenalty = (this.redLightSincePenalty || 0) + 1; + if (this.redLightSincePenalty >= 3) { + this.redLightSincePenalty = 0; + this.crashes += 1; + this.decrementVehicle('redlight'); + const msg = 'Du hast wegen Rotlicht-Übertretungen ein Auto verloren.'; + const title = 'Hinweis'; + this.$root?.$refs?.messageDialog?.open?.(msg, title, {}, () => { + if (this.vehicleCount === 0) { + this.handleOutOfVehicles(); + } + }); + } } } }, + + checkRadarMeasurement() { + if (!this.activeRadar || this.radarTicketShown || !this.radarAxis) return; + const size = this.tiles.size; + const thresholdSpeedLevel = 10; // >50 km/h + const speedLevel = this.taxi.speed; + + 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; + + if (this.radarAxis === 'H') { + const y = this.radarLinePos; + // Richtung beachten: von oben nach unten über y (wenn y nahe oben), sonst umgekehrt + if (y <= size / 2) { + if (speedLevel > thresholdSpeedLevel && prevCY < y && currCY >= y) this.issueSpeedTicket(); + } else { + if (speedLevel > thresholdSpeedLevel && prevCY > y && currCY <= y) this.issueSpeedTicket(); + } + } else if (this.radarAxis === 'V') { + const x = this.radarLinePos; + if (x <= size / 2) { + if (speedLevel > thresholdSpeedLevel && prevCX < x && currCX >= x) this.issueSpeedTicket(); + } else { + if (speedLevel > thresholdSpeedLevel && prevCX > x && currCX <= x) this.issueSpeedTicket(); + } + } + }, + + issueSpeedTicket() { + this.radarTicketShown = true; + this.speedViolations += 1; + this.playCameraSound(); + }, + + playCameraSound() { + try { + this.ensureAudioUnlockedInEvent(); + if (!this.audioContext) return; + const now = this.audioContext.currentTime; + const osc = this.audioContext.createOscillator(); + const gain = this.audioContext.createGain(); + // kurzer Klick (Kamera-Shutter) + osc.type = 'square'; + osc.frequency.setValueAtTime(2400, now); + gain.gain.setValueAtTime(0, now); + gain.gain.linearRampToValueAtTime(0.4, now + 0.005); + gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.06); + osc.connect(gain).connect(this.audioContext.destination); + osc.start(now); + osc.stop(now + 0.07); + } catch (e) { + // ignore + } + }, + + playRedLightSound() { + // Ein kurzer Piepton über die bestehende AudioContext-Pipeline + try { + this.ensureAudioUnlockedInEvent(); + if (!this.audioContext) return; + const now = this.audioContext.currentTime; + // Entprellen global: max 1x pro 550ms (Ton ist länger) + const wall = (this._rlLastWall || 0); + if (now < wall) return; + this._rlLastWall = now + 0.55; + + const osc = this.audioContext.createOscillator(); + const gain = this.audioContext.createGain(); + osc.type = 'square'; + // etwas höher + osc.frequency.setValueAtTime(1320, now); + gain.gain.setValueAtTime(0, now); + // etwas lauter und länger + gain.gain.linearRampToValueAtTime(0.35, now + 0.01); + gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.5); + osc.connect(gain).connect(this.audioContext.destination); + osc.start(now); + osc.stop(now + 0.52); + } catch (e) { + // ignore + } + }, handleCrash() { // Verhindere mehrfache Crashes in kurzer Zeit @@ -1028,6 +1156,25 @@ export default { this.canvas.focus(); } console.log('Nach nextTick - isPaused:', this.isPaused); + if (this.vehicleCount === 0) { + this.handleOutOfVehicles(); + } + }); + }, + + decrementVehicle(reason = '') { + this.vehicleCount = Math.max(0, this.vehicleCount - 1); + }, + + handleOutOfVehicles() { + const title = 'Hinweis'; + const msg = 'Keine Fahrzeuge mehr. Neustart.'; + this.$root?.$refs?.messageDialog?.open?.(msg, title, {}, () => { + this.restartLevel(); + this.vehicleCount = 5; + this.crashes = 0; + this.redLightViolations = 0; + this.redLightSincePenalty = 0; }); }, @@ -1225,6 +1372,15 @@ export default { // Zeichne Straßen this.drawRoads(); + + // Radar-Icon oben rechts (50x50), wenn aktiv + if (this.activeRadar) { + const img = this.tiles.images['radar']; + if (img && img.complete) { + const size = 50; + this.ctx.drawImage(img, this.canvas.width - size - 4, 4, size, size); + } + } // Zeichne Hindernisse this.obstacles.forEach(obstacle => { @@ -1700,6 +1856,10 @@ export default { img.src = `/images/taxi/${key}.svg`; this.tiles.images[key] = img; } + // Radar-Icon + const radar = new Image(); + radar.src = '/images/taxi/radar.svg'; + this.tiles.images['radar'] = radar; }, loadTaxiImage() { @@ -2181,7 +2341,7 @@ export default { .tacho-speed { display: flex; - justify-content: flex-end; + justify-content: space-between; align-items: center; gap: 6px; font-size: 10pt; @@ -2192,11 +2352,17 @@ export default { .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; } +.speed-group { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; } +.redlight-counter { white-space: nowrap; } .tacho-icon { font-size: 10pt; } +.lives { font-weight: 700; color: #d60000; } +.fuel { font-weight: 600; color: #0a7c00; margin-left: 8px; } +.speed-violations { font-weight: 700; color: #8a2be2; margin-left: 8px; } + .speed-value { font-weight: bold; color: #2196F3;