(() => { const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; // DOM const overlay = document.getElementById('overlay'); const startBtnCpu = document.getElementById('start-btn-cpu'); const startBtnPvp = document.getElementById('start-btn-pvp'); const hudLevel = document.getElementById('hud-level'); const hudShield = document.getElementById('hud-shield'); const hudPScore = document.getElementById('hud-pscore'); const hudCScore = document.getElementById('hud-cscore'); const popup = document.getElementById('popup'); const popupTitle = document.getElementById('popup-title'); const popupContent = document.getElementById('popup-content'); const popupSub = document.getElementById('popup-sub'); const popupBtn = document.getElementById('popup-btn'); const goalFlash = document.getElementById('goal-flash'); const flashScore = document.getElementById('flash-score'); const flashIcon = document.getElementById('flash-icon'); const flashTitle = document.getElementById('flash-title'); const flashText = document.getElementById('flash-text'); // ─── Game state ─── let level = 1, playerScore = 0, cpuScore = 0; let running = false, paused = false; let gameMode = 'cpu'; // 'cpu' or 'pvp' let ballX, ballY, ballVX, ballVY; let playerY = H/2 - PADDLE_H/2, cpuY = H/2 - PADDLE_H/2; let cpuSpeed = 1.6; let coveragePercent = 0, gapTop, gapBottom; let keyW = false, keyS = false; let keyUp = false, keyDown = false, mouseY = null; let particles = [], trails = []; let glowPhase = 0; let playerMsgIndex = 0, cpuMsgIndex = 0; // ─── Level config ─── function getCoverage(lvl) { const v = LEVEL_COVERAGE[lvl]; return v !== undefined ? v : 70; } function getCpuSpeed(lvl) { const v = LEVEL_CPU_SPEED[lvl]; return v !== undefined ? v : 3.8; } // ─── Helpers ─── function calcGap() { const openH = H * ((100 - coveragePercent) / 100); gapTop = (H - openH) / 2; gapBottom = gapTop + openH; } function resetBall(dir) { ballX = W / 2; ballY = H / 2; const angle = (Math.random() * 0.7 - 0.35); const speed = BASE_SPEED + level * 0.2; ballVX = speed * dir; ballVY = speed * Math.sin(angle); } function spawnParticles(x, y, color, count) { for (let i = 0; i < count; i++) { particles.push({ x, y, vx: (Math.random()-.5)*7, vy: (Math.random()-.5)*7, life: 1, decay: 0.02+Math.random()*0.03, r: 2+Math.random()*3, color }); } } function updateHUD() { hudLevel.textContent = level; hudShield.textContent = Math.round(coveragePercent); hudPScore.textContent = playerScore; hudCScore.textContent = cpuScore; } function startLevel() { playerScore = 0; cpuScore = 0; coveragePercent = getCoverage(level); cpuSpeed = getCpuSpeed(level); calcGap(); resetBall(1); updateHUD(); } // ─── Score a goal ─── function scoreGoal(isPlayerGoal) { if (isPlayerGoal) playerScore++; else cpuScore++; updateHUD(); void hudPScore.offsetHeight; showGoalFlash(isPlayerGoal); } // ─── Goal flash (edu per goal) ─── function showGoalFlash(isPlayerGoal) { paused = true; let msg; if (isPlayerGoal) { msg = playerGoalMessages[playerMsgIndex % playerGoalMessages.length]; playerMsgIndex++; } else { msg = cpuGoalMessages[cpuMsgIndex % cpuGoalMessages.length]; cpuMsgIndex++; } flashScore.textContent = `${playerScore} : ${cpuScore}`; flashIcon.textContent = msg.icon; flashTitle.textContent = msg.title; flashText.innerHTML = msg.text; void goalFlash.offsetHeight; goalFlash.className = isPlayerGoal ? 'show player-goal' : 'show cpu-goal'; function cont(e) { if (e && e.type === 'keydown') { const k = e.key; if (k === 'ArrowUp' || k === 'ArrowDown' || k === 'w' || k === 'W' || k === 's' || k === 'S') return; } goalFlash.className = ''; document.removeEventListener('keydown', cont); goalFlash.removeEventListener('click', cont); if (playerScore >= WIN_SCORE) { advanceLevel(); } else if (cpuScore >= WIN_SCORE) { showGameOver(); } else { resetBall(isPlayerGoal ? -1 : 1); setTimeout(() => { paused = false; }, BALL_RESUME_DELAY); } } setTimeout(() => { document.addEventListener('keydown', cont); goalFlash.addEventListener('click', cont); }, 400); } // ─── Popup helper ─── function showPopup(title, html, sub, btnText, btnClass, onContinue) { popupTitle.textContent = title; popupContent.innerHTML = html; popupSub.textContent = sub || ''; popupBtn.textContent = btnText; popupBtn.className = btnClass || 'game-btn'; popup.classList.add('show'); paused = true; popupBtn.onclick = () => { popup.classList.remove('show'); paused = false; popupBtn.blur(); if (onContinue) onContinue(); }; } // ─── Start screen ─── function showStartScreen() { running = false; paused = false; level = 1; coveragePercent = 0; popup.classList.remove('show'); overlay.classList.remove('hidden'); } // ─── Level transitions ─── function advanceLevel() { if (level === 1) { level = 2; showPopup('LEVEL 1 GESCHAFFT!', `
🛡️
Verstärkung kommt: Thiola!
Gut gespielt! Doch im nächsten Level wird der Gegner stärker.

Zum Glück bekommst du jetzt Unterstützung von ✦ THIOLA ✦

Thiola-Wände schützen Teile deines Tors und blockieren eingehende Bälle — genau wie Thiola (Tiopronin) die Neubildung von Cystinsteinen verhindert.
`, 'Level 2 — 40% Thiola-Schutz', 'MIT THIOLA WEITERSPIELEN', 'game-btn thiola', () => startLevel() ); } else if (level === 2) { level = 3; showPopup('LEVEL 2 GESCHAFFT!', `
💊
Thiola verstärkt den Schutz!
Der Gegner wird noch aggressiver — aber Thiola auch!

Im finalen Level schützt Thiola 70% deines Tors.
Konsequente Therapie = maximaler Schutz.
`, 'Finales Level — 70% Thiola-Schutz', 'THIOLA-SCHUTZ MAXIMIEREN', 'game-btn thiola', () => startLevel() ); } else { showFinalScreen(); } } function showFinalScreen() { showPopup('🏆 GEWONNEN!', `
ALLE 3 LEVEL GEMEISTERT!
Du hast erlebt, wie Thiola dein Tor Schritt für Schritt geschützt hat — genau so schützt Thiola (Tiopronin) Cystinurie-Patienten vor der Neubildung von Cystinsteinen.
🔬 Steinanalyse 🎯 Diagnose 💊 Thiola
Jeder Stein verdient eine Analyse.
Denn nur wer Cystinsteine erkennt, kann sie gezielt verhindern.
`, '', 'NOCHMAL SPIELEN', 'game-btn', () => showStartScreen() ); } function showGameOver() { showPopup(`LEVEL ${level} VERLOREN`, `
Nicht aufgeben!
Auch bei Cystinurie gilt: konsequente Therapie ist der Schlüssel.
Versuche es erneut — mit Thiola an deiner Seite!
`, 'Zurück zum Start', 'NOCHMAL VERSUCHEN', 'game-btn', () => showStartScreen() ); } // ─── Input ─── document.addEventListener('keydown', e => { if (e.key === 'ArrowUp') keyUp = true; if (e.key === 'ArrowDown') keyDown = true; if (e.key === 'w' || e.key === 'W') keyW = true; if (e.key === 's' || e.key === 'S') keyS = true; }); document.addEventListener('keyup', e => { if (e.key === 'ArrowUp') keyUp = false; if (e.key === 'ArrowDown') keyDown = false; if (e.key === 'w' || e.key === 'W') keyW = false; if (e.key === 's' || e.key === 'S') keyS = false; }); document.addEventListener('mousemove', e => { const rect = canvas.getBoundingClientRect(); mouseY = (e.clientY - rect.top) * (H / rect.height); }); // ─── Update ─── function update() { if (!running || paused) return; glowPhase += 0.03; const prevPlayerY = playerY; const prevCpuY = cpuY; // Player 1 (left) — PvP: W/S only; CPU mode: mouse OR W/S if (gameMode === 'pvp') { if (keyW) playerY -= 6; if (keyS) playerY += 6; } else { if (mouseY !== null) { playerY += (mouseY - PADDLE_H/2 - playerY) * 0.15; } else { if (keyUp || keyW) playerY -= 6; if (keyDown || keyS) playerY += 6; } } playerY = Math.max(0, Math.min(H - PADDLE_H, playerY)); // Player 2 / CPU (right) if (gameMode === 'pvp') { if (keyUp) cpuY -= 6; if (keyDown) cpuY += 6; } else { const diff = ballY - (cpuY + PADDLE_H/2); if (Math.abs(diff) > 10) cpuY += Math.sign(diff) * Math.min(cpuSpeed, Math.abs(diff) * 0.07); } cpuY = Math.max(0, Math.min(H - PADDLE_H, cpuY)); const playerVY = playerY - prevPlayerY; const cpuVY = cpuY - prevCpuY; // Ball movement ballX += ballVX; ballY += ballVY; if (ballY - BALL_R <= 0) { ballY = BALL_R; ballVY = Math.abs(ballVY); } if (ballY + BALL_R >= H) { ballY = H - BALL_R; ballVY = -Math.abs(ballVY); } trails.push({ x: ballX, y: ballY, life: 1 }); // Right (CPU) side if (ballX + BALL_R >= W - PADDLE_MARGIN - PADDLE_W) { if (ballX + BALL_R <= W - PADDLE_MARGIN + PADDLE_W && ballY >= cpuY && ballY <= cpuY + PADDLE_H) { ballVX = -Math.abs(ballVX) * 1.03; ballVY += ((ballY - cpuY)/PADDLE_H - 0.5) * 3 + cpuVY * PADDLE_SPIN_FACTOR; ballX = W - PADDLE_MARGIN - PADDLE_W - BALL_R; spawnParticles(ballX, ballY, '#ff6b35', 8); } else if (ballX > W + BALL_R) { spawnParticles(W, ballY, '#4ecdc4', 20); scoreGoal(true); } } // Left (Player) side + Thiola wall if (ballX - BALL_R <= PADDLE_MARGIN + PADDLE_W) { if (ballX - BALL_R >= PADDLE_MARGIN - PADDLE_W && ballY >= playerY && ballY <= playerY + PADDLE_H) { ballVX = Math.abs(ballVX) * 1.03; ballVY += ((ballY - playerY)/PADDLE_H - 0.5) * 3 + playerVY * PADDLE_SPIN_FACTOR; ballX = PADDLE_MARGIN + PADDLE_W + BALL_R; spawnParticles(ballX, ballY, '#4ecdc4', 8); } else if (ballX - BALL_R <= WALL_WIDTH && coveragePercent > 0) { if (ballY < gapTop || ballY > gapBottom) { ballVX = Math.abs(ballVX); ballX = WALL_WIDTH + BALL_R; spawnParticles(WALL_WIDTH, ballY, '#55efc4', 15); } else if (ballX - BALL_R <= 0) { spawnParticles(0, ballY, '#ff6b35', 20); scoreGoal(false); } } else if (ballX - BALL_R <= 0 && coveragePercent === 0) { spawnParticles(0, ballY, '#ff6b35', 20); scoreGoal(false); } } // Clamp speed const spd = Math.sqrt(ballVX*ballVX + ballVY*ballVY); if (spd > 12) { ballVX = (ballVX/spd)*12; ballVY = (ballVY/spd)*12; } particles = particles.filter(p => { p.x+=p.vx; p.y+=p.vy; p.life-=p.decay; p.vx*=.96; p.vy*=.96; return p.life>0; }); trails = trails.filter(t => { t.life -= 0.06; return t.life > 0; }); } // ─── Draw ─── function draw() { ctx.clearRect(0, 0, W, H); // Background grid ctx.strokeStyle = 'rgba(255,255,255,.02)'; ctx.lineWidth = 1; for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); } for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); } // Center line ctx.setLineDash([8,12]); ctx.strokeStyle='rgba(255,255,255,.08)'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(W/2,0); ctx.lineTo(W/2,H); ctx.stroke(); ctx.setLineDash([]); calcGap(); // Thiola walls if (coveragePercent > 0) { const pa = 0.7 + 0.3 * Math.sin(glowPhase); if (gapTop > 0) { drawThiolaWall(0, 0, WALL_WIDTH, gapTop, pa); drawThiolaText(WALL_WIDTH/2, gapTop/2, gapTop); } if (gapBottom < H) { drawThiolaWall(0, gapBottom, WALL_WIDTH, H-gapBottom, pa); drawThiolaText(WALL_WIDTH/2, gapBottom+(H-gapBottom)/2, H-gapBottom); } ctx.fillStyle='#fff'; ctx.shadowColor='#55efc4'; ctx.shadowBlur=14; ctx.fillRect(0, gapTop-3, WALL_WIDTH+6, 3); ctx.fillRect(0, gapBottom, WALL_WIDTH+6, 3); ctx.shadowBlur=0; } // Paddles const pG = ctx.createLinearGradient(PADDLE_MARGIN, 0, PADDLE_MARGIN+PADDLE_W, 0); pG.addColorStop(0,'#3ab0a5'); pG.addColorStop(1,'#4ecdc4'); ctx.fillStyle=pG; ctx.shadowColor='#4ecdc4'; ctx.shadowBlur=15; roundRect(PADDLE_MARGIN, playerY, PADDLE_W, PADDLE_H, 4); ctx.shadowBlur=0; const cG = ctx.createLinearGradient(W-PADDLE_MARGIN-PADDLE_W, 0, W-PADDLE_MARGIN, 0); cG.addColorStop(0,'#e85d2a'); cG.addColorStop(1,'#ff6b35'); ctx.fillStyle=cG; ctx.shadowColor='#ff6b35'; ctx.shadowBlur=15; roundRect(W-PADDLE_MARGIN-PADDLE_W, cpuY, PADDLE_W, PADDLE_H, 4); ctx.shadowBlur=0; // Ball trail trails.forEach(t => { ctx.beginPath(); ctx.arc(t.x, t.y, BALL_R*t.life*0.6, 0, Math.PI*2); ctx.fillStyle=`rgba(255,255,255,${t.life*0.12})`; ctx.fill(); }); // Ball ctx.beginPath(); ctx.arc(ballX, ballY, BALL_R, 0, Math.PI*2); ctx.fillStyle='#fff'; ctx.shadowColor='#fff'; ctx.shadowBlur=20; ctx.fill(); ctx.shadowBlur=0; // Particles particles.forEach(p => { ctx.beginPath(); ctx.arc(p.x, p.y, p.r*p.life, 0, Math.PI*2); ctx.fillStyle=p.color; ctx.globalAlpha=p.life; ctx.fill(); ctx.globalAlpha=1; }); // Big score overlay ctx.font='72px "Press Start 2P", monospace'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillStyle='rgba(78,205,196,.2)'; ctx.shadowColor='rgba(78,205,196,.08)'; ctx.shadowBlur=30; ctx.fillText(playerScore, W*0.3, H/2); ctx.shadowBlur=0; ctx.fillStyle='rgba(255,107,53,.2)'; ctx.shadowColor='rgba(255,107,53,.08)'; ctx.shadowBlur=30; ctx.fillText(cpuScore, W*0.7, H/2); ctx.shadowBlur=0; ctx.textBaseline='alphabetic'; } function drawThiolaWall(x, y, w, h, pulseAlpha) { const g = ctx.createLinearGradient(x, 0, x+w, 0); g.addColorStop(0, `rgba(0,184,148,${0.9*pulseAlpha})`); g.addColorStop(0.5, `rgba(85,239,196,${0.7*pulseAlpha})`); g.addColorStop(1, `rgba(78,205,196,${0.35*pulseAlpha})`); ctx.fillStyle=g; ctx.fillRect(x, y, w, h); ctx.fillStyle=`rgba(255,255,255,${0.06*pulseAlpha})`; ctx.fillRect(x+w-3, y, 3, h); } function drawThiolaText(cx, cy, segH) { if (segH < 50) return; ctx.save(); ctx.translate(cx, cy); ctx.rotate(-Math.PI/2); const fs = Math.min(18, segH * 0.14); ctx.font = `900 ${fs}px 'Orbitron', monospace`; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillStyle='rgba(0,0,0,.35)'; ctx.fillText('THIOLA', 1, 1); ctx.fillStyle='rgba(255,255,255,.93)'; ctx.shadowColor='rgba(85,239,196,.8)'; ctx.shadowBlur=12; ctx.fillText('THIOLA', 0, 0); ctx.shadowBlur=0; if (segH > 170) { ctx.fillStyle='rgba(255,255,255,.45)'; const off = segH * 0.3; ctx.fillText('THIOLA', -off, 0); ctx.fillText('THIOLA', off, 0); } ctx.restore(); } function roundRect(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x+r, y); ctx.lineTo(x+w-r, y); ctx.quadraticCurveTo(x+w, y, x+w, y+r); ctx.lineTo(x+w, y+h-r); ctx.quadraticCurveTo(x+w, y+h, x+w-r, y+h); ctx.lineTo(x+r, y+h); ctx.quadraticCurveTo(x, y+h, x, y+h-r); ctx.lineTo(x, y+r); ctx.quadraticCurveTo(x, y, x+r, y); ctx.closePath(); ctx.fill(); } // ─── Loop ─── function loop() { update(); draw(); requestAnimationFrame(loop); } function startGame(mode) { gameMode = mode; document.getElementById('hud-plabel').textContent = mode === 'pvp' ? 'SP1' : 'SPIELER'; document.getElementById('hud-clabel').textContent = mode === 'pvp' ? 'SP2' : 'CPU'; overlay.classList.add('hidden'); if (document.activeElement && document.activeElement.blur) { document.activeElement.blur(); } running = true; level = 1; coveragePercent = 0; playerMsgIndex = 0; cpuMsgIndex = 0; startLevel(); } startBtnCpu.addEventListener('click', () => startGame('cpu')); startBtnPvp.addEventListener('click', () => startGame('pvp')); calcGap(); resetBall(1); draw(); loop(); })();