managarten/games/arcade/apps/web/static/games/turbo_racer.html
Till JS 9e82e40e16 rename(mana-games): rebrand to Arcade
Rename games/mana-games/ to games/arcade/, update all package names
(@mana-games/* → @arcade/*), appIds, display names, docker-compose
service, root scripts, and documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:31:37 +02:00

791 lines
No EOL
27 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Turbo Racer</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a0a;
color: #fff;
font-family: 'Arial Black', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
.game-container {
position: relative;
filter: drop-shadow(0 0 30px rgba(255, 0, 100, 0.3));
}
canvas {
border: 3px solid #ff0066;
background: #1a1a1a;
box-shadow:
inset 0 0 50px rgba(255, 0, 100, 0.1),
0 0 30px rgba(0, 255, 255, 0.3);
}
.ui {
position: absolute;
top: 20px;
left: 20px;
font-size: 20px;
z-index: 10;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
.speed-meter {
color: #00ff88;
font-size: 28px;
margin-bottom: 10px;
font-style: italic;
}
.lap-counter {
color: #ffcc00;
margin-bottom: 10px;
}
.position {
color: #ff0066;
font-size: 32px;
font-weight: bold;
}
.boost-bar {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 200px;
height: 20px;
background: rgba(0,0,0,0.5);
border: 2px solid #00ffff;
border-radius: 10px;
overflow: hidden;
}
.boost-fill {
height: 100%;
background: linear-gradient(90deg, #00ffff, #ff00ff);
width: 100%;
transition: width 0.3s ease;
}
.start-screen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.9);
padding: 40px;
border: 3px solid #ff0066;
border-radius: 20px;
box-shadow: 0 0 30px rgba(255,0,100,0.5);
}
.start-screen h1 {
font-size: 48px;
margin-bottom: 20px;
background: linear-gradient(45deg, #ff0066, #00ffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: none;
}
.start-screen p {
font-size: 18px;
margin-bottom: 30px;
color: #ccc;
}
button {
padding: 15px 40px;
font-size: 24px;
font-weight: bold;
background: linear-gradient(45deg, #ff0066, #ff3388);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 2px;
}
button:hover {
transform: scale(1.1);
box-shadow: 0 0 20px rgba(255,0,100,0.5);
}
button:active {
transform: scale(0.95);
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.95);
padding: 40px;
border: 3px solid #ffcc00;
border-radius: 20px;
display: none;
}
.game-over h2 {
font-size: 36px;
margin-bottom: 20px;
color: #ffcc00;
}
.final-time {
font-size: 48px;
color: #00ff88;
margin-bottom: 20px;
}
.controls-info {
position: absolute;
bottom: 20px;
right: 20px;
font-size: 14px;
color: #666;
text-align: right;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="ui">
<div class="speed-meter">
<span id="speed">0</span> km/h
</div>
<div class="lap-counter">
Runde: <span id="lap">0</span>
</div>
<div class="position">
Zeit: <span id="time">0:00</span>
</div>
<div class="best-lap" style="color: #00ff88; font-size: 18px;">
Beste Runde: <span id="bestLap">--:--</span>
</div>
</div>
<div class="boost-bar">
<div class="boost-fill" id="boostFill"></div>
</div>
<div class="start-screen" id="startScreen">
<h1>TURBO RACER</h1>
<p>Drift durch die Kurven und stelle Bestzeiten auf!</p>
<p>🏁 Endlos-Runden • ⚡ Nitro-Boost • 🏆 Drift-Punkte</p>
<button onclick="startGame()">RENNEN STARTEN</button>
</div>
<div class="game-over" id="gameOver">
<h2 id="gameOverTitle">ZEIT-HERAUSFORDERUNG BEENDET!</h2>
<div class="final-time" id="finalTime">0 Runden</div>
<p id="finalPosition">Beste Runde: --:--</p>
<button onclick="restartGame()">NOCHMAL FAHREN</button>
</div>
<div class="controls-info">
↑↓ oder WS: Gas/Bremse | ←→ oder AD: Lenken | Leertaste: Boost
</div>
</div>
<script>
// Game ID für Statistiken
const GAME_ID = 'turbo-racer';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Spielvariablen
let gameRunning = false;
let raceStartTime = 0;
let currentLap = 1;
// Eingabe
const keys = {};
// Spieler Auto mit Drift-Physik
const player = {
x: 400,
y: 400,
angle: -Math.PI / 2, // Nach oben zeigend
velocity: { x: 0, y: 0 },
speed: 0,
maxSpeed: 4, // Noch langsamer
acceleration: 0.2, // Noch sanftere Beschleunigung
deceleration: 0.15,
turnSpeed: 0.08, // Noch weniger aggressiv
width: 30,
height: 20,
boost: 100,
boosting: false,
color: '#ff0066',
trail: [],
driftFactor: 0,
driftAngle: 0,
lapCount: 0,
bestLapTime: null,
currentLapStart: 0,
lastAngle: 0,
crossed: false
};
// Streckenmitte und Radien
const trackCenter = { x: 400, y: 300 };
const outerRadius = 250;
const innerRadius = 120;
const trackWidth = outerRadius - innerRadius;
// Zeit und Runden
let currentTime = 0;
let lastLapTime = 0;
let bestLapTime = Infinity;
// Partikel für Effekte
const particles = [];
// Sterne für Geschwindigkeitseffekt
const stars = [];
for (let i = 0; i < 50; i++) {
stars.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 2
});
}
// Input Handling
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
if (e.key === ' ') e.preventDefault();
});
document.addEventListener('keyup', (e) => {
keys[e.key.toLowerCase()] = false;
});
// Auto zeichnen
function drawCar(car, isPlayer = false) {
ctx.save();
ctx.translate(car.x, car.y);
ctx.rotate(car.angle);
// Drift-Rauch bei starkem Drift
if (isPlayer && player.driftFactor > 0.5) {
ctx.save();
ctx.globalAlpha = player.driftFactor * 0.3;
ctx.fillStyle = '#666';
ctx.beginPath();
ctx.arc(-car.width/2 - 5, 0, 8, 0, Math.PI * 2);
ctx.arc(car.width/2 + 5, 0, 8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Auto Body
ctx.fillStyle = car.color;
ctx.fillRect(-car.width/2, -car.height/2, car.width, car.height);
// Windschutzscheibe
ctx.fillStyle = 'rgba(100,100,100,0.8)';
ctx.fillRect(-car.width/4, -car.height/3, car.width/2, car.height/3);
// Räder
ctx.fillStyle = '#333';
ctx.fillRect(-car.width/2 - 2, -car.height/2 + 2, 4, 6);
ctx.fillRect(-car.width/2 - 2, car.height/2 - 8, 4, 6);
ctx.fillRect(car.width/2 - 2, -car.height/2 + 2, 4, 6);
ctx.fillRect(car.width/2 - 2, car.height/2 - 8, 4, 6);
// Spieler-Indikator
if (isPlayer) {
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, -car.height);
ctx.lineTo(-5, -car.height - 10);
ctx.lineTo(5, -car.height - 10);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
}
// Kreisförmige Strecke zeichnen
function drawTrack() {
// Hintergrund
ctx.fillStyle = '#0a3d0a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Äußerer Kreis (Strecke)
ctx.fillStyle = '#333';
ctx.beginPath();
ctx.arc(trackCenter.x, trackCenter.y, outerRadius, 0, Math.PI * 2);
ctx.fill();
// Innerer Kreis (Gras)
ctx.fillStyle = '#0a3d0a';
ctx.beginPath();
ctx.arc(trackCenter.x, trackCenter.y, innerRadius, 0, Math.PI * 2);
ctx.fill();
// Streckenbegrenzungen
ctx.strokeStyle = '#fff';
ctx.lineWidth = 4;
ctx.setLineDash([20, 10]);
// Äußere Linie
ctx.beginPath();
ctx.arc(trackCenter.x, trackCenter.y, outerRadius, 0, Math.PI * 2);
ctx.stroke();
// Innere Linie
ctx.beginPath();
ctx.arc(trackCenter.x, trackCenter.y, innerRadius, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
// Start/Ziel Linie
ctx.strokeStyle = '#fff';
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(trackCenter.x + innerRadius, trackCenter.y);
ctx.lineTo(trackCenter.x + outerRadius, trackCenter.y);
ctx.stroke();
// Schachbrettmuster auf Ziellinie
const lineWidth = outerRadius - innerRadius;
for (let i = 0; i < lineWidth / 10; i++) {
for (let j = 0; j < 2; j++) {
if ((i + j) % 2 === 0) {
ctx.fillStyle = '#fff';
ctx.fillRect(trackCenter.x + innerRadius + i * 10, trackCenter.y - 4 + j * 4, 10, 4);
}
}
}
// Driftspuren
if (player.driftFactor > 0.3) {
ctx.globalAlpha = player.driftFactor * 0.3;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
player.trail.forEach((point, i) => {
if (i > 0) {
ctx.beginPath();
ctx.moveTo(player.trail[i-1].x, player.trail[i-1].y);
ctx.lineTo(point.x, point.y);
ctx.stroke();
}
});
ctx.globalAlpha = 1;
}
}
// Kollisionserkennung mit kreisförmiger Strecke
function checkTrackCollision() {
const dx = player.x - trackCenter.x;
const dy = player.y - trackCenter.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Prüfe ob Auto außerhalb der Strecke
if (distance > outerRadius - 15 || distance < innerRadius + 15) {
return true;
}
return false;
}
// Rundenzählung
function checkLapCrossing() {
// Prüfe ob Ziellinie überquert wurde
const angle = Math.atan2(player.y - trackCenter.y, player.x - trackCenter.x);
const normalizedAngle = angle < 0 ? angle + Math.PI * 2 : angle;
// Ziellinie ist bei 0° (rechts)
if (normalizedAngle < 0.1 && player.lastAngle > 6.2) {
if (!player.crossed) {
player.crossed = true;
player.lapCount++;
// Rundenzeit berechnen
if (player.currentLapStart > 0) {
const lapTime = currentTime - player.currentLapStart;
if (lapTime < bestLapTime) {
bestLapTime = lapTime;
document.getElementById('bestLap').textContent = formatTime(bestLapTime);
}
}
player.currentLapStart = currentTime;
document.getElementById('lap').textContent = player.lapCount;
// Effekt für neue Runde
createParticles(player.x, player.y, '#00ff88');
}
} else if (normalizedAngle > 0.2) {
player.crossed = false;
}
player.lastAngle = normalizedAngle;
}
// Hilfsfunktion für Linien-Kreuzung
function lineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (denom === 0) return false;
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
}
// Spieler Update mit Drift-Physik
function updatePlayer() {
const oldAngle = player.angle;
// Steuerung
if (keys['arrowup'] || keys['w']) {
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed);
} else if (keys['arrowdown'] || keys['s']) {
player.speed = Math.max(player.speed - player.deceleration * 2, -player.maxSpeed / 2);
} else {
// Automatisches Abbremsen
if (player.speed > 0) {
player.speed = Math.max(0, player.speed - player.deceleration);
} else {
player.speed = Math.min(0, player.speed + player.deceleration);
}
}
// Lenken mit Drift
let turnAmount = 0;
if (Math.abs(player.speed) > 0.5) {
if (keys['arrowleft'] || keys['a']) {
turnAmount = -player.turnSpeed * (player.speed / player.maxSpeed);
player.angle += turnAmount;
}
if (keys['arrowright'] || keys['d']) {
turnAmount = player.turnSpeed * (player.speed / player.maxSpeed);
player.angle += turnAmount;
}
}
// Drift-Berechnung
const angleDiff = Math.abs(player.angle - oldAngle);
if (angleDiff > 0.04 && player.speed > 2.5) { // Angepasst für langsamere Geschwindigkeit
player.driftFactor = Math.min(1, player.driftFactor + 0.1);
player.driftAngle = player.angle - Math.atan2(player.velocity.y, player.velocity.x);
} else {
player.driftFactor = Math.max(0, player.driftFactor - 0.05);
}
// Velocity mit Drift
const targetVx = Math.cos(player.angle) * player.speed;
const targetVy = Math.sin(player.angle) * player.speed;
// Drift-Effekt: Velocity passt sich langsamer an die Richtung an
const driftStrength = 0.15 + (1 - player.driftFactor) * 0.1;
player.velocity.x += (targetVx - player.velocity.x) * driftStrength;
player.velocity.y += (targetVy - player.velocity.y) * driftStrength;
// Boost
if (keys[' '] && player.boost > 0 && player.speed > 2) {
player.boosting = true;
player.boost -= 2;
player.speed = Math.min(player.speed + 0.2, player.maxSpeed * 1.4); // Angepasster Boost für langsamere Geschwindigkeit
// Boost Partikel
createParticles(
player.x - Math.cos(player.angle) * 20,
player.y - Math.sin(player.angle) * 20,
'#00ffff'
);
} else {
player.boosting = false;
// Boost regeneriert langsam
player.boost = Math.min(100, player.boost + 0.3);
}
// Position update mit Velocity
const oldX = player.x;
const oldY = player.y;
player.x += player.velocity.x;
player.y += player.velocity.y;
// Kollision prüfen mit Abprall-Effekt
if (checkTrackCollision()) {
// Zurück zur alten Position
player.x = oldX;
player.y = oldY;
// Berechne Abprall-Richtung vom Streckenzentrum
const dx = player.x - trackCenter.x;
const dy = player.y - trackCenter.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Normalisiere und kehre Richtung um
let bounceX = dx / distance;
let bounceY = dy / distance;
// Wenn zu nah am Inneren, kehre um
if (distance < innerRadius + 15) {
bounceX = -bounceX;
bounceY = -bounceY;
}
// Wende Abprall-Kraft an
player.velocity.x = bounceX * player.speed * 0.6;
player.velocity.y = bounceY * player.speed * 0.6;
player.speed *= 0.5;
// Leichte Drehung beim Aufprall
player.angle += (Math.random() - 0.5) * 0.3;
// Kollisions-Partikel
createParticles(player.x, player.y, '#ff0066');
}
// Trail für Driftspuren
if (player.driftFactor > 0.3 && player.speed > 3) {
player.trail.push({x: player.x, y: player.y, life: 30});
if (player.trail.length > 100) {
player.trail.shift();
}
}
// Rundenzählung
checkLapCrossing();
// UI Update
document.getElementById('speed').textContent = Math.floor(Math.abs(player.speed) * 30); // Angepasst für noch langsamere Geschwindigkeit
document.getElementById('boostFill').style.width = player.boost + '%';
// Drift-Anzeige
if (player.driftFactor > 0.5) {
createParticles(player.x - 10, player.y - 10, '#ffcc00');
}
}
// Zeit formatieren
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const milliseconds = Math.floor((ms % 1000) / 10);
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
}
// Partikel erstellen
function createParticles(x, y, color) {
for (let i = 0; i < 5; i++) {
particles.push({
x: x,
y: y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
life: 20,
color: color
});
}
}
// Partikel update
function updateParticles() {
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.life--;
p.vx *= 0.95;
p.vy *= 0.95;
});
// Alte Partikel entfernen
for (let i = particles.length - 1; i >= 0; i--) {
if (particles[i].life <= 0) {
particles.splice(i, 1);
}
}
}
// Partikel zeichnen
function drawParticles() {
particles.forEach(p => {
ctx.globalAlpha = p.life / 20;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
});
ctx.globalAlpha = 1;
}
// Sterne für Geschwindigkeitseffekt
function drawStars() {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
stars.forEach(star => {
// Bewege Sterne basierend auf Spielergeschwindigkeit
star.y += player.speed * 0.5;
if (star.y > canvas.height) {
star.y = 0;
star.x = Math.random() * canvas.width;
}
ctx.globalAlpha = player.speed / player.maxSpeed * 0.5;
ctx.fillRect(star.x, star.y, star.size, star.size * 3);
});
ctx.globalAlpha = 1;
}
// Zeit-Update
function updateTime() {
if (gameRunning) {
currentTime += 16; // ~60 FPS
document.getElementById('time').textContent = formatTime(currentTime);
}
}
// Game Loop
function gameLoop() {
if (!gameRunning) return;
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw
drawStars();
drawTrack();
// Update
updatePlayer();
updateParticles();
updateTime();
// Draw car
drawCar(player, true);
// Effects
drawParticles();
requestAnimationFrame(gameLoop);
}
// Spiel starten
function startGame() {
document.getElementById('startScreen').style.display = 'none';
gameRunning = true;
currentTime = 0;
// Reset Spieler
player.x = trackCenter.x + (innerRadius + outerRadius) / 2;
player.y = trackCenter.y;
player.angle = -Math.PI / 2; // Nach oben zeigend
player.velocity = { x: 0, y: 0 };
player.speed = 0;
player.boost = 100;
player.lapCount = 0;
player.currentLapStart = 0;
player.bestLapTime = null;
player.trail = [];
player.driftFactor = 0;
player.lastAngle = 0;
player.crossed = false;
bestLapTime = Infinity;
document.getElementById('lap').textContent = '0';
document.getElementById('bestLap').textContent = '--:--';
// Event senden
window.parent.postMessage({
type: 'GAME_LOADED',
gameId: GAME_ID
}, '*');
gameLoop();
}
// Rennen beenden (optional - könnte nach X Runden aufgerufen werden)
function endRace() {
gameRunning = false;
document.getElementById('gameOverTitle').textContent = '🏆 ZEIT-HERAUSFORDERUNG BEENDET!';
document.getElementById('finalTime').textContent = `${player.lapCount} Runden`;
document.getElementById('finalPosition').textContent =
`Beste Runde: ${bestLapTime === Infinity ? '--:--' : formatTime(bestLapTime)}`;
document.getElementById('gameOver').style.display = 'block';
// Score basierend auf bester Rundenzeit
const score = bestLapTime === Infinity ? 0 : Math.max(0, 50000 - bestLapTime);
// Events senden
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'GAME_OVER',
data: {
score: Math.floor(score),
laps: player.lapCount,
bestLap: bestLapTime
}
}, '*');
// Achievements
if (bestLapTime < 15000) { // Unter 15 Sekunden
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'ACHIEVEMENT_UNLOCKED',
data: {
achievementId: 'drift_master',
name: 'Drift Master',
description: 'Schaffe eine Runde unter 15 Sekunden',
icon: '🏎️'
}
}, '*');
}
if (player.lapCount >= 10) {
window.parent.postMessage({
type: 'GAME_EVENT',
gameId: GAME_ID,
event: 'ACHIEVEMENT_UNLOCKED',
data: {
achievementId: 'endurance_racer',
name: 'Ausdauer-Rennfahrer',
description: 'Fahre 10 Runden in einer Session',
icon: '🎯'
}
}, '*');
}
}
// Neustart
function restartGame() {
document.getElementById('gameOver').style.display = 'none';
startGame();
}
</script>
</body>
</html>