All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m45s
538 lines
15 KiB
JavaScript
538 lines
15 KiB
JavaScript
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');
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
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 (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
|
|
};
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
|
|
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);
|
|
}
|
|
|
|
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 {
|
|
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');
|
|
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 = "";
|
|
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>`;
|
|
});
|
|
|
|
listEl.innerHTML = html;
|
|
} catch (e) {
|
|
console.error("Konnte Leaderboard nicht laden", e);
|
|
}
|
|
}
|
|
|
|
initGame(); |