This commit is contained in:
verboomp
2026-04-21 12:49:06 +02:00
parent 04c91983e7
commit 5c989c5e70
6 changed files with 758 additions and 0 deletions

BIN
Archive.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,33 @@
# Sys Digger Spielbeschreibung
## Was ist Sys Digger?
Sys Digger ist ein browserbasiertes Reaktionsspiel, das lose auf dem bekannten Arcade-Klassiker **Whac-a-Mole** basiert. Statt Maulwürfe zu treffen, geht es hier jedoch um Chemie und Moleküle: Der Spieler muss ein zentrales Atom davor schützen, zu einem gefährlichen **Cystinstein** heranzuwachsen.
## Spielprinzip
Im Zentrum des Bildschirms befindet sich ein Molekül mit mehreren **Ankerpunkten** um sich herum. Von den Rändern des Bildschirms nähern sich fremde Atome und versuchen, an diesen Ankerpunkten anzudocken. Gelingt eine Verbindung, wächst das Molekül ein Stück weiter in Richtung Cystinstein.
Der Spieler muss dies verhindern und zwar durch gezieltes Klicken auf den leuchtend roten Ankerpunkt, bevor das anfliegende Atom dort andockt.
## Steuerung
- **Mausklick** auf einen rot markierten Ankerpunkt, sobald sich ein Atom nähert.
- Trifft der Klick rechtzeitig, wird die Verbindung blockiert und der Spieler erhält einen **Punkt**.
- Kommt der Klick zu spät oder wird der Ankerpunkt verfehlt, dockt das Atom an.
## Spielverlauf
- Je länger das Spiel andauert, desto schneller erscheinen neue Atome die Schwierigkeit steigt kontinuierlich.
- In der **oberen linken Ecke** befindet sich ein kleines Panel, das den aktuellen Zustand des Cystinsteins zeigt. Mit jeder erfolgreichen Andockung wächst dort ein neues Atom hinzu.
- Wurden **6 Andockungen** nicht verhindert, ist der Cystinstein vollständig gebildet das Spiel ist verloren.
## Highscore
Nach jedem Spiel kann der Spieler seinen Namen eingeben und seinen Punktestand in der **Bestenliste** speichern. Die Top-10-Ergebnisse werden dauerhaft gespeichert und sind beim nächsten Besuch wieder abrufbar.
Bleibt der Startbildschirm 30 Sekunden unberührt, wechselt das Spiel automatisch in eine **Highscore-Ansicht**, die die aktuelle Bestenliste anzeigt. Ein Klick oder ein Tastendruck bringt den Startbildschirm zurück.
## Ziel
So viele Andockversuche wie möglich abwehren, den Highscore knacken und verhindern, dass der Cystinstein entsteht.

57
thiola-pong/constants.js Normal file
View File

