802 lines
24 KiB
HTML
802 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>NEON BLOCKS: Cyberpunk Tetris</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--neon-cyan: #0ff;
|
||
--neon-pink: #f0f;
|
||
--neon-yellow: #ffeb3b;
|
||
--neon-green: #0f0;
|
||
--bg-color: #050510;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background-color: var(--bg-color);
|
||
color: white;
|
||
font-family: 'Orbitron', sans-serif;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#game-container {
|
||
position: relative;
|
||
border: 2px solid #333;
|
||
box-shadow: 0 0 20px var(--neon-pink), inset 0 0 50px rgba(0,0,0,1);
|
||
padding: 10px;
|
||
background-image:
|
||
linear-gradient(rgba(18, 16, 16, 1) 50%, rgba(0, 0, 0, 1) 50%),
|
||
linear-gradient(90deg, rgba(255, 0, 255, .2), rgba(0, 255, 255, .1));
|
||
background-size: 100% 4px, 4px 100%;
|
||
}
|
||
|
||
canvas {
|
||
display: block;
|
||
background-color: #0a0a14;
|
||
box-shadow: inset 0 0 30px #000;
|
||
}
|
||
|
||
.ui-panel {
|
||
position: absolute;
|
||
right: -250px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.stat-box {
|
||
background: rgba(0, 20, 40, 0.8);
|
||
border-left: 4px solid var(--neon-cyan);
|
||
padding: 15px;
|
||
box-shadow: -5px 5px 15px rgba(0,255,255,0.3);
|
||
}
|
||
|
||
.stat-label {
|
||
color: #aaa;
|
||
font-size: 0.8rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 2px;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 2rem;
|
||
color: var(--neon-cyan);
|
||
text-shadow: 0 0 10px var(--neon-cyan);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
#level-indicator {
|
||
border-left-color: var(--neon-yellow);
|
||
}
|
||
|
||
#level-value {
|
||
color: var(--neon-yellow);
|
||
text-shadow: 0 0 10px var(--neon-yellow);
|
||
}
|
||
|
||
.overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(5, 5, 16, 0.9);
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 4rem;
|
||
margin-bottom: 20px;
|
||
color: var(--neon-pink);
|
||
text-shadow:
|
||
3px 3px 0px #0ff,
|
||
-3px -3px 0px var(--neon-yellow);
|
||
animation: glitch-skew 1s infinite linear alternate-reverse;
|
||
}
|
||
|
||
button {
|
||
background: transparent;
|
||
color: var(--neon-green);
|
||
font-family: 'Orbitron', sans-serif;
|
||
font-size: 2rem;
|
||
padding: 15px 40px;
|
||
border: 2px solid var(--neon-green);
|
||
box-shadow: 0 0 15px var(--neon-green), inset 0 0 15px var(--neon-green);
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
button:hover {
|
||
background: var(--neon-green);
|
||
color: black;
|
||
box-shadow: 0 0 30px var(--neon-green), inset 0 0 30px var(--neon-green);
|
||
}
|
||
|
||
.controls-hint {
|
||
margin-top: 40px;
|
||
color: #888;
|
||
font-size: 0.9rem;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* CRT Scanline Effect */
|
||
.scanlines {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: linear-gradient(
|
||
to bottom,
|
||
rgba(255,255,255,0),
|
||
rgba(255,255,255,0) 50%,
|
||
rgba(0,0,0,0.1) 50%,
|
||
rgba(0,0,0,0.1)
|
||
);
|
||
background-size: 100% 4px;
|
||
pointer-events: none;
|
||
z-index: 99;
|
||
}
|
||
|
||
@keyframes glitch-skew {
|
||
0% { transform: skewX(0); text-shadow: 2px 2px var(--neon-pink); }
|
||
25% { transform: skewX(-5deg); text-shadow: -2px -2px var(--neon-cyan); }
|
||
50% { transform: skewX(5deg); text-shadow: 2px -2px var(--neon-yellow); }
|
||
75% { transform: skewX(-3deg); text-shadow: -2px 2px var(--neon-green); }
|
||
100% { transform: skewX(0); text-shadow: 2px 2px var(--neon-pink); }
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Neon‑blue pulsing & glitch background (placed behind the game) */
|
||
/* ------------------------------------------------------------------ */
|
||
.bg-effect {
|
||
position: fixed;
|
||
inset: 0; /* fill the whole viewport */
|
||
background: #001f3f; /* deep blue base */
|
||
z-index: -1; /* sit behind everything */
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Pulsing neon glow */
|
||
.bg-effect::before {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(circle at 50% 50%,
|
||
rgba(0, 255, 255, 0.3),
|
||
transparent 70%);
|
||
animation: pulse 4s ease‑in‑out infinite;
|
||
}
|
||
|
||
/* Glitch “slice” effect */
|
||
.bg-effect::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
background: repeating-linear-gradient(
|
||
0deg,
|
||
rgba(0,255,255,0.15) 0,
|
||
rgba(0,255,255,0.15) 2px,
|
||
transparent 2px,
|
||
transparent 4px
|
||
);
|
||
mix-blend-mode: screen;
|
||
animation: glitch 2s steps(2, end) infinite;
|
||
}
|
||
|
||
/* Pulse animation – expands/brightens then contracts */
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); opacity: 0.6; }
|
||
50% { transform: scale(1.05); opacity: 1; }
|
||
}
|
||
|
||
/* Glitch animation – quickly shifts horizontal slices */
|
||
@keyframes glitch {
|
||
0% { transform: translateX(0); }
|
||
20% { transform: translateX(-2px); }
|
||
40% { transform: translateX(2px); }
|
||
60% { transform: translateX(-2px); }
|
||
80% { transform: translateX(2px); }
|
||
100% { transform: translateX(0); }
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Level‑Up text effect */
|
||
/* ------------------------------------------------------------------ */
|
||
.level-up {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-family: 'Orbitron', sans-serif;
|
||
font-size: 4rem;
|
||
color: var(--neon-yellow);
|
||
text-shadow: 0 0 20px var(--neon-yellow), 0 0 40px var(--neon-yellow);
|
||
pointer‑events: none;
|
||
opacity: 0;
|
||
transform: scale(0.5);
|
||
pointer-events: none;
|
||
z-index: 200;
|
||
}
|
||
|
||
/* animation: shake → grow → fade */
|
||
@keyframes levelUpAnim {
|
||
0% { opacity: 0; transform: scale(0.5) translateY(0); }
|
||
10% { opacity: 1; transform: scale(0.7) translateY(-5px) rotate(-2deg); }
|
||
20% { opacity: 1; transform: scale(0.8) translateY(5px) rotate(2deg); }
|
||
30% { opacity: 1; transform: scale(0.9) translateY(-5px) rotate(-1deg); }
|
||
40% { opacity: 1; transform: scale(1) translateY(0); }
|
||
70% { opacity: 1; transform: scale(1.5) translateY(-30px); }
|
||
100% { opacity: 0; transform: scale(2) translateY(-80px); }
|
||
}
|
||
|
||
/* class added by JS to start the animation */
|
||
.level-up.active {
|
||
animation: levelUpAnim 1.5s forwards;
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<audio id="bg-music" loop></audio>
|
||
<audio id="sfx-place" src="fx/place.mp3" preload="auto"></audio>
|
||
<audio id="sfx-explode" src="fx/explode.mp3" preload="auto"></audio>
|
||
<audio id="sfx-levelUp" src="fx/levelUp.mp3" preload="auto"></audio>
|
||
|
||
<div class="bg-effect"></div>
|
||
|
||
<div id="level-up-overlay" class="level-up">Level Up!</div>
|
||
|
||
<div class="scanlines"></div>
|
||
|
||
<div id="game-container">
|
||
<canvas id="tetris" width="300" height="600"></canvas>
|
||
|
||
<div class="ui-panel">
|
||
<div class="stat-box">
|
||
<div class="stat-label">Score</div>
|
||
<div id="score" class="stat-value">0</div>
|
||
</div>
|
||
<div class="stat-box" id="level-indicator">
|
||
<div class="stat-label">System Level</div>
|
||
<div id="level-value" class="stat-value">1</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-label">Lines</div>
|
||
<div id="lines" class="stat-value">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="start-screen" class="overlay">
|
||
<h1>NEON BLOCKS</h1>
|
||
<button onclick="startGame()">Initialize System</button>
|
||
<div class="controls-hint">
|
||
ARROWS to Move & Rotate<br>
|
||
DOWN to Accelerate<br>
|
||
SPACE to Hard Drop
|
||
</div>
|
||
</div>
|
||
|
||
<div id="game-over-screen" class="overlay" style="display: none;">
|
||
<h1 style="color: red; text-shadow: 0 0 20px red;">SYSTEM FAILURE</h1>
|
||
<button onclick="resetGame()">Reboot System</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const canvas = document.getElementById('tetris');
|
||
const context = canvas.getContext('2d');
|
||
|
||
// Scale up everything by 30 for the block size
|
||
context.scale(30, 30);
|
||
|
||
// Neon Palette
|
||
const COLORS = [
|
||
null,
|
||
'#FF0D72', // T - Magenta
|
||
'#0DC2FF', // I - Cyan
|
||
'#0DFF72', // S - Green
|
||
'#F538FF', // Z - Purple
|
||
'#FF8E0D', // L - Orange
|
||
'#FFE138', // O - Yellow
|
||
'#3877FF', // J - Blue
|
||
];
|
||
|
||
const GLOW_COLORS = [
|
||
null,
|
||
'magenta',
|
||
'cyan',
|
||
'lime',
|
||
'violet',
|
||
'orange',
|
||
'yellow',
|
||
'blue',
|
||
];
|
||
|
||
// Game State
|
||
let dropCounter = 0;
|
||
let dropInterval = 1000;
|
||
let lastTime = 0;
|
||
let score = 0;
|
||
let linesClearedTotal = 0;
|
||
let level = 1;
|
||
|
||
// Particle System Array
|
||
let particles = [];
|
||
|
||
const arena = createMatrix(10, 20);
|
||
|
||
const player = {
|
||
pos: {x: 0, y: 0},
|
||
matrix: null,
|
||
score: 0,
|
||
};
|
||
|
||
// --- PIECES GENERATORS ---
|
||
function createPiece(type) {
|
||
if (type === 'I') {
|
||
return [
|
||
[0, 1, 0, 0],
|
||
[0, 1, 0, 0],
|
||
[0, 1, 0, 0],
|
||
[0, 1, 0, 0],
|
||
];
|
||
} else if (type === 'L') {
|
||
return [
|
||
[0, 2, 0],
|
||
[0, 2, 0],
|
||
[0, 2, 2],
|
||
];
|
||
} else if (type === 'J') {
|
||
return [
|
||
[0, 3, 0],
|
||
[0, 3, 0],
|
||
[3, 3, 0],
|
||
];
|
||
} else if (type === 'O') {
|
||
return [
|
||
[4, 4],
|
||
[4, 4],
|
||
];
|
||
} else if (type === 'Z') {
|
||
return [
|
||
[5, 5, 0],
|
||
[0, 5, 5],
|
||
[0, 0, 0],
|
||
];
|
||
} else if (type === 'S') {
|
||
return [
|
||
[0, 6, 6],
|
||
[6, 6, 0],
|
||
[0, 0, 0],
|
||
];
|
||
} else if (type === 'T') {
|
||
return [
|
||
[0, 7, 0],
|
||
[7, 7, 7],
|
||
[0, 0, 0],
|
||
];
|
||
}
|
||
}
|
||
|
||
// ----- MUSIC PLAYLIST -------------------------------------------------
|
||
const MUSIC_PLAYLIST = [
|
||
// Add or remove URLs as you like – they can be relative paths or full URLs
|
||
'music/track01.mp3',
|
||
'music/track02.mp3',
|
||
'music/track03.mp3',
|
||
'music/track04.mp3',
|
||
];
|
||
|
||
let currentTrackIndex = 0;
|
||
const musicPlayer = document.getElementById('bg-music');
|
||
|
||
// Load the first track (but don’t start playing until the game begins)
|
||
function loadCurrentTrack() {
|
||
musicPlayer.src = MUSIC_PLAYLIST[currentTrackIndex];
|
||
musicPlayer.load();
|
||
}
|
||
|
||
// Advance to the next track in the playlist (wrap around)
|
||
function nextTrack() {
|
||
currentTrackIndex = (currentTrackIndex + 1) % MUSIC_PLAYLIST.length;
|
||
loadCurrentTrack();
|
||
musicPlayer.play();
|
||
}
|
||
|
||
// Call this once when the game starts
|
||
function startMusic() {
|
||
loadCurrentTrack();
|
||
musicPlayer.play();
|
||
}
|
||
|
||
// Call this when the game ends (optional – mute/stop)
|
||
function stopMusic() {
|
||
musicPlayer.pause();
|
||
musicPlayer.currentTime = 0;
|
||
}
|
||
|
||
// Play the “piece placed” sound effect
|
||
function playSFX(effect) {
|
||
const sfx = document.getElementById(effect);
|
||
// Re‑start the sound even if it’s still playing
|
||
sfx.currentTime = 0;
|
||
sfx.play().catch(() => {}); // silence any autoplay errors
|
||
}
|
||
|
||
// --- CORE LOGIC ---
|
||
|
||
function createMatrix(w, h) {
|
||
const matrix = [];
|
||
while (h--) {
|
||
matrix.push(new Array(w).fill(0));
|
||
}
|
||
return matrix;
|
||
}
|
||
|
||
function collide(arena, player) {
|
||
const m = player.matrix;
|
||
const o = player.pos;
|
||
for (let y = 0; y < m.length; ++y) {
|
||
for (let x = 0; x < m[y].length; ++x) {
|
||
if (m[y][x] !== 0 &&
|
||
(arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function draw() {
|
||
// Clear Canvas
|
||
context.fillStyle = '#050510';
|
||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// Draw Arena (Locked pieces)
|
||
drawMatrix(arena, {x: 0, y: 0});
|
||
|
||
// Draw Active Piece
|
||
drawMatrix(player.matrix, player.pos);
|
||
|
||
// Draw Particles
|
||
updateAndDrawParticles();
|
||
}
|
||
|
||
function drawMatrix(matrix, offset) {
|
||
matrix.forEach((row, y) => {
|
||
row.forEach((value, x) => {
|
||
if (value !== 0) {
|
||
// The Core Block
|
||
context.fillStyle = COLORS[value];
|
||
context.fillRect(x + offset.x, y + offset.y, 1, 1);
|
||
|
||
// The Neon Glow Effect (Inner)
|
||
context.shadowBlur = 15;
|
||
context.shadowColor = GLOW_COLORS[value];
|
||
|
||
// Bevel effect simulation
|
||
context.strokeStyle = 'rgba(255,255,255,0.5)';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Reset shadow for performance/cleanliness
|
||
context.shadowBlur = 0;
|
||
}
|
||
|
||
function merge(arena, player) {
|
||
player.matrix.forEach((row, y) => {
|
||
row.forEach((value, x) => {
|
||
if (value !== 0) {
|
||
arena[y + player.pos.y][x + player.pos.x] = value;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function rotate(matrix, dir) {
|
||
for (let y = 0; y < matrix.length; ++y) {
|
||
for (let x = 0; x < y; ++x) {
|
||
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
|
||
}
|
||
}
|
||
if (dir > 0) {
|
||
matrix.forEach(row => row.reverse());
|
||
} else {
|
||
matrix.reverse();
|
||
}
|
||
}
|
||
|
||
function playerDrop() {
|
||
player.pos.y++;
|
||
if (collide(arena, player)) {
|
||
player.pos.y--;
|
||
merge(arena, player);
|
||
playSFX('sfx-place');
|
||
playerReset();
|
||
arenaSweep();
|
||
updateScore();
|
||
}
|
||
dropCounter = 0;
|
||
}
|
||
|
||
function playerMove(offset) {
|
||
player.pos.x += offset;
|
||
if (collide(arena, player)) {
|
||
player.pos.x -= offset;
|
||
}
|
||
}
|
||
|
||
function playerReset() {
|
||
const pieces = 'ILJOTSZ';
|
||
player.matrix = createPiece(pieces[pieces.length * Math.random() | 0]);
|
||
player.pos.y = 0;
|
||
player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
|
||
|
||
if (collide(arena, player)) {
|
||
gameOver();
|
||
}
|
||
}
|
||
|
||
function playerRotate(dir) {
|
||
const pos = player.pos.x;
|
||
let offset = 1;
|
||
rotate(player.matrix, dir);
|
||
while (collide(arena, player)) {
|
||
player.pos.x += offset;
|
||
offset = -(offset + (offset > 0 ? 1 : -1));
|
||
if (offset > player.matrix[0].length) {
|
||
rotate(player.matrix, -dir);
|
||
player.pos.x = pos;
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- GAME MECHANICS ---
|
||
|
||
function arenaSweep() {
|
||
let rowCount = 0;
|
||
outer: for (let y = arena.length - 1; y > 0; --y) {
|
||
for (let x = 0; x < arena[y].length; ++x) {
|
||
if (arena[y][x] === 0) {
|
||
continue outer;
|
||
}
|
||
}
|
||
|
||
// ROW CLEARED
|
||
const row = arena.splice(y, 1)[0];
|
||
|
||
// Create Explosion Effect for the cleared row
|
||
createExplosion(y, row);
|
||
playSFX('sfx-explode');
|
||
|
||
arena.unshift(row.fill(0));
|
||
++y;
|
||
rowCount++;
|
||
}
|
||
|
||
if (rowCount > 0) {
|
||
// Scoring: 100, 300, 500, 800
|
||
const points = [100, 300, 500, 800];
|
||
score += points[rowCount - 1] * level;
|
||
|
||
linesClearedTotal += rowCount;
|
||
|
||
// Level Up Logic: Every 5 rows
|
||
while (linesClearedTotal >= level * 5) {
|
||
levelUp(); // increments `level` and updates UI
|
||
}
|
||
}
|
||
}
|
||
|
||
function levelUp() {
|
||
level++;
|
||
|
||
// Increase Speed (Decrease interval)
|
||
// Cap at minimum 100ms
|
||
dropInterval = Math.max(100, 1000 - (level * 120));
|
||
|
||
// Visual feedback on UI
|
||
document.getElementById('level-value').innerText = level;
|
||
|
||
// Flash border effect
|
||
const container = document.getElementById('game-container');
|
||
container.style.borderColor = '#fff';
|
||
setTimeout(() => {
|
||
container.style.borderColor = '#333';
|
||
}, 200);
|
||
|
||
playSFX('sfx-levelUp');
|
||
|
||
// *** advance to next music track ***
|
||
nextTrack();
|
||
|
||
// ----- NEW: show “Level Up!” animation -----
|
||
const overlay = document.getElementById('level-up-overlay');
|
||
overlay.classList.remove('active'); // reset (in case it’s still there)
|
||
// Force reflow so the removal takes effect
|
||
void overlay.offsetWidth;
|
||
overlay.classList.add('active');
|
||
|
||
// Remove the class after the animation finishes (1.5 s)
|
||
setTimeout(() => overlay.classList.remove('active'), 1500);
|
||
}
|
||
|
||
function updateScore() {
|
||
document.getElementById('score').innerText = score;
|
||
document.getElementById('lines').innerText = linesClearedTotal;
|
||
}
|
||
|
||
// --- PARTICLE SYSTEM (The Cool Stuff) ---
|
||
|
||
class Particle {
|
||
constructor(x, y, color) {
|
||
this.x = x;
|
||
this.y = y;
|
||
this.vx = (Math.random() - 0.5) * 1; // Velocity X
|
||
this.vy = (Math.random() - 0.5) * 1 - 0.5; // Velocity Y (upward bias)
|
||
this.life = 1.0; // Opacity/Life
|
||
this.color = color;
|
||
this.size = Math.random() * 0.8 + 0.2;
|
||
}
|
||
|
||
update() {
|
||
this.x += this.vx;
|
||
this.y += this.vy;
|
||
this.vy += 0.05; // Gravity
|
||
this.life -= 0.02; // Fade out
|
||
}
|
||
|
||
draw(ctx) {
|
||
ctx.save();
|
||
ctx.globalAlpha = this.life;
|
||
ctx.fillStyle = this.color;
|
||
|
||
// Glow for particles
|
||
ctx.shadowBlur = 10;
|
||
ctx.shadowColor = this.color;
|
||
|
||
// Draw Particle (Square or Circle)
|
||
if (Math.random() > 0.5) {
|
||
ctx.fillRect(this.x, this.y, this.size, this.size);
|
||
} else {
|
||
ctx.beginPath();
|
||
ctx.arc(this.x, this.y, this.size/2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
|
||
// Draw "Tech" line fragments
|
||
if (this.life > 0.5) {
|
||
context.strokeStyle = '#fff';
|
||
context.lineWidth = 0.1;
|
||
context.beginPath();
|
||
context.moveTo(this.x, this.y);
|
||
context.lineTo(this.x + (Math.random()-0.5), this.y + (Math.random()-0.5));
|
||
context.stroke();
|
||
}
|
||
|
||
ctx.restore();
|
||
}
|
||
}
|
||
|
||
function createExplosion(rowY, rowData) {
|
||
// Iterate through the row that was cleared
|
||
for (let x = 0; x < rowData.length; x++) {
|
||
// Create multiple particles per block
|
||
for (let i = 0; i < 5; i++) {
|
||
// Convert grid coordinates to pixels (approximate)
|
||
const colorIndex = rowData[x];
|
||
if(colorIndex !== 0) {
|
||
const color = COLORS[colorIndex];
|
||
particles.push(new Particle(x + Math.random()*0.5, rowY + Math.random()*0.5, color));
|
||
}
|
||
}
|
||
}
|
||
// Add some white sparks
|
||
for(let i=0; i<10; i++) {
|
||
particles.push(new Particle(Math.random() * 10, rowY + 0.5, '#ffffff'));
|
||
}
|
||
}
|
||
|
||
function updateAndDrawParticles() {
|
||
// Iterate backwards to remove dead particles safely
|
||
for (let i = particles.length - 1; i >= 0; i--) {
|
||
const p = particles[i];
|
||
p.update();
|
||
p.draw(context);
|
||
|
||
if (p.life <= 0) {
|
||
particles.splice(i, 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- GAME LOOP ---
|
||
|
||
function update(time = 0) {
|
||
const deltaTime = time - lastTime;
|
||
lastTime = time;
|
||
|
||
dropCounter += deltaTime;
|
||
if (dropCounter > dropInterval) {
|
||
playerDrop();
|
||
}
|
||
|
||
draw();
|
||
requestAnimationFrame(update);
|
||
}
|
||
|
||
// --- CONTROLS & STATE MANAGEMENT ---
|
||
|
||
document.addEventListener('keydown', event => {
|
||
if(event.keyCode === 37) { // Left
|
||
playerMove(-1);
|
||
} else if (event.keyCode === 39) { // Right
|
||
playerMove(1);
|
||
} else if (event.keyCode === 40) { // Down
|
||
playerDrop();
|
||
} else if (event.keyCode === 81) { // Q (Rotate Left - optional)
|
||
playerRotate(-1);
|
||
} else if (event.keyCode === 87 || event.keyCode === 38) { // W or Up (Rotate Right)
|
||
playerRotate(1);
|
||
} else if (event.keyCode === 32) { // Space (Hard Drop - instant to bottom)
|
||
while (!collide(arena, player)) {
|
||
player.pos.y++;
|
||
}
|
||
player.pos.y--;
|
||
merge(arena, player);
|
||
playerReset();
|
||
arenaSweep();
|
||
updateScore();
|
||
dropCounter = 0;
|
||
}
|
||
});
|
||
|
||
function startGame() {
|
||
document.getElementById('start-screen').style.display = 'none';
|
||
document.getElementById('game-over-screen').style.display = 'none';
|
||
|
||
// Reset Game State
|
||
arena.forEach(row => row.fill(0));
|
||
score = 0;
|
||
linesClearedTotal = 0;
|
||
level = 1;
|
||
dropInterval = 1000;
|
||
particles = [];
|
||
|
||
updateScore();
|
||
playerReset();
|
||
startMusic();
|
||
update();
|
||
}
|
||
|
||
function gameOver() {
|
||
document.getElementById('game-over-screen').style.display = 'flex';
|
||
cancelAnimationFrame(update);
|
||
}
|
||
|
||
function resetGame() {
|
||
startGame();
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
</html> |