pong
This commit is contained in:
BIN
Archive.zip
Normal file
BIN
Archive.zip
Normal file
Binary file not shown.
33
sys_digger/beschreibung.md
Normal file
33
sys_digger/beschreibung.md
Normal 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
57
thiola-pong/constants.js
Normal 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>1–2% 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
451
thiola-pong/game.js
Normal 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();
|
||||
})();
|
||||
217
thiola-pong/pong-thiola.html
Normal file
217
thiola-pong/pong-thiola.html
Normal 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>
|
||||
BIN
thiola-pong/thiola-pong-konzept.docx
Normal file
BIN
thiola-pong/thiola-pong-konzept.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user