275 lines
7.5 KiB
JavaScript
275 lines
7.5 KiB
JavaScript
'use strict';
|
|
|
|
/* ---------- DOM-referenties ---------- */
|
|
const boardCanvas = document.getElementById('canvas-board');
|
|
const holdCanvas = document.getElementById('canvas-hold');
|
|
const nextCanvas = document.getElementById('canvas-next');
|
|
|
|
const elScore = document.getElementById('stat-score');
|
|
const elLines = document.getElementById('stat-lines');
|
|
const elLevel = document.getElementById('stat-level');
|
|
|
|
const overlay = document.getElementById('overlay');
|
|
const overlayTitle = document.getElementById('overlay-title');
|
|
const overlaySub = document.getElementById('overlay-sub');
|
|
const nameForm = document.getElementById('name-form');
|
|
const nameInput = document.getElementById('name-input');
|
|
const nameError = document.getElementById('name-error');
|
|
const submitBtn = document.getElementById('btn-submit');
|
|
const btnPrimary = document.getElementById('btn-primary');
|
|
const btnSecondary = document.getElementById('btn-secondary');
|
|
|
|
const highscoreList = document.getElementById('highscore-list');
|
|
|
|
const game = new TetrisGame();
|
|
game.onUpdate = renderUI;
|
|
|
|
let gameOverHandled = false;
|
|
|
|
/* ---------- highscore-opslag in localStorage ---------- */
|
|
function loadScores() {
|
|
try {
|
|
const raw = localStorage.getItem(CONSTANTS.HIGHSCORE_KEY);
|
|
const list = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(list) ? list : [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveScores(list) {
|
|
try {
|
|
localStorage.setItem(
|
|
CONSTANTS.HIGHSCORE_KEY,
|
|
JSON.stringify(list.slice(0, CONSTANTS.MAX_HIGHSCORES))
|
|
);
|
|
} catch (e) {
|
|
/* opslag niet beschikbaar (bv. private browsing) - negeren */
|
|
}
|
|
}
|
|
|
|
function qualifies(score) {
|
|
if (score <= 0) return false;
|
|
const list = loadScores();
|
|
if (list.length < CONSTANTS.MAX_HIGHSCORES) return true;
|
|
return score > list[list.length - 1].score;
|
|
}
|
|
|
|
function recordScore(name, score, lines, level) {
|
|
const list = loadScores();
|
|
const entry = {
|
|
name: name || 'Speler',
|
|
score,
|
|
lines,
|
|
level,
|
|
date: new Date().toISOString()
|
|
};
|
|
list.push(entry);
|
|
list.sort((a, b) => b.score - a.score || a.date.localeCompare(b.date));
|
|
saveScores(list);
|
|
return list;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function renderHighscores() {
|
|
const list = loadScores();
|
|
highscoreList.innerHTML = '';
|
|
if (list.length === 0) {
|
|
const li = document.createElement('li');
|
|
li.className = 'no-scores';
|
|
li.textContent = 'Nog geen scores - wees de eerste!';
|
|
highscoreList.appendChild(li);
|
|
return;
|
|
}
|
|
list.forEach((entry, i) => {
|
|
const li = document.createElement('li');
|
|
const rank = document.createElement('span');
|
|
rank.className = 'hs-rank';
|
|
rank.textContent = i + 1;
|
|
const name = document.createElement('span');
|
|
name.className = 'hs-name';
|
|
name.textContent = entry.name;
|
|
const score = document.createElement('span');
|
|
score.className = 'hs-score';
|
|
score.textContent = entry.score;
|
|
li.appendChild(rank);
|
|
li.appendChild(name);
|
|
li.appendChild(score);
|
|
highscoreList.appendChild(li);
|
|
});
|
|
}
|
|
|
|
/* ---------- UI-panelen verversen ---------- */
|
|
function renderUI() {
|
|
elScore.textContent = game.score;
|
|
elLines.textContent = game.lines;
|
|
elLevel.textContent = game.level;
|
|
drawHold(holdCanvas, game.hold);
|
|
drawNext(nextCanvas, game);
|
|
}
|
|
|
|
|
|
/* ---------- overlays (pauze / game over) ---------- */
|
|
function setOverlay(title, sub, opts = {}) {
|
|
overlayTitle.textContent = title;
|
|
overlaySub.textContent = sub || '';
|
|
nameForm.classList.toggle('hidden', !opts.showName);
|
|
btnPrimary.textContent = opts.primaryLabel || 'Opnieuw spelen';
|
|
btnSecondary.classList.toggle('hidden', !opts.showSecondary);
|
|
overlay.classList.remove('hidden');
|
|
}
|
|
|
|
function hideOverlay() {
|
|
overlay.classList.add('hidden');
|
|
}
|
|
|
|
function togglePause() {
|
|
if (game.over) return;
|
|
game.paused = !game.paused;
|
|
if (game.paused) {
|
|
setOverlay('PAUZE', 'Druk op P of "Doorgaan" om verder te spelen.', {
|
|
showSecondary: true,
|
|
primaryLabel: 'Opnieuw spelen'
|
|
});
|
|
} else {
|
|
hideOverlay();
|
|
}
|
|
}
|
|
|
|
function restartGame() {
|
|
game.reset();
|
|
game.paused = false;
|
|
gameOverHandled = false;
|
|
nameForm.classList.add('hidden');
|
|
nameError.classList.add('hidden');
|
|
hideOverlay();
|
|
renderUI();
|
|
}
|
|
|
|
function onGameOver() {
|
|
const topTen = qualifies(game.score);
|
|
if (topTen) {
|
|
nameInput.value = localStorage.getItem(CONSTANTS.PLAYER_NAME_KEY) || '';
|
|
setOverlay('GAME OVER', 'Top 10! Voer je naam in:', {
|
|
showName: true,
|
|
primaryLabel: 'Opnieuw spelen'
|
|
});
|
|
nameInput.focus();
|
|
} else {
|
|
setOverlay('GAME OVER', `Score: ${game.score}`, {
|
|
primaryLabel: 'Opnieuw spelen'
|
|
});
|
|
}
|
|
renderHighscores();
|
|
}
|
|
|
|
function handleSubmitScore() {
|
|
const name = nameInput.value.trim();
|
|
if (!name) {
|
|
nameError.classList.remove('hidden');
|
|
nameInput.focus();
|
|
return;
|
|
}
|
|
localStorage.setItem(CONSTANTS.PLAYER_NAME_KEY, name);
|
|
recordScore(name.slice(0, 12), game.score, game.lines, game.level);
|
|
nameForm.classList.add('hidden');
|
|
overlaySub.textContent = 'Score opgeslagen in de highscore-lijst!';
|
|
renderHighscores();
|
|
btnPrimary.focus();
|
|
}
|
|
|
|
/* ---------- game-loop ---------- */
|
|
let lastTime = performance.now();
|
|
|
|
function loop(now) {
|
|
const dt = Math.min((now - lastTime) / 1000, 0.25);
|
|
lastTime = now;
|
|
|
|
game.update(dt * 1000);
|
|
drawBoard(boardCanvas, game);
|
|
renderUI();
|
|
|
|
if (game.over && !gameOverHandled) {
|
|
gameOverHandled = true;
|
|
onGameOver();
|
|
}
|
|
|
|
requestAnimationFrame(loop);
|
|
}
|
|
|
|
/* ---------- toetsenbord ---------- */
|
|
const KEYMAP = {
|
|
ArrowLeft: { fn: () => game.move(-1), prevent: true },
|
|
ArrowRight: { fn: () => game.move(1), prevent: true },
|
|
ArrowDown: { fn: () => game.softDrop(), prevent: true },
|
|
ArrowUp: { fn: () => game.rotate(1), prevent: true },
|
|
KeyX: { fn: () => game.rotate(1), prevent: true },
|
|
KeyZ: { fn: () => game.rotate(-1), prevent: false },
|
|
Space: { fn: () => game.hardDrop(), prevent: true },
|
|
KeyC: { fn: () => game.holdPiece(), prevent: false },
|
|
KeyP: { fn: () => togglePause(), prevent: false },
|
|
KeyR: { fn: () => restartGame(), prevent: false }
|
|
};
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
// typen in het naamveld: alleen Enter voor opslaan
|
|
if (event.target === nameInput) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
handleSubmitScore();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.key === 'Enter' && !overlay.classList.contains('hidden')) {
|
|
if (game.over) restartGame();
|
|
else if (game.paused) togglePause();
|
|
return;
|
|
}
|
|
|
|
const binding = KEYMAP[event.code];
|
|
if (!binding) return;
|
|
if (binding.prevent) event.preventDefault();
|
|
binding.fn();
|
|
});
|
|
|
|
/* ---------- knoppen + touch-controls ---------- */
|
|
btnPrimary.addEventListener('click', () => restartGame());
|
|
btnSecondary.addEventListener('click', () => {
|
|
if (game.paused) togglePause();
|
|
});
|
|
submitBtn.addEventListener('click', handleSubmitScore);
|
|
// voorkom dat Enter in het naamveld de pagina ververst (default form-submit)
|
|
nameForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
handleSubmitScore();
|
|
});
|
|
|
|
const ACTION_MAP = {
|
|
rotate: () => game.rotate(1),
|
|
rotateCcw: () => game.rotate(-1),
|
|
hold: () => game.holdPiece(),
|
|
hard: () => game.hardDrop(),
|
|
left: () => game.move(-1),
|
|
right: () => game.move(1),
|
|
drop: () => game.softDrop()
|
|
};
|
|
|
|
document.querySelectorAll('.t-btn').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
if (ACTION_MAP[btn.dataset.action]) ACTION_MAP[btn.dataset.action]();
|
|
});
|
|
});
|
|
|
|
/* ---------- start ---------- */
|
|
renderUI();
|
|
renderHighscores();
|
|
requestAnimationFrame(loop);
|