@@ -0,0 +1,57 @@
const PADDLE_W = 12;
const PADDLE_H = 90;
const PADDLE_MARGIN = 42;
const BALL_R = 7;
const BASE_SPEED = 5;
const WIN_SCORE = 5;
const MAX_LEVEL = 3;
const WALL_WIDTH = 32;
const LEVEL_COVERAGE = [0, 0, 40, 70];
const LEVEL_CPU_SPEED = [0, 1.6, 3.0, 3.8];
const playerGoalMessages = [
{ icon: '🔬', title: 'Steinanalyse rettet Leben',
text: 'Jeder Nierenstein sollte analysiert werden. Nur so kann ein <strong class="thl">Cystinstein</strong> sicher identifiziert werden.' },
{ icon: '🎯', title: 'Früherkennung ist der Schlüssel',
text: 'Je früher Cystinurie erkannt wird, desto besser die Prognose. Die <strong>Steinanalyse</strong> ist der erste Schritt zur Diagnose.' },
{ icon: '💊', title: 'Thiola senkt das Cystinniveau',
text: '<strong class="thl">Thiola (Tiopronin)</strong> senkt die Cystinkonzentration im Urin und <strong>verhindert die Neubildung</strong> von Steinen.' },
{ icon: '🛡️', title: 'Schutz durch konsequente Therapie',
text: 'Unter konsequenter <strong class="thl">Thiola</strong>-Therapie kann die Steinbildungsrate <strong>signifikant reduziert</strong> werden.' },
{ icon: '✅', title: 'Evidenzbasierter Therapiestandard',
text: '<strong class="thl">Tiopronin (Thiola)</strong> ist der <strong>Goldstandard</strong> in der medikamentösen Prävention von Cystinsteinen.' },
{ icon: '🔁', title: 'Rezidivprophylaxe mit Thiola',
text: 'Ohne spezifische Therapie liegt die <strong class="warn">Rezidivrate bei Cystinsteinen bei bis zu 60%</strong>. <strong class="thl">Thiola</strong> kann das verhindern.' },
{ icon: '📋', title: 'Leitliniengerecht handeln',
text: 'Die <strong>urologischen Leitlinien</strong> empfehlen eine konsequente <strong>Steinanalyse</strong> als Basis jeder Metaphylaxe.' },
{ icon: '🏆', title: 'Therapieerfolg messbar',
text: 'Unter <strong class="thl">Thiola</strong>-Therapie kann die Cystinausscheidung im Urin kontrolliert und der <strong>Therapieerfolg laborchemisch überwacht</strong> werden.' },
{ icon: '💎', title: 'Analyse schafft Klarheit',
text: 'Cystinsteine machen nur <strong>12% aller Nierensteine</strong> aus — ohne Analyse bleiben sie <strong class="warn">unerkannt</strong>.' },
{ icon: '🔑', title: 'Der Schlüssel zur Prävention',
text: '<strong>Steinanalyse → Diagnose → <span class="thl">Thiola</span></strong> — dieser Dreiklang schützt Patienten vor neuen Steinen.' },
];
const cpuGoalMessages = [
{ icon: '⚡', title: 'Kolikschmerzen — unerträglich',
text: 'Nierenkoliken gehören zu den <strong class="warn">stärksten Schmerzen</strong>, die ein Mensch erleben kann. Cystinstein-Patienten erleben das <strong>immer wieder</strong>.' },
{ icon: '🔄', title: 'Rezidive ohne Ende',
text: 'Ohne gezielte Therapie bilden sich Cystinsteine <strong class="warn">immer wieder neu</strong>. Bis zu <strong class="warn">60% Rezidivrate</strong> — ein Teufelskreis für Betroffene.' },
{ icon: '🏥', title: 'Wiederholte Operationen',
text: 'Viele Cystinurie-Patienten müssen <strong class="warn">mehrfach operiert</strong> werden. Jeder Eingriff belastet die Niere zusätzlich.' },
{ icon: '⏳', title: 'Diagnose oft viel zu spät',
text: 'Ohne Steinanalyse dauert es oft <strong class="warn">Jahre bis zur richtigen Diagnose</strong>. Wertvolle Zeit, in der die Niere Schaden nimmt.' },
{ icon: '🫘', title: 'Nierenfunktion in Gefahr',
text: 'Wiederholte Steinereignisse können zum <strong class="warn">Verlust der Nierenfunktion</strong> führen — besonders bei <strong>jungen Patienten</strong>.' },
{ icon: '👶', title: 'Junge Patienten betroffen',
text: 'Cystinurie manifestiert sich oft schon im <strong>Kindes- und Jugendalter</strong>. Betroffene leiden ihr <strong class="warn">ganzes Leben</strong>.' },
{ icon: '🧬', title: 'Genetisch bedingt — lebenslang',
text: 'Cystinurie ist eine <strong>autosomal-rezessiv vererbte</strong> Erkrankung. Die Steinbildung hört <strong class="warn">ohne Therapie nie auf</strong>.' },
{ icon: '😔', title: 'Lebensqualität massiv eingeschränkt',
text: 'Ständige Angst vor der nächsten Kolik, Krankenhausaufenthalte, OPs — die <strong class="warn">Lebensqualität sinkt drastisch</strong>.' },
{ icon: '❓', title: 'Fehlende Steinanalyse = Blindflug',
text: 'Ohne Steinanalyse wird <strong class="warn">symptomatisch statt kausal</strong> behandelt. Die wahre Ursache bleibt im Dunkeln.' },
{ icon: '📉', title: 'Kosten für das Gesundheitssystem',
text: 'Wiederholte Notaufnahmen, OPs und Krankheitstage — <strong class="warn">unerkannte Cystinurie</strong> verursacht immense Folgekosten.' },
];

451
thiola-pong/game.js Normal file
View File

