Files
kaltaquise-gamification/sys_digger/game.js
2026-04-20 14:44:51 +02:00

228 lines
8.5 KiB
JavaScript

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const cysCanvas = document.getElementById('cystinstein-canvas');
const cysCtx = cysCanvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const CX = canvas.width / 2;
const CY = canvas.height / 2;
// ─── Leaderboard ─────────────────────────────────────────────────────────────
const LEADERBOARD_KEY = 'sysdigger_leaderboard';
const LEADERBOARD_MAX = 10;
function loadLeaderboard() {
try {
return JSON.parse(localStorage.getItem(LEADERBOARD_KEY)) || [];
} catch {
return [];
}
}
function saveLeaderboard(board) {
localStorage.setItem(LEADERBOARD_KEY, JSON.stringify(board));
}
function addEntry(name, score) {
const board = loadLeaderboard();
board.push({ name: name.trim() || 'Anonymous', score });
board.sort((a, b) => b.score - a.score);
const trimmed = board.slice(0, LEADERBOARD_MAX);
saveLeaderboard(trimmed);
return trimmed;
}
function renderLeaderboard(highlightEntry) {
const board = loadLeaderboard();
const tbody = document.getElementById('leaderboard-rows');
tbody.innerHTML = '';
if (board.length === 0) {
const tr = document.createElement('tr');
tr.innerHTML = `<td colspan="3" style="color:#555;text-align:center;padding:12px">No scores yet</td>`;
tbody.appendChild(tr);
return;
}
board.forEach((entry, i) => {
const tr = document.createElement('tr');
const isHighlight = highlightEntry &&
entry.name === highlightEntry.name &&
entry.score === highlightEntry.score;
if (isHighlight) tr.classList.add('highlight');
else if (i === 0) tr.classList.add('rank-1');
else if (i === 1) tr.classList.add('rank-2');
else if (i === 2) tr.classList.add('rank-3');
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : i + 1;
tr.innerHTML = `
<td class="rank-num">${medal}</td>
<td style="text-align:left">${escapeHtml(entry.name)}</td>
<td style="text-align:right">${entry.score}</td>`;
tbody.appendChild(tr);
});
}
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function topScore() {
const board = loadLeaderboard();
return board.length > 0 ? board[0].score : null;
}
// ─── State ───────────────────────────────────────────────────────────────────
let score = 0;
let docks = 0;
let gameOver = false;
let incomingAtoms = [];
let anchorPoints = [];
let particles = [];
let clickEffects = [];
let lastSpawn = 0;
let spawnInterval = SPAWN_INTERVAL_START;
const stars = Array.from({ length: STAR_COUNT }, () => ({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
r: Math.random() * STAR_RADIUS_MAX,
a: STAR_ALPHA_MIN + Math.random() * STAR_ALPHA_RANGE
}));
// ─── Anchors ─────────────────────────────────────────────────────────────────
function initAnchors() {
anchorPoints = [];
for (let i = 0; i < NUM_ANCHORS; i++) {
const angle = (i / NUM_ANCHORS) * Math.PI * 2 - Math.PI / 2;
anchorPoints.push({
x: CX + Math.cos(angle) * ANCHOR_RADIUS,
y: CY + Math.sin(angle) * ANCHOR_RADIUS,
angle,
active: false,
blockTimer: 0,
pulsePhase: Math.random() * Math.PI * 2
});
}
}
// ─── Spawn ───────────────────────────────────────────────────────────────────
function spawnAtom() {
const available = anchorPoints.map((_, i) => i).filter(i => !anchorPoints[i].active);
if (available.length === 0) return;
const idx = available[Math.floor(Math.random() * available.length)];
incomingAtoms.push(new IncomingAtom(idx));
}
// ─── Input ───────────────────────────────────────────────────────────────────
canvas.addEventListener('click', e => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
let hit = false;
anchorPoints.forEach((ap, i) => {
if (!ap.active) return;
if (Math.hypot(mx - ap.x, my - ap.y) > CLICK_RADIUS) return;
const atom = incomingAtoms.find(a => a.anchorIndex === i && !a.dead);
if (atom) {
atom.dead = true;
ap.active = false;
ap.blockTimer = BLOCK_TIMER_FRAMES;
score++;
document.getElementById('score').textContent = `Score: ${score}`;
burst(ap.x, ap.y, '#00ff88');
clickEffects.push({ x: ap.x, y: ap.y, radius: 5, life: 1 });
hit = true;
}
});
if (!hit) clickEffects.push({ x: mx, y: my, radius: 5, life: 0.5 });
});
// ─── Game flow ───────────────────────────────────────────────────────────────
function triggerGameOver() {
gameOver = true;
const best = topScore();
const isNewRecord = best === null || score > best;
document.getElementById('final-score').textContent = `Score: ${score}`;
document.getElementById('new-record').classList.toggle('visible', isNewRecord);
const input = document.getElementById('player-name');
input.value = '';
setTimeout(() => input.focus(), 50);
renderLeaderboard(null);
document.getElementById('overlay').classList.add('active');
}
function submitScore() {
const name = document.getElementById('player-name').value.trim() || 'Anonymous';
const entry = { name, score };
addEntry(name, score);
renderLeaderboard(entry);
const best = topScore();
updateHudBest(best);
document.getElementById('name-form').style.display = 'none';
}
function restartGame() {
score = docks = 0;
gameOver = false;
incomingAtoms = [];
particles = [];
clickEffects = [];
lastSpawn = 0;
spawnInterval = SPAWN_INTERVAL_START;
document.getElementById('score').textContent = 'Score: 0';
document.getElementById('docks-count').textContent = '0 / 8 docks';
document.getElementById('name-form').style.display = 'flex';
document.getElementById('overlay').classList.remove('active');
initAnchors();
updateCystinstein();
}
function updateHudBest(best) {
document.getElementById('highscore').textContent = best !== null ? `Best: ${best}` : 'Best: —';
}
// ─── Enter key on name input ──────────────────────────────────────────────────
document.getElementById('player-name').addEventListener('keydown', e => {
if (e.key === 'Enter') submitScore();
});
// ─── Main loop ───────────────────────────────────────────────────────────────
function gameLoop(ts) {
drawBackground();
if (!gameOver && ts - lastSpawn > spawnInterval) {
spawnAtom();
lastSpawn = ts;
spawnInterval = Math.max(SPAWN_INTERVAL_MIN, spawnInterval - SPAWN_DIFFICULTY_STEP);
}
incomingAtoms = incomingAtoms.filter(a => !a.dead);
incomingAtoms.forEach(a => { a.update(); a.draw(); });
drawCentralMolecule();
drawAnchorPoints();
drawParticles();
drawClickEffects();
requestAnimationFrame(gameLoop);
}
// ─── Boot ────────────────────────────────────────────────────────────────────
updateHudBest(topScore());
initAnchors();
updateCystinstein();
requestAnimationFrame(gameLoop);