Files
landingpage/games/fussball.html
2026-02-06 22:14:01 +01:00

612 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elfmeterschießen Simulator</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #222;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: white;
overflow: hidden;
}
#game-container {
position: relative;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
border: 4px solid #fff;
border-radius: 8px;
overflow: hidden;
}
canvas {
display: block;
background: linear-gradient(to bottom, #87CEEB 0%, #87CEEB 30%, #2E8B57 30%, #228B22 100%);
cursor: crosshair;
}
#ui-overlay {
position: absolute;
top: 20px;
left: 0;
width: 100%;
text-align: center;
pointer-events: none;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
h1 {
margin: 0;
font-size: 24px;
text-transform: uppercase;
letter-spacing: 2px;
}
#score-board {
font-size: 18px;
margin-top: 5px;
font-weight: bold;
color: #ffeb3b;
}
#message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 48px;
font-weight: 900;
opacity: 0;
transition: opacity 0.3s;
text-shadow: 0 0 10px rgba(0,0,0,0.5);
pointer-events: none;
}
.instruction {
position: absolute;
bottom: 20px;
width: 100%;
text-align: center;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
pointer-events: none;
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="ui-overlay">
<h1>Elfmeter Simulator</h1>
<div id="score-board">Tore: 0 | Versuche: 0</div>
</div>
<div id="message">TOOR!</div>
<div class="instruction">Klicke irgendwo ins Tor, um zu schießen!</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const messageEl = document.getElementById('message');
const scoreEl = document.getElementById('score-board');
// Zuschauer-Daten (statisch)
const spectators = [];
// Spielzustand
let score = 0;
let attempts = 0;
let isBallFlying = false;
let showMessageTimer = null;
let frameCount = 0; // Für Animationen
// Ball Physik
const startBallY = 520;
const startBallX = 400;
const ball = {
x: startBallX,
y: startBallY,
z: 0,
radius: 15,
targetX: 0,
targetY: 0,
speed: 0
};
// Torwart (Verbessert)
const goalie = {
x: 400,
baseY: 230, // Bodenlinie
y: 230, // Aktuelle Y Position
width: 50, // Körperbreite
height: 90,
speed: 1.5,
direction: 1,
diveTargetX: 0,
diveTargetY: 0,
isDiving: false,
angle: 0 // Rotation für Hechtsprung
};
// Tordimensionen
const goal = {
topY: 130,
bottomY: 230,
topLeftX: 250,
topRightX: 550,
bottomLeftX: 230,
bottomRightX: 570
};
// Hilfsfunktionen
function initSpectators() {
for (let y = 55; y < 220; y += 20) {
for (let x = 10; x < canvas.width; x += 15 + Math.random() * 10) {
spectators.push({
x: x,
y: y,
color: `hsl(${Math.random() * 360}, 70%, 60%)`
});
}
}
}
function resetBall() {
isBallFlying = false;
ball.x = startBallX;
ball.y = startBallY;
ball.z = 0;
ball.radius = 15;
// Torwart zurücksetzen
goalie.isDiving = false;
goalie.x = 400;
goalie.y = goalie.baseY;
goalie.angle = 0;
}
function showStatus(text, color) {
messageEl.innerText = text;
messageEl.style.color = color;
messageEl.style.opacity = 1;
clearTimeout(showMessageTimer);
showMessageTimer = setTimeout(() => {
messageEl.style.opacity = 0;
resetBall();
}, 2000);
}
function updateScore() {
scoreEl.innerText = `Tore: ${score} | Versuche: ${attempts}`;
}
// --- Zeichnen ---
function drawStands() {
ctx.fillStyle = "#555";
ctx.fillRect(0, 50, canvas.width, 180);
ctx.fillStyle = "#777";
for (let y = 60; y < 230; y += 20) {
ctx.fillRect(0, y, canvas.width, 10);
}
for (const spectator of spectators) {
ctx.fillStyle = spectator.color;
ctx.beginPath();
ctx.arc(spectator.x, spectator.y, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "#333";
ctx.fillRect(0, 0, canvas.width, 50);
ctx.strokeStyle = "#222";
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(0, 50);
ctx.lineTo(canvas.width, 50);
ctx.stroke();
}
function drawPitch() {
ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(100, 600);
ctx.lineTo(200, 230);
ctx.lineTo(600, 230);
ctx.lineTo(700, 600);
ctx.stroke();
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(400, 480, 3, 0, Math.PI * 2);
ctx.fill();
}
function drawGoal() {
ctx.strokeStyle = "white";
ctx.lineWidth = 8;
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(goal.topLeftX + 20, goal.topY - 10);
ctx.lineTo(goal.topRightX - 20, goal.topY - 10);
ctx.lineTo(goal.bottomRightX - 10, goal.bottomY);
ctx.moveTo(goal.topLeftX + 20, goal.topY - 10);
ctx.lineTo(goal.bottomLeftX + 10, goal.bottomY);
ctx.stroke();
ctx.strokeStyle = "rgba(255, 255, 255, 0.3)";
ctx.lineWidth = 1;
for (let i = goal.topLeftX; i < goal.topRightX; i += 20) {
ctx.beginPath();
ctx.moveTo(i, goal.topY);
ctx.lineTo(i + (i - 400) * 0.2, goal.bottomY);
ctx.stroke();
}
for (let j = goal.topY; j < goal.bottomY; j += 15) {
ctx.beginPath();
ctx.moveTo(goal.topLeftX, j);
ctx.lineTo(goal.topRightX, j);
ctx.stroke();
}
ctx.strokeStyle = "white";
ctx.lineWidth = 8;
ctx.beginPath();
ctx.moveTo(goal.bottomLeftX, goal.bottomY);
ctx.lineTo(goal.topLeftX, goal.topY);
ctx.lineTo(goal.topRightX, goal.topY);
ctx.lineTo(goal.bottomRightX, goal.bottomY);
ctx.stroke();
}
function drawGoalie() {
ctx.save();
// Position setzen (Mitte des Torsos ca.)
let drawX = goalie.x;
let drawY = goalie.y - 45; // Etwas höher als die Füße
ctx.translate(drawX, drawY);
ctx.rotate(goalie.angle); // Rotation für Hechtsprung
// Farben
const skinColor = "#ffccaa";
const shirtColor = "#FFD700"; // Gelb
const shortsColor = "#111"; // Schwarz
// --- Zeichne Körperteile (Menschlicher) ---
// Kopf
ctx.fillStyle = skinColor;
ctx.beginPath();
ctx.arc(0, -35, 12, 0, Math.PI * 2); // Relativ zur Mitte
ctx.fill();
// Körper (Trikot)
ctx.fillStyle = shirtColor;
// Abgerundetes Rechteck für Torso
ctx.beginPath();
ctx.roundRect(-18, -25, 36, 45, 5);
ctx.fill();
// Hose
ctx.fillStyle = shortsColor;
ctx.beginPath();
ctx.roundRect(-18, 15, 36, 15, 5);
ctx.fill();
// Gliedmaßen Animation
ctx.lineCap = "round";
ctx.lineJoin = "round";
// Beine
ctx.strokeStyle = skinColor; // Beine Hautfarbe oder Socken? Sagen wir schwarz für Stutzen
ctx.lineWidth = 10;
ctx.strokeStyle = "#111"; // Stutzen/Hose Verlängerung
ctx.beginPath();
if (goalie.isDiving) {
// Beine strecken beim Sprung
ctx.moveTo(-10, 30); ctx.lineTo(-15, 60); // Linkes Bein
ctx.moveTo(10, 30); ctx.lineTo(15, 60); // Rechtes Bein
} else {
// Beine leicht gebeugt beim Stehen (Idle Animation)
const kneeBend = Math.sin(frameCount * 0.1) * 2;
ctx.moveTo(-10, 30); ctx.lineTo(-12, 55 + kneeBend);
ctx.moveTo(10, 30); ctx.lineTo(12, 55 + kneeBend);
}
ctx.stroke();
// Arme (Trikot Farbe Ärmel + Haut)
// Wir zeichnen vereinfacht ganze Arme in Hautfarbe mit gelben Ärmeln
// Ärmel
ctx.strokeStyle = shirtColor;
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(-15, -20); ctx.lineTo(-25, -5); // Links
ctx.moveTo(15, -20); ctx.lineTo(25, -5); // Rechts
ctx.stroke();
// Unterarme / Hände
ctx.strokeStyle = skinColor;
ctx.lineWidth = 10;
ctx.beginPath();
if (goalie.isDiving) {
// Arme weit strecken zum Ball
// Wenn wir nach rechts springen (angle > 0), ist "oben" relativ zur Rotation
// Da wir das ganze Canvas rotiert haben, zeichnen wir einfach Arme nach "oben" (über den Kopf)
ctx.moveTo(-25, -5); ctx.lineTo(-35, -40); // Arme hochreißen
ctx.moveTo(25, -5); ctx.lineTo(35, -40);
// Handschuhe (Weiß)
ctx.strokeStyle = "white";
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-35, -40); ctx.lineTo(-37, -45);
ctx.moveTo(35, -40); ctx.lineTo(37, -45);
ctx.stroke();
} else {
// Arme bereit halten (Idle)
const armWave = Math.cos(frameCount * 0.1) * 3;
ctx.moveTo(-25, -5); ctx.lineTo(-35 - armWave, 10);
ctx.moveTo(25, -5); ctx.lineTo(35 + armWave, 10);
// Handschuhe
ctx.strokeStyle = "white";
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(-35 - armWave, 10); ctx.lineTo(-37 - armWave, 15);
ctx.moveTo(35 + armWave, 10); ctx.lineTo(37 + armWave, 15);
ctx.stroke();
}
ctx.stroke();
ctx.restore();
}
function drawBall() {
ctx.save();
ctx.translate(ball.x, ball.y);
if (isBallFlying) {
const rotation = Date.now() / 100;
ctx.rotate(rotation);
}
ctx.fillStyle = "white";
ctx.beginPath();
ctx.arc(0, 0, ball.radius, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(0, 0, ball.radius * 0.5, 0, Math.PI * 2, false);
ctx.fill();
for(let i=0; i<5; i++) {
ctx.beginPath();
ctx.arc(
Math.cos(i * 2 * Math.PI / 5) * ball.radius * 0.7,
Math.sin(i * 2 * Math.PI / 5) * ball.radius * 0.7,
ball.radius * 0.2, 0, Math.PI*2
);
ctx.fill();
}
if (!isBallFlying || ball.z < 0.1) {
ctx.restore();
ctx.fillStyle = "rgba(0,0,0,0.3)";
ctx.beginPath();
ctx.ellipse(ball.x + 5, ball.y + ball.radius + 2, ball.radius, ball.radius * 0.3, 0, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.restore();
}
}
// --- Logik ---
function update() {
frameCount++;
// Torwart Idle Bewegung (Automatisch hin und her)
if (!isBallFlying && !goalie.isDiving) {
goalie.x += goalie.speed * goalie.direction;
// Leichte Wippbewegung im Stand
goalie.y = goalie.baseY + Math.sin(frameCount * 0.2) * 2;
goalie.angle = 0; // Aufrecht stehen
if (goalie.x > goal.bottomRightX - 40 || goalie.x < goal.bottomLeftX + 40) {
goalie.direction *= -1;
}
}
else if (isBallFlying && goalie.isDiving) {
// HECHT-LOGIK
// X Bewegung
const dx = goalie.diveTargetX - goalie.x;
goalie.x += dx * 0.08; // Sanfteres Gleiten
// Y Bewegung (Sprung)
const dy = goalie.diveTargetY - goalie.y;
goalie.y += dy * 0.08;
// Rotation berechnen (Hecht)
// Wenn er weit zur Seite muss (>50px vom Zentrum), dreht er sich
const distFromCenter = goalie.diveTargetX - 400;
let targetAngle = 0;
if (Math.abs(distFromCenter) > 40) {
// 90 Grad (PI/2) nach rechts oder links
// Wir nehmen etwas weniger als 90 Grad für realistischeren Sprung (ca 70 Grad)
targetAngle = (distFromCenter > 0) ? 1.2 : -1.2;
} else {
// Bei zentralen Bällen nur leicht neigen oder aufrecht bleiben
targetAngle = (distFromCenter > 0) ? 0.2 : -0.2;
}
// Winkel interpolieren
goalie.angle += (targetAngle - goalie.angle) * 0.1;
}
// Ball Bewegung
if (isBallFlying) {
ball.z += 0.05;
const progress = ball.z;
ball.x = startBallX + (ball.targetX - startBallX) * progress;
ball.y = startBallY + (ball.targetY - startBallY) * progress;
ball.radius = 15 - (7 * progress);
if (progress >= 1) {
isBallFlying = false;
checkResult();
}
}
}
function checkResult() {
// Verbesserte Kollisionserkennung für den neuen Körper
// Wir prüfen die Distanz zum Zentrum des Torsos
// Da er sich dreht, ist eine einfache Box schwer, wir nehmen eine etwas größere Kreis-Distanz an
// Torso Position anpassen für Hitbox
let hitX = goalie.x;
let hitY = goalie.y - 40; // Körpermitte
const distX = Math.abs(ball.x - hitX);
const distY = Math.abs(ball.y - hitY);
// Hitbox
let caught = false;
// Körpertreffer
if (distX < 45 && distY < 45) caught = true;
// Optional: Wenn er hechtet, deckt er mehr Breite aber weniger Höhe ab
// Das simulieren wir einfachheitshalber über den distX Parameter oben, der schon relativ generisch ist.
if (caught) {
showStatus("GEHALTEN!", "#ff4444");
return;
}
// Tor Check
const inGoalX = ball.x > goal.topLeftX && ball.x < goal.topRightX;
const inGoalY = ball.y > goal.topY && ball.y < goal.bottomY;
if (inGoalX && inGoalY) {
score++;
showStatus("TOOOOR!", "#44ff44");
} else {
showStatus("VORBEI!", "#aaaaaa");
}
updateScore();
}
function gameLoop() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawStands();
// Boden
ctx.fillStyle = "#2E8B57";
ctx.fillRect(0, 230, canvas.width, canvas.height - 230);
drawPitch();
drawGoal();
// Torwart zeichnen (Reihenfolge wichtig für Tiefe)
// Wenn Ball hinten ist (z nahe 1), ist Ball HINTER Torwart? Nein, Ball fliegt auf Tor zu.
// Im echten Leben steht Torwart VOR der Linie.
// Also: Ball (wenn weit weg) -> Torwart -> Ball (wenn nah)
// Hier einfachheitshalber: Torwart vor Netz, Ball davor.
drawGoalie();
drawBall();
update();
requestAnimationFrame(gameLoop);
}
// Input Handler
function handleInput(x, y) {
if (isBallFlying || messageEl.style.opacity == "1") return;
isBallFlying = true;
attempts++;
updateScore();
ball.targetX = x;
ball.targetY = y;
// KI Entscheidung
goalie.isDiving = true;
// Fehlerquote
const errorMargin = (Math.random() * 200) - 100;
// Ziel für den Torwart setzen
goalie.diveTargetX = ball.targetX + errorMargin;
// Er soll auch springen (Y-Achse)!
// Ziel-Y ist Ballhöhe, aber begrenzt durch Boden
// Wir addieren etwas Offset, da er "hoch" springen muss um an hohe Bälle zu kommen
let jumpHeight = ball.targetY;
if (jumpHeight > goalie.baseY) jumpHeight = goalie.baseY; // Nicht in den Boden graben
goalie.diveTargetY = jumpHeight + 40; // +40 weil seine Koordinaten bei den Füßen sind
// Begrenzen
if (goalie.diveTargetX < goal.bottomLeftX - 50) goalie.diveTargetX = goal.bottomLeftX - 50;
if (goalie.diveTargetX > goal.bottomRightX + 50) goalie.diveTargetX = goal.bottomRightX + 50;
}
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const clickX = (e.clientX - rect.left) * scaleX;
const clickY = (e.clientY - rect.top) * scaleY;
handleInput(clickX, clickY);
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const touch = e.touches[0];
const clickX = (touch.clientX - rect.left) * scaleX;
const clickY = (touch.clientY - rect.top) * scaleY;
handleInput(clickX, clickY);
}, {passive: false});
initSpectators();
updateScore();
gameLoop();
</script>
</body>
</html>