Private
Public Access
1
0

big refactor
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Failing after 48s

This commit is contained in:
Sebastian Unterschütz
2025-11-25 18:11:47 +01:00
parent 6afcd4aa94
commit 732f507547
17 changed files with 1519 additions and 1295 deletions

View File

@@ -1,538 +1,328 @@
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('game-container');
(function() {
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const loadingText = document.getElementById('loadingText');
const gameOverScreen = document.getElementById('gameOverScreen');
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('game-container');
class PseudoRNG {
constructor(seed) {
this.state = BigInt(seed);
}
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const loadingText = document.getElementById('loadingText');
const gameOverScreen = document.getElementById('gameOverScreen');
nextFloat() {
const a = 1664525n;
const c = 1013904223n;
const m = 4294967296n;
this.state = (this.state * a + c) % m;
return Number(this.state) / Number(m);
}
nextRange(min, max) {
return min + (this.nextFloat() * (max - min));
}
pick(array) {
if (!array || array.length === 0) return null;
const idx = Math.floor(this.nextRange(0, array.length));
return array[idx];
}
}
const GAME_WIDTH = 800;
const GAME_HEIGHT = 400;
canvas.width = GAME_WIDTH;
canvas.height = GAME_HEIGHT;
const GRAVITY = 0.6;
const JUMP_POWER = -12;
const GROUND_Y = 350;
const GAME_SPEED = 5;
const CHUNK_SIZE = 60;
let gameConfig = null;
let isLoaded = false;
let isGameRunning = false;
let isGameOver = false;
let sessionID = null;
let rng = null;
let score = 0;
let currentTick = 0;
let lastSentTick = 0;
let inputLog = [];
let isCrouching = false;
let sprites = {};
let playerSprite = new Image();
let bgSprite = new Image();
let player = {
x: 50, y: 300, w: 30, h: 50, color: "red",
vy: 0, grounded: false
};
let obstacles = [];
function resize() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
let finalWidth, finalHeight;
if (windowWidth / windowHeight < targetRatio) {
finalWidth = windowWidth;
finalHeight = windowWidth / targetRatio;
} else {
finalHeight = windowHeight;
finalWidth = finalHeight * targetRatio;
}
canvas.style.width = `${finalWidth}px`;
canvas.style.height = `${finalHeight}px`;
if(container) {
container.style.width = `${finalWidth}px`;
container.style.height = `${finalHeight}px`;
}
}
window.addEventListener('resize', resize);
resize();
async function loadAssets() {
playerSprite.src = "assets/player.gif";
if (gameConfig.backgrounds && gameConfig.backgrounds.length > 0) {
const bgName = gameConfig.backgrounds[0];
if (!bgName.startsWith("#")) {
bgSprite.src = "assets/" + bgName;
class PseudoRNG {
constructor(seed) {
this.state = BigInt(seed);
}
nextFloat() {
const a = 1664525n; const c = 1013904223n; const m = 4294967296n;
this.state = (this.state * a + c) % m;
return Number(this.state) / Number(m);
}
nextRange(min, max) {
return min + (this.nextFloat() * (max - min));
}
pick(array) {
if (!array || array.length === 0) return null;
const idx = Math.floor(this.nextRange(0, array.length));
return array[idx];
}
}
const promises = gameConfig.obstacles.map(def => {
return new Promise((resolve) => {
if (!def.image) { resolve(); return; }
const img = new Image();
img.src = "assets/" + def.image;
img.onload = () => { sprites[def.id] = img; resolve(); };
img.onerror = () => { resolve(); };
// Config
const GAME_WIDTH = 800; const GAME_HEIGHT = 400;
canvas.width = GAME_WIDTH; canvas.height = GAME_HEIGHT;
const GRAVITY = 0.6; const JUMP_POWER = -12; const GROUND_Y = 350;
const GAME_SPEED = 5; const CHUNK_SIZE = 60;
// State (JETZT PRIVATE VARIABLEN!)
let gameConfig = null;
let isLoaded = false;
let isGameRunning = false;
let isGameOver = false;
let sessionID = null;
let rng = null;
let score = 0;
let currentTick = 0;
let lastSentTick = 0;
let inputLog = [];
let isCrouching = false;
let sprites = {};
let playerSprite = new Image();
let bgSprite = new Image();
let player = { x: 50, y: 300, w: 30, h: 50, color: "red", vy: 0, grounded: false };
let obstacles = [];
let serverObstacles = [];
// --- Funktionen ---
function resize() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
let finalWidth, finalHeight;
if (windowWidth / windowHeight < targetRatio) {
finalWidth = windowWidth; finalHeight = windowWidth / targetRatio;
} else {
finalHeight = windowHeight; finalWidth = finalHeight * targetRatio;
}
canvas.style.width = `${finalWidth}px`;
canvas.style.height = `${finalHeight}px`;
if(container) { container.style.width = `${finalWidth}px`; container.style.height = `${finalHeight}px`; }
}
window.addEventListener('resize', resize); resize();
async function loadAssets() {
playerSprite.src = "assets/player.png";
if (gameConfig.backgrounds && gameConfig.backgrounds.length > 0) {
const bgName = gameConfig.backgrounds[0];
if (!bgName.startsWith("#")) bgSprite.src = "assets/" + bgName;
}
const promises = gameConfig.obstacles.map(def => {
return new Promise((resolve) => {
if (!def.image) { resolve(); return; }
const img = new Image(); img.src = "assets/" + def.image;
img.onload = () => { sprites[def.id] = img; resolve(); };
img.onerror = () => { resolve(); };
});
});
});
if (bgSprite.src) {
promises.push(new Promise(r => {
bgSprite.onload = r;
bgSprite.onerror = r;
}));
}
await Promise.all(promises);
}
window.startGameClick = async function() {
if (!isLoaded) return;
startScreen.style.display = 'none';
try {
const sRes = await fetch('/api/start', {method:'POST'});
const sData = await sRes.json();
sessionID = sData.sessionId;
rng = new PseudoRNG(sData.seed);
isGameRunning = true;
} catch(e) {
location.reload();
}
};
function handleInput(action, active) {
if (isGameOver) { if(active) location.reload(); return; }
const relativeTick = currentTick - lastSentTick;
if (action === "JUMP" && active) {
if (player.grounded && !isCrouching) {
player.vy = JUMP_POWER;
player.grounded = false;
inputLog.push({ t: relativeTick, act: "JUMP" });
if (bgSprite.src) {
promises.push(new Promise(r => { bgSprite.onload = r; bgSprite.onerror = r; }));
}
await Promise.all(promises);
}
if (action === "DUCK") {
isCrouching = active;
}
}
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true);
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false);
});
window.addEventListener('mousedown', (e) => {
if (e.target === canvas && e.button === 0) {
handleInput("JUMP", true);
}
});
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
if(e.target === canvas) {
e.preventDefault();
touchStartY = e.touches[0].clientY;
}
}, { passive: false });
window.addEventListener('touchend', (e) => {
if(e.target === canvas) {
e.preventDefault();
const touchEndY = e.changedTouches[0].clientY;
const diff = touchEndY - touchStartY;
if (diff < -30) {
handleInput("JUMP", true);
} else if (diff > 30) {
handleInput("DUCK", true);
setTimeout(() => handleInput("DUCK", false), 800);
} else if (Math.abs(diff) < 10) {
handleInput("JUMP", true);
}
}
});
async function sendChunk() {
const ticksToSend = currentTick - lastSentTick;
if (ticksToSend <= 0) return;
const payload = {
sessionId: sessionID,
inputs: [...inputLog],
totalTicks: ticksToSend
// Global verfügbar machen für HTML Button
window.startGameClick = async function() {
if (!isLoaded) return;
startScreen.style.display = 'none';
try {
const sRes = await fetch('/api/start', {method:'POST'});
const sData = await sRes.json();
sessionID = sData.sessionId;
rng = new PseudoRNG(sData.seed);
isGameRunning = true;
} catch(e) { location.reload(); }
};
inputLog = [];
lastSentTick = currentTick;
try {
const res = await fetch('/api/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.status === "dead") {
gameOver("Vom Server gestoppt");
} else {
const sScore = data.verifiedScore;
if (Math.abs(score - sScore) > 200) {
score = sScore;
}
}
} catch (e) {
console.error(e);
}
}
function updateGameLogic() {
if (isCrouching) {
function handleInput(action, active) {
if (isGameOver) { if(active) location.reload(); return; }
const relativeTick = currentTick - lastSentTick;
inputLog.push({ t: relativeTick, act: "DUCK" });
}
const originalHeight = 50;
const crouchHeight = 25;
player.h = isCrouching ? crouchHeight : originalHeight;
let drawY = player.y;
if (isCrouching) {
drawY = player.y + (originalHeight - crouchHeight);
}
player.vy += GRAVITY;
if (isCrouching && !player.grounded) player.vy += 2.0;
player.y += player.vy;
if (player.y + originalHeight >= GROUND_Y) {
player.y = GROUND_Y - originalHeight;
player.vy = 0;
player.grounded = true;
} else {
player.grounded = false;
}
let nextObstacles = [];
let rightmostX = 0;
for (let obs of obstacles) {
obs.x -= GAME_SPEED;
const playerHitbox = {
x: player.x,
y: drawY,
w: player.w,
h: player.h
};
if (checkCollision(playerHitbox, obs)) {
player.color = "darkred";
if (!isGameOver) {
sendChunk();
gameOver("Kollision (Client)");
}
}
if (obs.x + obs.def.width > -100) {
nextObstacles.push(obs);
if (obs.x + obs.def.width > rightmostX) {
rightmostX = obs.x + obs.def.width;
if (action === "JUMP" && active) {
if (player.grounded && !isCrouching) {
player.vy = JUMP_POWER; player.grounded = false;
inputLog.push({ t: relativeTick, act: "JUMP" });
}
}
if (action === "DUCK") { isCrouching = active; }
}
obstacles = nextObstacles;
if (rightmostX < GAME_WIDTH - 10 && gameConfig && gameConfig.obstacles) {
const gap = Math.floor(400 + rng.nextRange(0, 500));
let spawnX = rightmostX + gap;
if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH;
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true);
});
window.addEventListener('keyup', (e) => {
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false);
});
window.addEventListener('mousedown', (e) => {
if (e.target === canvas && e.button === 0) handleInput("JUMP", true);
});
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
if(e.target === canvas) { e.preventDefault(); touchStartY = e.touches[0].clientY; }
}, { passive: false });
window.addEventListener('touchend', (e) => {
if(e.target === canvas) {
e.preventDefault();
const diff = e.changedTouches[0].clientY - touchStartY;
if (diff < -30) handleInput("JUMP", true);
else if (diff > 30) { handleInput("DUCK", true); setTimeout(() => handleInput("DUCK", false), 800); }
else if (Math.abs(diff) < 10) handleInput("JUMP", true);
}
});
let possibleObs = [];
gameConfig.obstacles.forEach(def => {
if (def.id === "eraser") {
if (score >= 500) possibleObs.push(def);
} else {
possibleObs.push(def);
async function sendChunk() {
const ticksToSend = currentTick - lastSentTick;
if (ticksToSend <= 0) return;
const payload = { sessionId: sessionID, inputs: [...inputLog], totalTicks: ticksToSend };
inputLog = []; lastSentTick = currentTick;
try {
const res = await fetch('/api/validate', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
const data = await res.json();
if (data.serverObs) serverObstacles = data.serverObs;
if (data.status === "dead") gameOver("Vom Server gestoppt");
else {
const sScore = data.verifiedScore;
if (Math.abs(score - sScore) > 200) score = sScore;
}
});
} catch (e) {}
}
const def = rng.pick(possibleObs);
function updateGameLogic() {
if (isCrouching) {
const relativeTick = currentTick - lastSentTick;
inputLog.push({ t: relativeTick, act: "DUCK" });
}
const originalHeight = 50; const crouchHeight = 25;
player.h = isCrouching ? crouchHeight : originalHeight;
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
player.vy += GRAVITY;
if (isCrouching && !player.grounded) player.vy += 2.0;
player.y += player.vy;
if (player.y + originalHeight >= GROUND_Y) {
player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true;
} else { player.grounded = false; }
let speech = null;
if (def && def.canTalk) {
if (rng.nextFloat() > 0.7) {
speech = rng.pick(def.speechLines);
let nextObstacles = []; let rightmostX = 0;
for (let obs of obstacles) {
obs.x -= GAME_SPEED;
const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
if (checkCollision(playerHitbox, obs)) {
player.color = "darkred";
if (!isGameOver) { sendChunk(); gameOver("Kollision"); }
}
if (obs.x + obs.def.width > -100) {
nextObstacles.push(obs);
if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width;
}
}
obstacles = nextObstacles;
if (def) {
const yOffset = def.yOffset || 0;
obstacles.push({
x: spawnX,
y: GROUND_Y - def.height - yOffset,
def: def,
speech: speech
if (rightmostX < GAME_WIDTH - 10 && gameConfig) {
const gap = Math.floor(400 + rng.nextRange(0, 500));
let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH;
let possibleObs = [];
gameConfig.obstacles.forEach(def => {
if (def.id === "eraser") { if (score >= 500) possibleObs.push(def); } else possibleObs.push(def);
});
const def = rng.pick(possibleObs);
let speech = null;
if (def && def.canTalk) { if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines); }
if (def) {
const yOffset = def.yOffset || 0;
obstacles.push({ x: spawnX, y: GROUND_Y - def.height - yOffset, def: def, speech: speech });
}
}
}
}
function checkCollision(p, obs) {
const paddingX = 5;
const paddingY_Top = 5;
const paddingY_Bottom = 5;
return (
p.x + p.w - paddingX > obs.x + paddingX &&
p.x + paddingX < obs.x + obs.def.width - paddingX &&
p.y + p.h - paddingY_Bottom > obs.y + paddingY_Top &&
p.y + paddingY_Top < obs.y + obs.def.height - paddingY_Bottom
);
}
window.submitScore = async function() {
const nameInput = document.getElementById('playerNameInput');
const name = nameInput.value;
const btn = document.getElementById('submitBtn');
if (!name) return alert("Namen eingeben!");
btn.disabled = true;
try {
const res = await fetch('/api/submit-name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sessionID, name: name })
});
const data = await res.json();
const claimCode = data.claimCode;
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
myClaims.push({
name: name,
score: Math.floor(score / 10),
code: claimCode,
date: new Date().toLocaleString('de-DE'),
sessionId: sessionID
});
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
alert(`Gespeichert!\nDein Beweis-Code: ${claimCode}\n(Findest du unter "Meine Codes")`);
document.getElementById('inputSection').style.display = 'none';
loadLeaderboard();
} catch (e) {
console.error(e);
alert("Fehler beim Speichern!");
btn.disabled = false;
}
};
async function loadLeaderboard() {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
let html = "<h3>BESTENLISTE</h3>";
entries.forEach(e => {
const color = e.isMe ? "yellow" : "white";
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;">
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span>
</div>`;
if(e.rank===3 && entries.length>3) html+="<div style='color:gray'>...</div>";
});
document.getElementById('leaderboard').innerHTML = html;
}
function gameOver(reason) {
if (isGameOver) return;
isGameOver = true;
const finalScoreVal = Math.floor(score / 10);
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
if (finalScoreVal > currentHighscore) {
localStorage.setItem('escape_highscore', finalScoreVal);
function checkCollision(p, obs) {
const paddingX = 10; const paddingY_Top = 25; const paddingY_Bottom = 5;
return (p.x + p.w - paddingX > obs.x + paddingX && p.x + paddingX < obs.x + obs.def.width - paddingX &&
p.y + p.h - paddingY_Bottom > obs.y + paddingY_Top && p.y + paddingY_Top < obs.y + obs.def.height - paddingY_Bottom);
}
gameOverScreen.style.display = 'flex';
document.getElementById('finalScore').innerText = finalScoreVal;
window.submitScore = async function() {
const nameInput = document.getElementById('playerNameInput');
const name = nameInput.value;
const btn = document.getElementById('submitBtn');
if (!name) return alert("Namen eingeben!");
btn.disabled = true;
try {
const res = await fetch('/api/submit-name', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ sessionId: sessionID, name: name })
});
const data = await res.json();
loadLeaderboard();
drawGame();
}
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
myClaims.push({
name: name, score: Math.floor(score / 10), code: data.claimCode,
date: new Date().toLocaleString('de-DE'), sessionId: sessionID
});
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
function drawGame() {
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
document.getElementById('inputSection').style.display = 'none';
loadLeaderboard();
} catch (e) {}
};
if (bgSprite.complete && bgSprite.naturalHeight !== 0) {
ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else {
const bgColor = (gameConfig && gameConfig.backgrounds) ? gameConfig.backgrounds[0] : "#eee";
if (bgColor.startsWith("#")) ctx.fillStyle = bgColor;
else ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
}
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
obstacles.forEach(obs => {
const img = sprites[obs.def.id];
if (img) {
ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height);
} else {
ctx.fillStyle = obs.def.color;
ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height);
}
if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech);
});
const drawY = isCrouching ? player.y + 25 : player.y;
const drawH = isCrouching ? 25 : 50;
if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
} else {
ctx.fillStyle = player.color;
ctx.fillRect(player.x, drawY, player.w, drawH);
}
if (isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
}
}
function drawSpeechBubble(x, y, text) {
const bX = x-20; const bY = y-40; const bW = 120; const bH = 30;
ctx.fillStyle="white"; ctx.fillRect(bX,bY,bW,bH);
ctx.strokeRect(bX,bY,bW,bH);
ctx.fillStyle="black"; ctx.font="10px Arial"; ctx.textAlign="center";
ctx.fillText(text, bX+bW/2, bY+20);
}
function gameLoop() {
if (!isLoaded) return;
if (isGameRunning && !isGameOver) {
updateGameLogic();
currentTick++;
score++;
const scoreEl = document.getElementById('score');
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
if (currentTick - lastSentTick >= CHUNK_SIZE) sendChunk();
}
drawGame();
requestAnimationFrame(gameLoop);
}
async function initGame() {
try {
const cRes = await fetch('/api/config');
gameConfig = await cRes.json();
await loadAssets();
await loadStartScreenLeaderboard();
isLoaded = true;
if(loadingText) loadingText.style.display = 'none';
if(startBtn) startBtn.style.display = 'inline-block';
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
const hsEl = document.getElementById('localHighscore');
if(hsEl) hsEl.innerText = savedHighscore;
requestAnimationFrame(gameLoop);
} catch(e) {
if(loadingText) loadingText.innerText = "Fehler!";
}
}
// Lädt die Top-Liste für den Startbildschirm (ohne Session ID)
async function loadStartScreenLeaderboard() {
try {
const listEl = document.getElementById('startLeaderboardList');
if (!listEl) return;
// Anfrage an API (ohne SessionID gibt der Server automatisch die Top 3 zurück)
const res = await fetch('/api/leaderboard');
async function loadLeaderboard() {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
if (entries.length === 0) {
listEl.innerHTML = "<div style='text-align:center; padding:20px; color:#666;'>Noch keine Scores.</div>";
return;
}
let html = "";
let html = "<h3>BESTENLISTE</h3>";
entries.forEach(e => {
// Medaillen Icons für Top 3
let icon = "#" + e.rank;
if (e.rank === 1) icon = "🥇";
if (e.rank === 2) icon = "🥈";
if (e.rank === 3) icon = "🥉";
html += `
<div class="hof-entry">
<span><span class="hof-rank">${icon}</span> ${e.name}</span>
<span class="hof-score">${Math.floor(e.score / 10)}</span>
</div>`;
const color = e.isMe ? "yellow" : "white";
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;">
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
});
listEl.innerHTML = html;
} catch (e) {
console.error("Konnte Leaderboard nicht laden", e);
document.getElementById('leaderboard').innerHTML = html;
}
}
initGame();
async function loadStartScreenLeaderboard() {
try {
const listEl = document.getElementById('startLeaderboardList');
if (!listEl) return;
const res = await fetch('/api/leaderboard');
const entries = await res.json();
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
let html = "";
entries.forEach(e => {
let icon = "#" + e.rank;
if (e.rank === 1) icon = "🥇"; if (e.rank === 2) icon = "🥈"; if (e.rank === 3) icon = "🥉";
html += `<div class="hof-entry"><span><span class="hof-rank">${icon}</span> ${e.name}</span><span class="hof-score">${Math.floor(e.score / 10)}</span></div>`;
});
listEl.innerHTML = html;
} catch (e) {}
}
function gameOver(reason) {
if (isGameOver) return;
isGameOver = true;
const finalScoreVal = Math.floor(score / 10);
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
if (finalScoreVal > currentHighscore) localStorage.setItem('escape_highscore', finalScoreVal);
gameOverScreen.style.display = 'flex';
document.getElementById('finalScore').innerText = finalScoreVal;
loadLeaderboard();
drawGame();
}
function drawGame() {
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
if (bgSprite.complete && bgSprite.naturalHeight !== 0) ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT);
else { ctx.fillStyle = "#f0f0f0"; ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT); }
ctx.fillStyle = "rgba(60, 60, 60, 0.8)"; ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
obstacles.forEach(obs => {
const img = sprites[obs.def.id];
if (img) ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height);
else { ctx.fillStyle = obs.def.color; ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height); }
if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech);
});
ctx.strokeStyle = isGameOver ? "red" : "lime"; ctx.lineWidth = 2;
serverObstacles.forEach(srvObs => ctx.strokeRect(srvObs.x, srvObs.y, srvObs.w, srvObs.h));
const drawY = isCrouching ? player.y + 25 : player.y; const drawH = isCrouching ? 25 : 50;
if (playerSprite.complete && playerSprite.naturalHeight !== 0) ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
else { ctx.fillStyle = player.color; ctx.fillRect(player.x, drawY, player.w, drawH); }
if (isGameOver) { ctx.fillStyle = "rgba(0,0,0,0.7)"; ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT); }
}
function drawSpeechBubble(x, y, text) {
const bX = x-20; const bY = y-40; const bW = 120; const bH = 30;
ctx.fillStyle="white"; ctx.fillRect(bX,bY,bW,bH); ctx.strokeRect(bX,bY,bW,bH);
ctx.fillStyle="black"; ctx.font="10px Arial"; ctx.textAlign="center"; ctx.fillText(text, bX+bW/2, bY+20);
}
function gameLoop() {
if (!isLoaded) return;
if (isGameRunning && !isGameOver) {
updateGameLogic(); currentTick++; score++;
const scoreEl = document.getElementById('score'); if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
if (currentTick - lastSentTick >= CHUNK_SIZE) sendChunk();
}
drawGame(); requestAnimationFrame(gameLoop);
}
async function initGame() {
try {
const cRes = await fetch('/api/config'); gameConfig = await cRes.json();
await loadAssets();
await loadStartScreenLeaderboard();
isLoaded = true;
if(loadingText) loadingText.style.display = 'none'; if(startBtn) startBtn.style.display = 'inline-block';
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
const hsEl = document.getElementById('localHighscore'); if(hsEl) hsEl.innerText = savedHighscore;
requestAnimationFrame(gameLoop);
} catch(e) { if(loadingText) loadingText.innerText = "Fehler!"; }
}
initGame();
})();

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Escape the Teacher</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
@@ -22,8 +23,10 @@
</div>
<div id="startScreen">
<div class="start-left">
<h1>ESCAPE THE<br>TEACHER</h1>
<p style="font-size: 12px; color: #aaa;">Dein Rekord: <span id="localHighscore" style="color:yellow">0</span></p>
<button id="startBtn" onclick="startGameClick()">STARTEN</button>
@@ -34,22 +37,22 @@
<p>
• Herr Müller verteilt heute Nachsitzen!<br>
• Spring über Tische und Mülleimer.<br>
Lass dich nicht erwischen!
<strong>Neu:</strong> Ducken (Pfeil Runter) gegen fliegende Schwämme!
</p>
</div>
<div class="info-box">
<div class="info-title">STEUERUNG</div>
<p>
PC: <strong>Leertaste</strong>, <strong>Pfeil Hoch/Runter</strong> oder <strong>Mausklick</strong><br>
Handy: <strong>Tippen</strong> (Springen) oder <strong>Wischen</strong> (Ducken)
PC: <strong>Leertaste/Maus</strong> (Springen), <strong>Pfeil Runter</strong> (Ducken)<br>
Handy: <strong>Tippen</strong> (Springen), <strong>Wischen nach unten</strong> (Ducken)
</p>
</div>
<div class="legal-bar">
<button class="legal-btn" onclick="showMyCodes()" style="color:yellow; border-color:yellow;">★ MEINE CODES</button>
<button class="legal-btn" onclick="openModal('impressum')">Impressum</button>
<button class="legal-btn" onclick="openModal('datenschutz')">Datenschutz</button>
<button class="legal-btn" onclick="showMyCodes()" style="border-color: yellow; color: yellow;">★ MEINE CODES</button>
</div>
</div>
@@ -76,24 +79,28 @@
</div>
</div>
<div id="modal-impressum" class="modal-overlay">
<div class="modal-content">
<button class="close-modal" onclick="closeModal()">X</button>
<h2>Impressum</h2>
<p><strong>Angaben gemäß § 5 TMG:</strong></p>
<p>Max Mustermann<br>Musterschule 1<br>12345 Musterstadt</p>
<p>Kontakt: max@schule.de</p>
<p><em>Schulprojekt. Keine kommerzielle Absicht.</em></p>
</div>
</div>
<div id="modal-codes" class="modal-overlay">
<div class="modal-content">
<button class="close-modal" onclick="closeModal()">X</button>
<h2 style="color:yellow">MEINE BEWEISE</h2>
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
</div>
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code dem Lehrer für deinen Preis oder zum Löschen.</p>
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code dem Lehrer für deinen Preis oder lösche den Eintrag.</p>
</div>
</div>
<div id="modal-impressum" class="modal-overlay">
<div class="modal-content">
<button class="close-modal" onclick="closeModal()">X</button>
<h2>Impressum</h2>
<p><strong>Angaben gemäß § 5 TMG:</strong></p>
<p>
Sebastian Unterschütz<br>
Göltzschtalblick 16 <br>
08236 Ellefeld
</p>
<p>Kontakt: sebastian@unterschuetz.de</p>
<p><em>Dies ist ein Schulprojekt ohne kommerzielle Absicht.</em></p>
</div>
</div>
@@ -101,81 +108,54 @@
<div class="modal-content">
<button class="close-modal" onclick="closeModal()">X</button>
<h2>Datenschutz</h2>
<p>Wir speichern deinen Namen und Score für die Bestenliste.</p>
<p>Dein persönlicher Highscore wird lokal auf deinem Gerät gespeichert.</p>
<p><strong>1. Allgemeines</strong><br>
Dies ist ein Schulprojekt. Wir speichern so wenig Daten wie möglich.</p>
<p><strong>2. Welche Daten speichern wir?</strong><br>
Wenn du einen Highscore einträgst, speichern wir auf unserem Server:
<ul>
<li>Deinen gewählten Namen</li>
<li>Deinen Punktestand</li>
<li>Einen Zeitstempel</li>
<li>Einen zufälligen "Beweis-Code"</li>
</ul>
</p>
<p><strong>3. Lokale Speicherung (Dein Gerät)</strong><br>
Das Spiel nutzt den "LocalStorage" deines Browsers, um deinen persönlichen Rekord und deine gesammelten Beweis-Codes zu speichern. Diese Daten verlassen dein Gerät nicht, außer du sendest sie aktiv ab.</p>
<p><strong>4. Cookies & Tracking</strong><br>
Wir verwenden <strong>keine</strong> Tracking-Cookies, keine Analyse-Tools (wie Google Analytics) und laden keine Schriftarten von fremden Servern.</p>
<p><strong>5. Deine Rechte</strong><br>
Du kannst deine Einträge jederzeit selbstständig über das Menü "Meine Codes" vom Server löschen.</p>
</div>
</div>
<script src="game.js"></script>
<script src="js/config.js"></script>
<script src="js/state.js"></script>
<script src="js/network.js"></script>
<script src="js/input.js"></script>
<script src="js/logic.js"></script>
<script src="js/render.js"></script>
<script src="js/main.js"></script>
<script>
function openModal(id) { document.getElementById('modal-' + id).style.display = 'flex'; }
function closeModal() { document.querySelectorAll('.modal-overlay').forEach(el => el.style.display = 'none'); }
function openModal(id) {
document.getElementById('modal-' + id).style.display = 'flex';
}
function closeModal() {
const modals = document.querySelectorAll('.modal-overlay');
modals.forEach(el => el.style.display = 'none');
}
// Schließen wenn man daneben klickt
window.onclick = function(event) {
if (event.target.classList.contains('modal-overlay')) closeModal();
}
async function deleteClaim(index, sid, code) {
if(!confirm("Willst du diesen Score wirklich unwiderruflich löschen?")) return;
try {
const res = await fetch('/api/claim/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sid, claimCode: code })
});
if (!res.ok) {
alert("Fehler: Konnte nicht löschen (Vielleicht Session abgelaufen?)");
} else {
alert("Erfolgreich gelöscht!");
}
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
claims.splice(index, 1);
localStorage.setItem('escape_claims', JSON.stringify(claims));
showMyCodes();
} catch(e) {
alert("Verbindungsfehler");
if (event.target.classList.contains('modal-overlay')) {
closeModal();
}
}
function showMyCodes() {
openModal('codes');
const listEl = document.getElementById('codesList');
const claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
if (claims.length === 0) {
listEl.innerHTML = "Noch keine Scores eingetragen.";
return;
}
let html = "";
// Wir gehen rückwärts durch (neueste oben)
// Wir brauchen den Index 'i', um das Element auch aus dem LocalStorage zu löschen
for (let i = claims.length - 1; i >= 0; i--) {
const c = claims[i];
// Fallback, falls alte Einträge noch keine SessionID haben
const btnDisabled = c.sessionId ? "" : "disabled";
const btnStyle = c.sessionId ? "cursor:pointer; color:red;" : "color:gray;";
html += `
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
<div>
<span style="color:white; font-weight:bold;">${c.code}</span>
<span style="color:#ffcc00;">(${c.score} Pkt)</span><br>
<span style="color:#aaa;">${c.name} - ${c.date}</span>
</div>
<button onclick="deleteClaim(${i}, '${c.sessionId}', '${c.code}')"
style="background:transparent; border:1px solid #555; padding:5px; font-size:10px; ${btnStyle}"
${btnDisabled}>
LÖSCHEN
</button>
</div>`;
}
listEl.innerHTML = html;
}
</script>
</body>
</html>

28
static/js/config.js Normal file
View File

@@ -0,0 +1,28 @@
// Konstanten
const GAME_WIDTH = 800;
const GAME_HEIGHT = 400;
const GRAVITY = 0.6;
const JUMP_POWER = -12;
const GROUND_Y = 350;
const GAME_SPEED = 5;
const CHUNK_SIZE = 60;
// RNG Klasse
class PseudoRNG {
constructor(seed) {
this.state = BigInt(seed);
}
nextFloat() {
const a = 1664525n; const c = 1013904223n; const m = 4294967296n;
this.state = (this.state * a + c) % m;
return Number(this.state) / Number(m);
}
nextRange(min, max) {
return min + (this.nextFloat() * (max - min));
}
pick(array) {
if (!array || array.length === 0) return null;
const idx = Math.floor(this.nextRange(0, array.length));
return array[idx];
}
}

45
static/js/input.js Normal file
View File

@@ -0,0 +1,45 @@
function handleInput(action, active) {
if (isGameOver) { if(active) location.reload(); return; }
const relativeTick = currentTick - lastSentTick;
if (action === "JUMP" && active) {
if (player.grounded && !isCrouching) {
player.vy = JUMP_POWER;
player.grounded = false;
inputLog.push({ t: relativeTick, act: "JUMP" });
}
}
if (action === "DUCK") { isCrouching = active; }
}
// Event Listeners
window.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true);
});
window.addEventListener('keyup', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false);
});
window.addEventListener('mousedown', (e) => {
if (e.target === canvas && e.button === 0) handleInput("JUMP", true);
});
// Touch Logic
let touchStartY = 0;
window.addEventListener('touchstart', (e) => {
if(e.target === canvas) { e.preventDefault(); touchStartY = e.touches[0].clientY; }
}, { passive: false });
window.addEventListener('touchend', (e) => {
if(e.target === canvas) {
e.preventDefault();
const diff = e.changedTouches[0].clientY - touchStartY;
if (diff < -30) handleInput("JUMP", true);
else if (diff > 30) { handleInput("DUCK", true); setTimeout(() => handleInput("DUCK", false), 800); }
else if (Math.abs(diff) < 10) handleInput("JUMP", true);
}
});

59
static/js/logic.js Normal file
View File

@@ -0,0 +1,59 @@
function updateGameLogic() {
if (isCrouching) {
inputLog.push({ t: currentTick - lastSentTick, act: "DUCK" });
}
const originalHeight = 50; const crouchHeight = 25;
player.h = isCrouching ? crouchHeight : originalHeight;
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
player.vy += GRAVITY;
if (isCrouching && !player.grounded) player.vy += 2.0;
player.y += player.vy;
if (player.y + originalHeight >= GROUND_Y) {
player.y = GROUND_Y - originalHeight; player.vy = 0; player.grounded = true;
} else { player.grounded = false; }
let nextObstacles = []; let rightmostX = 0;
for (let obs of obstacles) {
obs.x -= GAME_SPEED;
const playerHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
if (checkCollision(playerHitbox, obs)) {
player.color = "darkred";
if (!isGameOver) { sendChunk(); gameOver("Kollision"); }
}
if (obs.x + obs.def.width > -100) {
nextObstacles.push(obs);
if (obs.x + obs.def.width > rightmostX) rightmostX = obs.x + obs.def.width;
}
}
obstacles = nextObstacles;
// Spawning
if (rightmostX < GAME_WIDTH - 10 && gameConfig) {
const gap = Math.floor(400 + rng.nextRange(0, 500));
let spawnX = rightmostX + gap; if (spawnX < GAME_WIDTH) spawnX = GAME_WIDTH;
let possibleObs = [];
gameConfig.obstacles.forEach(def => {
if (def.id === "eraser") { if (score >= 500) possibleObs.push(def); } else possibleObs.push(def);
});
const def = rng.pick(possibleObs);
let speech = null;
if (def && def.canTalk) { if (rng.nextFloat() > 0.7) speech = rng.pick(def.speechLines); }
if (def) {
const yOffset = def.yOffset || 0;
obstacles.push({ x: spawnX, y: GROUND_Y - def.height - yOffset, def: def, speech: speech });
}
}
}
function checkCollision(p, obs) {
const paddingX = 10; const paddingY_Top = 25; const paddingY_Bottom = 5;
return (p.x + p.w - paddingX > obs.x + paddingX && p.x + paddingX < obs.x + obs.def.width - paddingX &&
p.y + p.h - paddingY_Bottom > obs.y + paddingY_Top && p.y + paddingY_Top < obs.y + obs.def.height - paddingY_Bottom);
}

70
static/js/main.js Normal file
View File

@@ -0,0 +1,70 @@
async function loadAssets() {
playerSprite.src = "assets/player.png";
if (gameConfig.backgrounds && gameConfig.backgrounds.length > 0) {
const bgName = gameConfig.backgrounds[0];
if (!bgName.startsWith("#")) bgSprite.src = "assets/" + bgName;
}
const promises = gameConfig.obstacles.map(def => {
return new Promise((resolve) => {
if (!def.image) { resolve(); return; }
const img = new Image(); img.src = "assets/" + def.image;
img.onload = () => { sprites[def.id] = img; resolve(); };
img.onerror = () => { resolve(); };
});
});
if (bgSprite.src) {
promises.push(new Promise(r => { bgSprite.onload = r; bgSprite.onerror = r; }));
}
await Promise.all(promises);
}
window.startGameClick = async function() {
if (!isLoaded) return;
startScreen.style.display = 'none';
document.body.classList.add('game-active'); // Handy Rotate Check
try {
const sRes = await fetch('/api/start', {method:'POST'});
const sData = await sRes.json();
sessionID = sData.sessionId;
rng = new PseudoRNG(sData.seed);
isGameRunning = true;
resize();
} catch(e) { location.reload(); }
};
function gameOver(reason) {
if (isGameOver) return;
isGameOver = true;
const finalScoreVal = Math.floor(score / 10);
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
if (finalScoreVal > currentHighscore) localStorage.setItem('escape_highscore', finalScoreVal);
gameOverScreen.style.display = 'flex';
document.getElementById('finalScore').innerText = finalScoreVal;
loadLeaderboard();
drawGame();
}
function gameLoop() {
if (!isLoaded) return;
if (isGameRunning && !isGameOver) {
updateGameLogic(); currentTick++; score++;
const scoreEl = document.getElementById('score'); if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
if (currentTick - lastSentTick >= CHUNK_SIZE) sendChunk();
}
drawGame(); requestAnimationFrame(gameLoop);
}
async function initGame() {
try {
const cRes = await fetch('/api/config'); gameConfig = await cRes.json();
await loadAssets();
await loadStartScreenLeaderboard();
isLoaded = true;
if(loadingText) loadingText.style.display = 'none'; if(startBtn) startBtn.style.display = 'inline-block';
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
const hsEl = document.getElementById('localHighscore'); if(hsEl) hsEl.innerText = savedHighscore;
requestAnimationFrame(gameLoop);
} catch(e) { if(loadingText) loadingText.innerText = "Fehler!"; }
}
initGame();

92
static/js/network.js Normal file
View File

@@ -0,0 +1,92 @@
async function sendChunk() {
const ticksToSend = currentTick - lastSentTick;
if (ticksToSend <= 0) return;
const payload = {
sessionId: sessionID,
inputs: [...inputLog],
totalTicks: ticksToSend
};
inputLog = [];
lastSentTick = currentTick;
try {
const res = await fetch('/api/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.serverObs) serverObstacles = data.serverObs;
if (data.status === "dead") {
console.error("SERVER TOT", data);
gameOver("Vom Server gestoppt");
} else {
const sScore = data.verifiedScore;
if (Math.abs(score - sScore) > 200) score = sScore;
}
} catch (e) {
console.error("Netzwerkfehler:", e);
}
}
window.submitScore = async function() {
const nameInput = document.getElementById('playerNameInput');
const name = nameInput.value;
const btn = document.getElementById('submitBtn');
if (!name) return alert("Namen eingeben!");
btn.disabled = true;
try {
const res = await fetch('/api/submit-name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ sessionId: sessionID, name: name })
});
const data = await res.json();
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
myClaims.push({
name: name, score: Math.floor(score / 10), code: data.claimCode,
date: new Date().toLocaleString('de-DE'), sessionId: sessionID
});
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
document.getElementById('inputSection').style.display = 'none';
loadLeaderboard();
} catch (e) {}
};
async function loadLeaderboard() {
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
const entries = await res.json();
let html = "<h3>BESTENLISTE</h3>";
entries.forEach(e => {
const color = e.isMe ? "yellow" : "white";
html += `<div style="display:flex; justify-content:space-between; color:${color}; margin-bottom:5px;">
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
});
document.getElementById('leaderboard').innerHTML = html;
}
async function loadStartScreenLeaderboard() {
try {
const listEl = document.getElementById('startLeaderboardList');
if (!listEl) return;
const res = await fetch('/api/leaderboard');
const entries = await res.json();
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
let html = "";
entries.forEach(e => {
let icon = "#" + e.rank;
if (e.rank === 1) icon = "🥇"; if (e.rank === 2) icon = "🥈"; if (e.rank === 3) icon = "🥉";
html += `<div class="hof-entry"><span><span class="hof-rank">${icon}</span> ${e.name}</span><span class="hof-score">${Math.floor(e.score / 10)}</span></div>`;
});
listEl.innerHTML = html;
} catch (e) {}
}

107
static/js/render.js Normal file
View File

@@ -0,0 +1,107 @@
function resize() {
// 1. INTERNE SPIEL-AUFLÖSUNG ERZWINGEN
// Das behebt den "Zoom/Nur Ecke sichtbar" Fehler
canvas.width = GAME_WIDTH; // 800
canvas.height = GAME_HEIGHT; // 400
// 2. Verfügbaren Platz im Browser berechnen (Minus etwas Rand)
const windowWidth = window.innerWidth - 20;
const windowHeight = window.innerHeight - 20;
const targetRatio = GAME_WIDTH / GAME_HEIGHT; // 2.0
const windowRatio = windowWidth / windowHeight;
let finalWidth, finalHeight;
// 3. Letterboxing berechnen
if (windowRatio < targetRatio) {
// Screen ist schmaler (z.B. Handy Portrait) -> Breite limitiert
finalWidth = windowWidth;
finalHeight = windowWidth / targetRatio;
} else {
// Screen ist breiter (z.B. Desktop) -> Höhe limitiert
finalHeight = windowHeight;
finalWidth = finalHeight * targetRatio;
}
// 4. Größe auf den CONTAINER anwenden
if (container) {
container.style.width = `${Math.floor(finalWidth)}px`;
container.style.height = `${Math.floor(finalHeight)}px`;
}
// Hinweis: Wir setzen KEINE style.width/height auf das Canvas Element selbst.
// Das Canvas erbt "width: 100%; height: 100%" vom CSS und füllt den Container.
}
// Event Listener
window.addEventListener('resize', resize);
// Einmal sofort ausführen
resize();
// --- DRAWING ---
function drawGame() {
// Alles löschen
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
// Background
if (bgSprite.complete && bgSprite.naturalHeight !== 0) {
// Hintergrundbild exakt auf 800x400 skalieren
ctx.drawImage(bgSprite, 0, 0, GAME_WIDTH, GAME_HEIGHT);
} else {
// Fallback Farbe
ctx.fillStyle = "#f0f0f0";
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
}
// Boden
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
// Hindernisse
obstacles.forEach(obs => {
const img = sprites[obs.def.id];
if (img) {
ctx.drawImage(img, obs.x, obs.y, obs.def.width, obs.def.height);
} else {
ctx.fillStyle = obs.def.color;
ctx.fillRect(obs.x, obs.y, obs.def.width, obs.def.height);
}
if(obs.speech) drawSpeechBubble(obs.x, obs.y, obs.speech);
});
// Debug Rahmen (Server Hitboxen)
ctx.strokeStyle = isGameOver ? "red" : "lime";
ctx.lineWidth = 2;
serverObstacles.forEach(srvObs => {
ctx.strokeRect(srvObs.x, srvObs.y, srvObs.w, srvObs.h);
});
// Spieler
const drawY = isCrouching ? player.y + 25 : player.y;
const drawH = isCrouching ? 25 : 50;
if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
} else {
ctx.fillStyle = player.color;
ctx.fillRect(player.x, drawY, player.w, drawH);
}
// Game Over Overlay (Dunkelheit)
if (isGameOver) {
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
}
}
function drawSpeechBubble(x, y, text) {
const bX = x-20; const bY = y-40; const bW = 120; const bH = 30;
ctx.fillStyle="white"; ctx.fillRect(bX,bY,bW,bH);
ctx.strokeRect(bX,bY,bW,bH);
ctx.fillStyle="black"; ctx.font="10px Arial"; ctx.textAlign="center";
ctx.fillText(text, bX+bW/2, bY+20);
}

36
static/js/state.js Normal file
View File

@@ -0,0 +1,36 @@
// Globale Status-Variablen
let gameConfig = null;
let isLoaded = false;
let isGameRunning = false;
let isGameOver = false;
let sessionID = null;
let rng = null;
let score = 0;
let currentTick = 0;
let lastSentTick = 0;
let inputLog = [];
let isCrouching = false;
// Grafiken
let sprites = {};
let playerSprite = new Image();
let bgSprite = new Image();
// Spiel-Objekte
let player = {
x: 50, y: 300, w: 30, h: 50, color: "red",
vy: 0, grounded: false
};
let obstacles = [];
let serverObstacles = [];
// HTML Elemente (Caching)
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const container = document.getElementById('game-container');
const startScreen = document.getElementById('startScreen');
const startBtn = document.getElementById('startBtn');
const loadingText = document.getElementById('loadingText');
const gameOverScreen = document.getElementById('gameOverScreen');

View File

@@ -35,17 +35,18 @@ body, html {
box-shadow: 0 0 50px rgba(0,0,0,0.8);
border: 4px solid #444;
background: #000;
max-width: 100%;
max-height: 100%;
/* Größe wird von JS gesetzt, hier nur Layout-Verhalten */
display: flex;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
background-color: #f0f0f0;
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100%;
height: auto;
}
/* =========================================
@@ -64,7 +65,7 @@ canvas {
}
/* =========================================
3. OVERLAYS (Start, Game Over)
3. OVERLAYS (Basis-Einstellungen)
========================================= */
#startScreen, #gameOverScreen {
position: absolute;
@@ -89,21 +90,21 @@ h1 {
text-transform: uppercase;
}
/* --- FIX: INPUT SECTION ZENTRIEREN --- */
/* Fix für Input Section (Name eingeben) */
#inputSection {
display: flex;
flex-direction: column; /* Untereinander */
align-items: center; /* Horizontal mittig */
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
margin: 15px 0;
}
/* =========================================
4. START SCREEN LAYOUT
4. START SCREEN (Links/Rechts Layout)
========================================= */
#startScreen {
flex-direction: row;
flex-direction: row; /* Nebeneinander */
gap: 40px;
}
@@ -125,6 +126,32 @@ h1 {
max-width: 35%;
}
/* =========================================
5. GAME OVER SCREEN (WICHTIG: Untereinander)
========================================= */
#gameOverScreen {
/* HIER IST DER FIX: */
flex-direction: column !important;
gap: 15px;
}
/* Das Leaderboard im Game Over Screen */
#leaderboard {
margin-top: 10px;
font-size: 12px;
width: 90%;
max-width: 450px;
background: rgba(0,0,0,0.5);
padding: 15px;
border: 2px solid #666;
/* Begrenzte Höhe mit Scrollen, falls Liste lang ist */
max-height: 200px;
overflow-y: auto;
}
/* =========================================
6. HALL OF FAME BOX (Startseite)
========================================= */
.hall-of-fame-box {
background: rgba(0, 0, 0, 0.6);
border: 4px solid #ffcc00;
@@ -159,7 +186,7 @@ h1 {
.hof-score { color: white; font-weight: bold; }
/* =========================================
5. BUTTONS & INPUTS
7. BUTTONS & INPUTS
========================================= */
button {
font-family: 'Press Start 2P', cursive;
@@ -190,7 +217,7 @@ input {
}
/* =========================================
6. INFO BOXEN
8. INFO BOXEN
========================================= */
.info-box {
background: rgba(255, 255, 255, 0.1);
@@ -217,20 +244,8 @@ input {
text-decoration: underline;
}
/* Game Over Screen Anpassung */
#gameOverScreen { flex-direction: column; }
#leaderboard {
margin-top: 20px;
font-size: 12px;
width: 90%;
max-width: 450px;
background: rgba(0,0,0,0.5);
padding: 15px;
border: 2px solid #666;
}
/* =========================================
7. RECHTLICHES
9. RECHTLICHES
========================================= */
.legal-bar {
margin-top: 20px;
@@ -241,8 +256,14 @@ input {
font-size: 10px;
padding: 8px 12px;
margin: 0;
background: transparent;
border: 1px solid #666;
color: #888;
box-shadow: none;
}
.legal-btn:hover { background: #333; color: white; border-color: white; }
/* Modals */
.modal-overlay {
display: none;
position: fixed;
@@ -275,10 +296,12 @@ input {
width: 35px; height: 35px;
font-size: 16px;
line-height: 30px;
margin: 0; padding: 0;
box-shadow: none;
}
/* =========================================
8. PC / DESKTOP SPEZIAL
10. PC / DESKTOP SPEZIAL
========================================= */
@media (min-width: 1024px) {
h1 { font-size: 48px; margin-bottom: 40px; }
@@ -295,7 +318,7 @@ input {
}
/* =========================================
9. MOBILE ANPASSUNG
11. MOBILE ANPASSUNG
========================================= */
@media (max-width: 700px) {
#startScreen {
@@ -313,7 +336,7 @@ input {
}
/* =========================================
10. ROTATE OVERLAY
12. ROTATE OVERLAY
========================================= */
#rotate-overlay {
display: none;
@@ -329,7 +352,8 @@ input {
}
.icon { font-size: 60px; margin-bottom: 20px; }
/* Nur anzeigen, wenn Spiel läuft UND Portrait */
@media screen and (orientation: portrait) {
#rotate-overlay { display: flex; }
#game-container { display: none !important; }
body.game-active #rotate-overlay { display: flex; }
body.game-active #game-container { display: none !important; }
}