@@ -0,0 +1,451 @@
(() => {
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 { paused = false; resetBall(isPlayerGoal ? -1 : 1); }
}
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!',
`<div class="edu-card">
<span class="edu-icon">🛡️</span>
<div class="edu-title">Verstärkung kommt: Thiola!</div>
<div class="edu-text">
Gut gespielt! Doch im nächsten Level wird der Gegner stärker.<br><br>
Zum Glück bekommst du jetzt Unterstützung von
<strong class="thl">✦ THIOLA ✦</strong><br><br>
Thiola-Wände schützen Teile deines Tors und blockieren eingehende Bälle —
genau wie <strong class="thl">Thiola (Tiopronin)</strong> die Neubildung von Cystinsteinen verhindert.
</div>
</div>`,
'Level 2 — 40% Thiola-Schutz',
'MIT THIOLA WEITERSPIELEN', 'game-btn thiola',
() => startLevel()
);
} else if (level === 2) {
level = 3;
showPopup('LEVEL 2 GESCHAFFT!',
`<div class="edu-card">
<span class="edu-icon">💊</span>
<div class="edu-title">Thiola verstärkt den Schutz!</div>
<div class="edu-text">
Der Gegner wird noch aggressiver — aber <strong class="thl">Thiola</strong> auch!<br><br>
Im finalen Level schützt Thiola <strong>70%</strong> deines Tors.<br>
<strong>Konsequente Therapie = maximaler Schutz.</strong>
</div>
</div>`,
'Finales Level — 70% Thiola-Schutz',
'THIOLA-SCHUTZ MAXIMIEREN', 'game-btn thiola',
() => startLevel()
);
} else {
showFinalScreen();
}
}
function showFinalScreen() {
showPopup('🏆 GEWONNEN!',
`<div class="final-summary">
<div class="final-title">ALLE 3 LEVEL GEMEISTERT!</div>
<div class="final-text">
Du hast erlebt, wie <strong style="color:#55efc4">Thiola</strong> dein Tor Schritt für Schritt geschützt hat —
genau so schützt <strong style="color:#55efc4">Thiola (Tiopronin)</strong> Cystinurie-Patienten
vor der Neubildung von Cystinsteinen.
</div>
<div class="pathway">
<span class="step">🔬 Steinanalyse</span>
<span class="arrow">→</span>
<span class="step">🎯 Diagnose</span>
<span class="arrow">→</span>
<span class="step" style="background:rgba(0,184,148,.2); border-color:rgba(0,184,148,.5)">💊 Thiola</span>
</div>
<div class="final-text" style="font-size:10px; color:#aaa; margin-top:6px">
<strong>Jeder Stein verdient eine Analyse.</strong><br>
Denn nur wer Cystinsteine erkennt, kann sie gezielt verhindern.
</div>
</div>`,
'', 'NOCHMAL SPIELEN', 'game-btn',
() => showStartScreen()
);
}
function showGameOver() {
showPopup(`LEVEL ${level} VERLOREN`,
`<div class="edu-card">
<span class="edu-icon">⚡</span>
<div class="edu-title">Nicht aufgeben!</div>
<div class="edu-text">
Auch bei Cystinurie gilt: <strong>konsequente Therapie</strong> ist der Schlüssel.<br>
Versuche es erneut — mit <strong class="thl">Thiola</strong> an deiner Seite!
</div>
</div>`,
'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;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouseY = (e.clientY - rect.top) * (H / rect.height);
});
canvas.addEventListener('mouseleave', () => { mouseY = null; });
// ─── Update ───
function update() {
if (!running || paused) return;
glowPhase += 0.03;
// 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));
// 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;
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;
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();
})();

View File

