tetris klaar
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
/* ---------- basis ---------- */
|
||||
:root {
|
||||
--bg: #070a11;
|
||||
--panel: #10141f;
|
||||
--panel-border: #1e2637;
|
||||
--text: #dbe4f0;
|
||||
--muted: #75839c;
|
||||
--accent: #35d23d;
|
||||
--accent2: #3c6cf0;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: radial-gradient(1200px 800px at 50% -10%, #152137 0%, var(--bg) 60%);
|
||||
color: var(--text);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 20px 16px 60px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: inherit;
|
||||
font-size: 0.7em;
|
||||
background: #1d2638;
|
||||
border: 1px solid #2c3952;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
color: var(--muted);
|
||||
margin-left: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- layout ---------- */
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 210px 300px 260px;
|
||||
gap: 22px;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.35em;
|
||||
margin-bottom: 14px;
|
||||
color: var(--text);
|
||||
text-shadow: 0 0 18px rgba(53, 210, 61, 0.35), 0 0 40px rgba(60, 108, 240, 0.25);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title span {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ---------- panelen ---------- */
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.box {
|
||||
background: linear-gradient(180deg, var(--panel) 0%, #0d111b 100%);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.28em;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.box canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* statistieken */
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.stat .label {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.stat .value {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---------- speelbord ---------- */
|
||||
.play-area {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.board-wrap {
|
||||
position: relative;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--panel-border), 0 20px 60px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
#canvas-board {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
image-rendering: pixelated;
|
||||
background: #0a0e17;
|
||||
}
|
||||
|
||||
/* ---------- overlay ---------- */
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(5, 8, 14, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay-card {
|
||||
background: linear-gradient(180deg, #141a29, #0d1120);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
padding: 26px 30px;
|
||||
width: 84%;
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.overlay-card h2 {
|
||||
font-size: 1.7rem;
|
||||
letter-spacing: 0.24em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#overlay-sub {
|
||||
color: var(--muted);
|
||||
|
||||
#name-form label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
#name-form.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#name-input {
|
||||
background: #0a0f1a;
|
||||
border: 1px solid #2c3952;
|
||||
border-radius: 7px;
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
padding: 9px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#name-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
font-size: 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.overlay-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
padding: 11px 18px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
background: #1d2638;
|
||||
border: 1px solid #2c3952;
|
||||
transition: transform 0.05s ease, filter 0.15s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: linear-gradient(180deg, var(--accent), #1f9c26);
|
||||
border-color: transparent;
|
||||
color: #05160b;
|
||||
}
|
||||
|
||||
.overlay-hint {
|
||||
margin-top: 14px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* ---------- highscores ---------- */
|
||||
.highscore-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.highscore-list li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
font-size: 0.9rem;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.highscore-list li.no-scores {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hs-rank {
|
||||
width: 20px;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hs-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hs-score {
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ---------- besturing ---------- */
|
||||
.controls-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.controls-table td {
|
||||
padding: 4px 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.controls-table td:first-child {
|
||||
width: 70px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- touch-controls (mobiel) ---------- */
|
||||
.touch-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ---------- responsief ---------- */
|
||||
@media (max-width: 880px) {
|
||||
.layout {
|
||||
grid-template-columns: 300px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-left,
|
||||
.panel-right {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.panel-left .box:first-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.touch-controls {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 14px;
|
||||
background: linear-gradient(180deg, transparent, rgba(7, 10, 17, 0.95) 30%);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.touch-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
max-width: 360px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.t-btn {
|
||||
padding: 14px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
background: rgba(29, 38, 56, 0.85);
|
||||
border: 1px solid #2c3952;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.t-btn:active {
|
||||
background: var(--accent2);
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
#name-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||
<title>Neo Tetris</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%230b1118'/%3E%3Crect x='3' y='3' width='4' height='4' fill='%2335d23d'/%3E%3Crect x='9' y='3' width='4' height='4' fill='%2335d23d'/%3E%3Crect x='9' y='9' width='4' height='4' fill='%2335d23d'/%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="layout">
|
||||
<!-- Linkerpaneel: hold + statistieken -->
|
||||
<section class="panel panel-left">
|
||||
<div class="box">
|
||||
<h2 class="box-title">HOLD <kbd>C</kbd></h2>
|
||||
<canvas id="canvas-hold" width="120" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="box stats">
|
||||
<div class="stat">
|
||||
<span class="label">Score</span>
|
||||
<span id="stat-score" class="value">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Lijnen</span>
|
||||
<span id="stat-lines" class="value">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="label">Level</span>
|
||||
<span id="stat-level" class="value">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Centrum: het speelbord -->
|
||||
<section class="play-area">
|
||||
<h1 class="title">NEO <span>TE­TRIS</span></h1>
|
||||
<div class="board-wrap">
|
||||
<canvas id="canvas-board" width="300" height="600"></canvas>
|
||||
|
||||
<div id="overlay" class="overlay hidden">
|
||||
<div class="overlay-card">
|
||||
<h2 id="overlay-title">PAUZE</h2>
|
||||
<p id="overlay-sub"></p>
|
||||
<form id="name-form" class="hidden" autocomplete="off">
|
||||
<label for="name-input">Naam:</label>
|
||||
<input id="name-input" type="text" maxlength="12" placeholder="Speler" />
|
||||
<p id="name-error" class="error hidden">Vul een naam in.</p>
|
||||
<button type="button" id="btn-submit" class="btn btn-accent">Opslaan</button>
|
||||
</form>
|
||||
<div class="overlay-actions">
|
||||
<button type="button" id="btn-primary" class="btn btn-accent">Opnieuw spelen</button>
|
||||
<button type="button" id="btn-secondary" class="btn hidden">Doorgaan</button>
|
||||
</div>
|
||||
<p class="overlay-hint">ENTER of klik op een knop</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rechterpaneel: next + highscores + besturing -->
|
||||
<section class="panel panel-right">
|
||||
<div class="box">
|
||||
<h2 class="box-title">NEXT</h2>
|
||||
<canvas id="canvas-next" width="120" height="252"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="box-title">HIGHSCORES</h2>
|
||||
<ol id="highscore-list" class="highscore-list"></ol>
|
||||
</div>
|
||||
|
||||
<details class="box controls" open>
|
||||
<summary class="box-title">BESTURING</summary>
|
||||
<table class="controls-table">
|
||||
<tr><td>◀ ▶</td><td>verschuiven</td></tr>
|
||||
<tr><td>▼</td><td>zacht laten zakken</td></tr>
|
||||
<tr><td>▲ / X</td><td>rechtsom draaien</td></tr>
|
||||
<tr><td>Z</td><td>linksom draaien</td></tr>
|
||||
<tr><td>Spatie</td><td>hard drop</td></tr>
|
||||
<tr><td>C</td><td>hold</td></tr>
|
||||
<tr><td>P</td><td>pauze</td></tr>
|
||||
<tr><td>R</td><td>opnieuw starten</td></tr>
|
||||
</table>
|
||||
</details>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Touch-controls (mobiel) -->
|
||||
<div class="touch-controls">
|
||||
<div class="touch-row">
|
||||
<button type="button" class="t-btn" data-action="rotate" aria-label="Draaien">⟳</button>
|
||||
<button type="button" class="t-btn" data-action="hold" aria-label="Hold">HOLD</button>
|
||||
<button type="button" class="t-btn" data-action="hard" aria-label="Hard drop">⤓</button>
|
||||
</div>
|
||||
<div class="touch-row">
|
||||
<button type="button" class="t-btn" data-action="left" aria-label="Links">◀</button>
|
||||
<button type="button" class="t-btn" data-action="drop" aria-label="Omlaag">▼</button>
|
||||
<button type="button" class="t-btn" data-action="right" aria-label="Rechts">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/constants.js"></script>
|
||||
<script src="js/tetris.js"></script>
|
||||
<script src="js/render.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
/* Centrale configuratie voor het spel */
|
||||
const CONSTANTS = {
|
||||
COLS: 10, // breedte van het bord
|
||||
ROWS: 20, // hoogte van het bord
|
||||
CELL: 30, // pixelgrootte van een cel op het bord
|
||||
PREVIEW_CELL: 18, // pixelgrootte van een cel in de previews
|
||||
PREVIEW_COUNT: 3, // aantal zichtbare volgende blokken
|
||||
COLORS: {
|
||||
I: '#00e5f0',
|
||||
O: '#f7d000',
|
||||
T: '#b14ce0',
|
||||
S: '#35d23d',
|
||||
Z: '#f03c3c',
|
||||
J: '#3c6cf0',
|
||||
L: '#f09a20'
|
||||
},
|
||||
SCORE: {
|
||||
SINGLE: 100, // 1 rij
|
||||
DOUBLE: 300, // 2 rijen
|
||||
TRIPLE: 500, // 3 rijen
|
||||
TETRIS: 800, // 4 rijen
|
||||
SOFT_DROP: 1, // punt per cel bij zachte drop
|
||||
HARD_DROP: 2, // punten per cel bij harde drop
|
||||
LINES_PER_LEVEL: 10 // aantal rijen nodig voor een nieuw level
|
||||
},
|
||||
GRAVITY: [800, 720, 630, 550, 470, 380, 300, 220, 160, 100,
|
||||
80, 70, 60, 50, 40, 30, 20, 15, 10, 10], // ms per zetje per level
|
||||
HIGHSCORE_KEY: 'neo-tetris.highscores',
|
||||
PLAYER_NAME_KEY: 'neo-tetris.playername',
|
||||
MAX_HIGHSCORES: 10
|
||||
};
|
||||
|
||||
/* Punten per aantal tegelijk gewiste rijen (index = aantal rijen) */
|
||||
const SCORE_POINTS = [0, 100, 300, 500, 800];
|
||||
|
||||
/* Definitie van de 7 tetromino's als matrices (1 = gevuld blokje) */
|
||||
const SHAPES = {
|
||||
I: [
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]
|
||||
],
|
||||
O: [
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
],
|
||||
T: [
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
S: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0]
|
||||
],
|
||||
Z: [
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
J: [
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
L: [
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
};
|
||||
|
||||
const PIECE_TYPES = Object.keys(SHAPES);
|
||||
|
||||
/* Eenvoudige wall-kicks die proberen een blokje langs obstakels te schuiven bij rotatie */
|
||||
const WALL_KICKS = [
|
||||
[ 0, 0],
|
||||
[-1, 0],
|
||||
[ 1, 0],
|
||||
[ 0, -1],
|
||||
[-2, 0],
|
||||
[ 2, 0]
|
||||
];
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
'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);
|
||||
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
/* Tekenen van een blokje met simpele 3D-schaduwen */
|
||||
function drawCell(ctx, px, py, size, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(px, py, size, size);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.22)';
|
||||
ctx.fillRect(px, py, size, size * 0.28);
|
||||
ctx.fillRect(px, py, size * 0.28, size);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.25)';
|
||||
ctx.fillRect(px, py + size * 0.72, size, size * 0.28);
|
||||
ctx.fillRect(px + size * 0.72, py, size * 0.28, size);
|
||||
}
|
||||
|
||||
/* Ghost piece: alleen een omlijning, half transparant */
|
||||
function drawGhostCell(ctx, px, py, size, color) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.45;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = Math.max(2, size * 0.12);
|
||||
ctx.strokeRect(px + 3, py + 3, size - 6, size - 6);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/* Het hoofd-bord */
|
||||
function drawBoard(canvas, game) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const size = CONSTANTS.CELL;
|
||||
const W = CONSTANTS.COLS * size;
|
||||
const H = CONSTANTS.ROWS * size;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// achtergrond met subtiel verloop
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
||||
grad.addColorStop(0, '#121722');
|
||||
grad.addColorStop(1, '#070a11');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// gridlijnen
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let c = 1; c < CONSTANTS.COLS; c++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c * size, 0);
|
||||
ctx.lineTo(c * size, H);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let r = 1; r < CONSTANTS.ROWS; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * size);
|
||||
ctx.lineTo(W, r * size);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// vastgezette blokken
|
||||
for (let r = 0; r < CONSTANTS.ROWS; r++) {
|
||||
for (let c = 0; c < CONSTANTS.COLS; c++) {
|
||||
const color = game.board[r][c];
|
||||
if (color) drawCell(ctx, c * size, r * size, size, color);
|
||||
}
|
||||
}
|
||||
|
||||
// flitsende rijen tijdens het wissen
|
||||
if (game.flashLines.length) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.75)';
|
||||
for (const r of game.flashLines) {
|
||||
ctx.fillRect(0, r * size, W, size);
|
||||
}
|
||||
}
|
||||
|
||||
// ghost + actief blok
|
||||
if (game.piece && !game.over) {
|
||||
if (!game.locking) {
|
||||
const ghostY = game.ghostY();
|
||||
if (ghostY !== game.piece.y) {
|
||||
const m = game.piece.matrix;
|
||||
for (let r = 0; r < m.length; r++) {
|
||||
for (let c = 0; c < m[r].length; c++) {
|
||||
if (!m[r][c]) continue;
|
||||
drawGhostCell(ctx, (game.piece.x + c) * size, (ghostY + r) * size, size, game.piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
const m = game.piece.matrix;
|
||||
for (let r = 0; r < m.length; r++) {
|
||||
for (let c = 0; c < m[r].length; c++) {
|
||||
if (!m[r][c]) continue;
|
||||
const py = (game.piece.y + r) * size;
|
||||
if (py < 0) continue; // boven het bord: niet tekenen
|
||||
drawCell(ctx, (game.piece.x + c) * size, py, size, game.piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Kleine preview (hold) */
|
||||
function drawHold(canvas, type) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const cell = CONSTANTS.PREVIEW_CELL;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (!type) return;
|
||||
const shape = SHAPES[type];
|
||||
const w = shape[0].length * cell;
|
||||
const h = shape.length * cell;
|
||||
const ox = (canvas.width - w) / 2;
|
||||
const oy = (canvas.height - h) / 2;
|
||||
for (let r = 0; r < shape.length; r++) {
|
||||
for (let c = 0; c < shape[r].length; c++) {
|
||||
if (!shape[r][c]) continue;
|
||||
drawCell(ctx, ox + c * cell, oy + r * cell, cell, CONSTANTS.COLORS[type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Volgende blokken onder elkaar */
|
||||
function drawNext(canvas, game) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const cell = CONSTANTS.PREVIEW_CELL;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const list = game.preview;
|
||||
const band = canvas.height / CONSTANTS.PREVIEW_COUNT;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const type = list[i];
|
||||
const shape = SHAPES[type];
|
||||
const w = shape[0].length * cell;
|
||||
const h = shape.length * cell;
|
||||
const ox = (canvas.width - w) / 2;
|
||||
const oy = i * band + (band - h) / 2;
|
||||
for (let r = 0; r < shape.length; r++) {
|
||||
for (let c = 0; c < shape[r].length; c++) {
|
||||
if (!shape[r][c]) continue;
|
||||
drawCell(ctx, ox + c * cell, oy + r * cell, cell, CONSTANTS.COLORS[type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
'use strict';
|
||||
|
||||
/* Helpers */
|
||||
function cloneMatrix(matrix) {
|
||||
return matrix.map((row) => row.slice());
|
||||
}
|
||||
|
||||
/*
|
||||
* Roteert een matrix 90 graden.
|
||||
* dir = 1 : rechtsom (met de klok mee)
|
||||
* dir = -1 : linksom (tegen de klok in)
|
||||
*/
|
||||
function rotateMatrix(matrix, dir) {
|
||||
const n = matrix.length;
|
||||
const m = matrix[0].length;
|
||||
const out = Array.from({ length: m }, () => Array(n).fill(0));
|
||||
for (let r = 0; r < n; r++) {
|
||||
for (let c = 0; c < m; c++) {
|
||||
if (!matrix[r][c]) continue;
|
||||
if (dir > 0) out[c][n - 1 - r] = 1;
|
||||
else out[m - 1 - c][r] = 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Volledige Tetris-spelstaat en -regels.
|
||||
* De UI hangt een onUpdate-callback op voor het verversen van de panelen.
|
||||
*/
|
||||
class TetrisGame {
|
||||
constructor() {
|
||||
this.onUpdate = null;
|
||||
this.reset();
|
||||
}
|
||||
|
||||
notify() {
|
||||
if (this.onUpdate) this.onUpdate();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.board = Array.from({ length: CONSTANTS.ROWS }, () =>
|
||||
Array(CONSTANTS.COLS).fill(null)
|
||||
);
|
||||
this.queue = []; // komende bloktypes (7-bag)
|
||||
this.hold = null; // vastgehouden bloktype
|
||||
this.canHold = true; // mag er een keer vastgehouden worden?
|
||||
this.piece = null; // actief blok
|
||||
this.score = 0;
|
||||
this.lines = 0;
|
||||
this.level = 1;
|
||||
this.over = false;
|
||||
this.paused = false;
|
||||
this.locking = false; // true tijdens wis-animatie
|
||||
this.flashLines = []; // rijen die op dit moment flitsen
|
||||
this.dropTimer = 0;
|
||||
this.dropInterval = this.gravity();
|
||||
this.spawn();
|
||||
}
|
||||
|
||||
/* ---------- 7-bag generator: elk blok komt 1x per zak voor ---------- */
|
||||
generateBag() {
|
||||
const bag = PIECE_TYPES.slice();
|
||||
for (let i = bag.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[bag[i], bag[j]] = [bag[j], bag[i]];
|
||||
}
|
||||
this.queue.push(...bag);
|
||||
}
|
||||
|
||||
nextType() {
|
||||
while (this.queue.length === 0) this.generateBag();
|
||||
return this.queue.shift();
|
||||
}
|
||||
|
||||
get preview() {
|
||||
return this.queue.slice(0, CONSTANTS.PREVIEW_COUNT);
|
||||
}
|
||||
|
||||
/* ---------- spawn / plaatsing ---------- */
|
||||
placePiece(type) {
|
||||
const shape = cloneMatrix(SHAPES[type]);
|
||||
this.piece = {
|
||||
type,
|
||||
matrix: shape,
|
||||
x: Math.floor((CONSTANTS.COLS - shape[0].length) / 2),
|
||||
y: 0,
|
||||
color: CONSTANTS.COLORS[type]
|
||||
};
|
||||
if (this.collides(this.piece.matrix, this.piece.x, this.piece.y)) {
|
||||
this.over = true; // bord is te vol: game over
|
||||
}
|
||||
}
|
||||
|
||||
spawn() {
|
||||
const type = this.nextType();
|
||||
this.placePiece(type);
|
||||
this.canHold = true;
|
||||
this.dropTimer = 0;
|
||||
this.dropInterval = this.gravity();
|
||||
}
|
||||
|
||||
gravity() {
|
||||
return CONSTANTS.GRAVITY[Math.min(this.level - 1, CONSTANTS.GRAVITY.length - 1)];
|
||||
}
|
||||
|
||||
/* ---------- botsing / beweging ---------- */
|
||||
collides(matrix, x, y) {
|
||||
for (let r = 0; r < matrix.length; r++) {
|
||||
for (let c = 0; c < matrix[r].length; c++) {
|
||||
if (!matrix[r][c]) continue;
|
||||
const bx = x + c;
|
||||
const by = y + r;
|
||||
if (bx < 0 || bx >= CONSTANTS.COLS || by >= CONSTANTS.ROWS) return true;
|
||||
if (by >= 0 && this.board[by][bx]) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ghostY() {
|
||||
let y = this.piece.y;
|
||||
while (!this.collides(this.piece.matrix, this.piece.x, y + 1)) y++;
|
||||
return y;
|
||||
}
|
||||
|
||||
move(dx) {
|
||||
if (this.over || this.paused || this.locking) return false;
|
||||
if (!this.collides(this.piece.matrix, this.piece.x + dx, this.piece.y)) {
|
||||
this.piece.x += dx;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
rotate(dir) {
|
||||
if (this.over || this.paused || this.locking) return false;
|
||||
const rotated = rotateMatrix(this.piece.matrix, dir);
|
||||
for (const [xOff, yOff] of WALL_KICKS) {
|
||||
if (!this.collides(rotated, this.piece.x + xOff, this.piece.y + yOff)) {
|
||||
this.piece.matrix = rotated;
|
||||
this.piece.x += xOff;
|
||||
this.piece.y += yOff;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
stepDown() {
|
||||
if (this.collides(this.piece.matrix, this.piece.x, this.piece.y + 1)) {
|
||||
this.lock();
|
||||
return false; // kon niet verder: blok is vastgezet
|
||||
}
|
||||
this.piece.y++;
|
||||
return true;
|
||||
}
|
||||
|
||||
softDrop() {
|
||||
if (this.over || this.paused || this.locking) return;
|
||||
if (this.stepDown()) this.addScore(CONSTANTS.SCORE.SOFT_DROP);
|
||||
}
|
||||
|
||||
hardDrop() {
|
||||
if (this.over || this.paused || this.locking) return;
|
||||
let dropped = 0;
|
||||
while (this.stepDown()) dropped++;
|
||||
this.addScore(dropped * CONSTANTS.SCORE.HARD_DROP);
|
||||
}
|
||||
|
||||
holdPiece() {
|
||||
if (this.over || this.paused || this.locking || !this.canHold) return;
|
||||
const current = this.piece.type;
|
||||
this.canHold = false;
|
||||
if (this.hold) {
|
||||
const swap = this.hold;
|
||||
this.hold = current;
|
||||
this.placePiece(swap);
|
||||
} else {
|
||||
this.hold = current;
|
||||
this.spawn();
|
||||
this.canHold = false; // spawn() zet dit weer op true; hier terugzetten
|
||||
}
|
||||
this.dropTimer = 0;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
|
||||
addScore(pts) {
|
||||
this.score += pts;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/* ---------- vastzetten + rijen wissen ---------- */
|
||||
lock() {
|
||||
const { matrix, x, y, color } = this.piece;
|
||||
for (let r = 0; r < matrix.length; r++) {
|
||||
for (let c = 0; c < matrix[r].length; c++) {
|
||||
if (!matrix[r][c]) continue;
|
||||
const by = y + r;
|
||||
if (by < 0) {
|
||||
this.over = true; // blok steekt te ver boven het bord uit
|
||||
return;
|
||||
}
|
||||
this.board[by][x + c] = color;
|
||||
}
|
||||
}
|
||||
|
||||
const fullRows = [];
|
||||
for (let r = 0; r < CONSTANTS.ROWS; r++) {
|
||||
if (this.board[r].every((cell) => cell !== null)) fullRows.push(r);
|
||||
}
|
||||
|
||||
if (fullRows.length === 0) {
|
||||
this.spawn();
|
||||
this.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
// wis-animatie: kort flitsen, daarna pas de rijen daadwerkelijk verwijderen
|
||||
this.locking = true;
|
||||
this.flashLines = fullRows.slice();
|
||||
const cleared = fullRows.length;
|
||||
const rowsToRemove = fullRows.slice();
|
||||
|
||||
setTimeout(() => {
|
||||
this.locking = false;
|
||||
this.flashLines = [];
|
||||
|
||||
const kept = this.board.filter((row, r) => !rowsToRemove.includes(r));
|
||||
while (kept.length < CONSTANTS.ROWS) {
|
||||
kept.unshift(Array(CONSTANTS.COLS).fill(null));
|
||||
}
|
||||
this.board = kept;
|
||||
|
||||
this.lines += cleared;
|
||||
this.addScore(SCORE_POINTS[cleared] * this.level);
|
||||
|
||||
const newLevel = Math.floor(this.lines / CONSTANTS.SCORE.LINES_PER_LEVEL) + 1;
|
||||
if (newLevel !== this.level) {
|
||||
this.level = newLevel;
|
||||
this.dropInterval = this.gravity();
|
||||
}
|
||||
|
||||
if (!this.over) this.spawn();
|
||||
this.notify();
|
||||
}, 140);
|
||||
|
||||
this.notify();
|
||||
}
|
||||
|
||||
/* ---------- updates door de game-loop (delta-tijd in ms) ---------- */
|
||||
update(dtMs) {
|
||||
if (this.over || this.paused || this.locking) return;
|
||||
this.dropTimer += dtMs;
|
||||
while (this.dropTimer >= this.dropInterval) {
|
||||
this.dropTimer -= this.dropInterval;
|
||||
if (this.locking || this.over) return;
|
||||
this.stepDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user