@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PONG Thiola Edition</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Orbitron:wght@400;700;900&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a12;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Orbitron', monospace;
overflow: hidden;
user-select: none;
}
#game-wrapper {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
#hud {
display: flex;
justify-content: space-between;
align-items: center;
width: 800px;
padding: 0 20px;
color: #ccc;
font-size: 11px;
letter-spacing: 1px;
}
#hud .level-display { color: #ff6b35; font-weight: 700; font-size: 13px; text-shadow: 0 0 10px rgba(255,107,53,.5); }
#hud .shield-display { color: #4ecdc4; font-size: 11px; }
#game-container {
position: relative;
border: 2px solid #1a1a2e;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 40px rgba(78,205,196,.08), 0 0 80px rgba(255,107,53,.05), inset 0 0 60px rgba(0,0,0,.5);
}
canvas { display: block; background: #0d0d1a; }
/* ─── Start overlay ─── */
#overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; justify-content: center; align-items: center;
background: rgba(10,10,18,.94); backdrop-filter: blur(8px);
z-index: 10; transition: opacity .4s;
}
#overlay.hidden { opacity: 0; pointer-events: none; }
#overlay h1 { font-family: 'Press Start 2P', monospace; font-size: 28px; color: #ff6b35; text-shadow: 0 0 20px rgba(255,107,53,.6); margin-bottom: 8px; letter-spacing: 4px; }
#overlay .subtitle { font-size: 12px; color: #4ecdc4; margin-bottom: 28px; letter-spacing: 2px; }
#overlay .info-box { background: rgba(255,255,255,.04); border: 1px solid rgba(78,205,196,.15); border-radius: 8px; padding: 18px 28px; margin-bottom: 28px; text-align: center; max-width: 520px; }
#overlay .info-box p { font-size: 10px; color: #999; line-height: 2.1; }
.hl { color: #ff6b35; font-weight: 700; }
.hl2 { color: #4ecdc4; font-weight: 700; }
/* ─── Buttons ─── */
.game-btn {
font-family: 'Orbitron', monospace; font-size: 13px; font-weight: 700; color: #0d0d1a;
background: linear-gradient(135deg, #ff6b35, #ff8c42); border: none;
padding: 14px 40px; border-radius: 6px; cursor: pointer; letter-spacing: 2px; text-transform: uppercase;
transition: transform .15s, box-shadow .15s; box-shadow: 0 4px 20px rgba(255,107,53,.3);
}
.game-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 30px rgba(255,107,53,.5); }
.game-btn.thiola { background: linear-gradient(135deg, #00b894, #55efc4); box-shadow: 0 4px 20px rgba(0,184,148,.3); }
.game-btn.thiola:hover { box-shadow: 0 6px 30px rgba(0,184,148,.5); }
/* ─── Full-screen popup (level transitions) ─── */
#popup {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; justify-content: center; align-items: center;
background: rgba(10,10,18,.94); backdrop-filter: blur(8px);
z-index: 20; opacity: 0; pointer-events: none; transition: opacity .4s; padding: 30px;
}
#popup.show { opacity: 1; pointer-events: auto; }
#popup h2 { font-family: 'Press Start 2P', monospace; font-size: 18px; color: #4ecdc4; text-shadow: 0 0 20px rgba(78,205,196,.5); margin-bottom: 18px; text-align: center; line-height: 1.5; }
.edu-card { background: rgba(255,255,255,.04); border: 1px solid rgba(78,205,196,.2); border-radius: 10px; padding: 24px 32px; max-width: 540px; margin-bottom: 22px; text-align: center; }
.edu-card .edu-icon { font-size: 36px; margin-bottom: 10px; display: block; }
.edu-card .edu-title { font-size: 12px; font-weight: 900; color: #4ecdc4; margin-bottom: 12px; letter-spacing: 1px; text-transform: uppercase; }
.edu-card .edu-text { font-size: 11px; color: #bbb; line-height: 2; }
.edu-card .edu-text strong { color: #fff; }
.edu-card .edu-text .thl { color: #55efc4; font-weight: 900; }
.edu-card .edu-text .warn { color: #ff6b35; font-weight: 700; }
#popup .popup-sub { font-size: 10px; color: #666; margin-bottom: 18px; }
.final-summary { background: linear-gradient(135deg, rgba(0,184,148,.08), rgba(78,205,196,.04)); border: 1px solid rgba(0,184,148,.25); border-radius: 12px; padding: 28px 36px; max-width: 560px; margin-bottom: 22px; text-align: center; }
.final-title { font-family: 'Press Start 2P', monospace; font-size: 14px; color: #55efc4; margin-bottom: 16px; text-shadow: 0 0 15px rgba(85,239,196,.4); }
.final-text { font-size: 11px; color: #ccc; line-height: 2; }
.final-text strong { color: #fff; }
.pathway { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 18px 0; font-size: 11px; font-weight: 700; flex-wrap: wrap; }
.pathway .step { background: rgba(78,205,196,.12); border: 1px solid rgba(78,205,196,.3); border-radius: 6px; padding: 8px 14px; color: #4ecdc4; white-space: nowrap; }
.pathway .arrow { color: #ff6b35; font-size: 16px; }
/* ─── Goal flash banner (in-game edu) ─── */
#goal-flash {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; justify-content: center; align-items: center;
z-index: 15; opacity: 0; pointer-events: none; transition: opacity .3s;
padding: 20px;
}
#goal-flash.show { opacity: 1; pointer-events: auto; }
#goal-flash.player-goal { background: rgba(0,30,25,.88); }
#goal-flash.cpu-goal { background: rgba(40,10,5,.88); }
#goal-flash .flash-score {
font-family: 'Press Start 2P', monospace;
font-size: 32px;
margin-bottom: 16px;
letter-spacing: 6px;
}
#goal-flash.player-goal .flash-score { color: #4ecdc4; text-shadow: 0 0 20px rgba(78,205,196,.6); }
#goal-flash.cpu-goal .flash-score { color: #ff6b35; text-shadow: 0 0 20px rgba(255,107,53,.6); }
#goal-flash .flash-card {
border-radius: 10px;
padding: 20px 32px;
max-width: 520px;
text-align: center;
margin-bottom: 18px;
}
#goal-flash.player-goal .flash-card { background: rgba(78,205,196,.08); border: 1px solid rgba(78,205,196,.25); }
#goal-flash.cpu-goal .flash-card { background: rgba(255,107,53,.06); border: 1px solid rgba(255,107,53,.2); }
.flash-card .flash-icon { font-size: 28px; margin-bottom: 8px; display: block; }
.flash-card .flash-title { font-size: 11px; font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; text-transform: uppercase; }
#goal-flash.player-goal .flash-title { color: #4ecdc4; }
#goal-flash.cpu-goal .flash-title { color: #ff6b35; }
.flash-card .flash-text { font-size: 11px; color: #ccc; line-height: 1.9; }
.flash-card .flash-text strong { color: #fff; }
.flash-card .flash-text .thl { color: #55efc4; font-weight: 900; }
.flash-card .flash-text .warn { color: #ff6b35; font-weight: 700; }
#goal-flash .flash-continue {
font-size: 10px; color: #666; letter-spacing: 1px;
animation: pulse-hint 1.5s ease-in-out infinite;
}
@keyframes pulse-hint { 0%,100% { opacity: .5; } 50% { opacity: 1; } }
#controls-hint { font-size: 9px; color: #444; letter-spacing: 1px; margin-top: 4px; }
</style>
</head>
<body>
<div id="game-wrapper">
<div id="hud">
<div class="level-display">LEVEL <span id="hud-level">1</span> / 3</div>
<div class="shield-display">THIOLA-SCHUTZ: <span id="hud-shield">0</span>%</div>
<div style="color:#aaa; font-size:12px;">
<span id="hud-plabel">SPIELER</span> <span id="hud-pscore" style="color:#4ecdc4; font-weight:700">0</span>
:
<span id="hud-cscore" style="color:#ff6b35; font-weight:700">0</span> <span id="hud-clabel">CPU</span>
</div>
</div>
<div id="game-container">
<canvas id="canvas" width="800" height="500"></canvas>
<div id="overlay">
<h1>PONG</h1>
<div class="subtitle">THIOLA EDITION</div>
<div class="info-box">
<p>
Nierensteine treffen auf dein Tor — kannst du sie abwehren?<br>
Gewinne <span class="hl">3 Level</span> mit jeweils <span class="hl">5 Punkten</span> Vorsprung.<br>
Ab Level 2 schützt <span class="hl2">Thiola</span> dein Tor!
</p>
</div>
<div style="font-size:11px; color:#888; margin-bottom:14px; letter-spacing:1px;">SPIELMODUS WÄHLEN</div>
<div style="display:flex; gap:14px; flex-wrap:wrap; justify-content:center;">
<button class="game-btn" id="start-btn-cpu">SPIELER VS. CPU</button>
<button class="game-btn thiola" id="start-btn-pvp">SPIELER VS. SPIELER</button>
</div>
<div id="controls-info" style="font-size:9px; color:#666; margin-top:18px; line-height:1.8; text-align:center;">
<span style="color:#4ecdc4">SPIELER 1 (links):</span> W / S oder Maus<br>
<span style="color:#ff6b35">SPIELER 2 (rechts):</span> ↑ / ↓ Pfeiltasten
</div>
</div>
<!-- Goal flash (in-game edu per goal) -->
<div id="goal-flash">
<div class="flash-score" id="flash-score"></div>
<div class="flash-card">
<span class="flash-icon" id="flash-icon"></span>
<div class="flash-title" id="flash-title"></div>
<div class="flash-text" id="flash-text"></div>
</div>
<div class="flash-continue">KLICKEN ODER TASTE DRÜCKEN</div>
</div>
<!-- Level transition popup -->
<div id="popup">
<h2 id="popup-title"></h2>
<div id="popup-content"></div>
<div class="popup-sub" id="popup-sub"></div>
<button class="game-btn" id="popup-btn">WEITER</button>
</div>
</div>
<div id="controls-hint">PONG · THIOLA EDITION</div>
</div>
<script src="constants.js"></script>
<script src="game.js"></script>
</body>
</html>

Binary file not shown.