NeonBlocks/index.html

987 lines
31 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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); }
}
/* ------------------------------------------------------------------ */
/* Neonblue 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 easeinout 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); }
}
/* ------------------------------------------------------------------ */
/* LevelUp 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);
pointerevents: 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;
}
.wipe {
/* A fullscreen coloured overlay that slides across */
position: fixed;
inset: 0;
background: var(--wipe-color);
mix-blend-mode: screen; /* keep the glitch/scanline look */
animation: wipeAnim 0.8s ease-out forwards;
pointer-events: none;
z-index: 150; /* above .bg-effect but below UI */
}
/* Wipe moves from left → right */
@keyframes wipeAnim {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* ------------------------------------------------------------------ */
/* 2⃣ Define a palette of 10 neon colours (CSS custom properties) */
/* ------------------------------------------------------------------ */
:root {
/* existing vars … */
--neon-0: #c266c2; /* muted magenta */
--neon-1: #66c2c2; /* muted cyan */
--neon-2: #c26633; /* muted orangered */
--neon-3: #66c266; /* muted chartreuse */
--neon-4: #c2668a; /* muted deep pink */
--neon-5: #66c28a; /* muted spring green */
--neon-6: #c28533; /* muted dark orange */
--neon-7: #6688c2; /* muted deep sky blue */
--neon-8: #c266a0; /* muted hot pink */
--neon-9: #66c266; /* muted lawn green */
}
/* --------------------------------------------------------------
Digital Rain (Matrixstyle) sits above .bg-effect, below UI
-------------------------------------------------------------- */
.digital-rain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background: transparent;
}
/* Glitch flash that appears when wind changes */
body.glitch-flash::after {
content: "";
position: fixed;
inset: 0;
background: rgba(255,255,255,0.3);
mix-blend-mode: screen;
pointer-events: none;
animation: glitchFlash 0.3s forwards;
}
/* Flash animation */
@keyframes glitchFlash {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
</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>
<canvas class="digital-rain"></canvas>
<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 dont 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);
// Restart the sound even if its 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
}
}
}
// 10colour neon palette (same order as the CSS vars above)
const NEON_PALETTE = [
getComputedStyle(document.documentElement).getPropertyValue('--neon-0').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-1').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-2').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-3').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-4').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-5').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-6').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-7').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-8').trim(),
getComputedStyle(document.documentElement).getPropertyValue('--neon-9').trim(),
];
function levelUp() {
level++;
dropInterval = Math.max(100, 1000 - (level * 120));
document.getElementById('level-value').innerText = level;
// Flash border (unchanged)
const container = document.getElementById('game-container');
container.style.borderColor = '#fff';
setTimeout(() => container.style.borderColor = '#333', 200);
playSFX('sfx-levelUp');
nextTrack();
// ----- NEW: LevelUp “Level Up!” overlay (unchanged) -----
const overlay = document.getElementById('level-up-overlay');
overlay.classList.remove('active');
void overlay.offsetWidth;
overlay.classList.add('active');
setTimeout(() => overlay.classList.remove('active'), 1500);
/* -------------------------------------------------------------
* NEW SECTION background colour change + wipe effect
* ------------------------------------------------------------- */
// 1⃣ Pick a random neon colour
const newColor = NEON_PALETTE[Math.floor(Math.random() * NEON_PALETTE.length)];
// 2⃣ Apply it to the .bg-effect element (the base background)
const bg = document.querySelector('.bg-effect');
bg.style.background = newColor;
// 3⃣ Create a temporary wipe overlay
const wipe = document.createElement('div');
wipe.className = 'wipe';
// expose the colour to the CSS animation via a custom property
wipe.style.setProperty('--wipe-color', newColor);
document.body.appendChild(wipe);
// 4⃣ Remove the wipe element after the animation finishes
wipe.addEventListener('animationend', () => wipe.remove());
}
function updateScore() {
document.getElementById('score').innerText = score;
document.getElementById('lines').innerText = linesClearedTotal;
}
// --------------------------------------------------------------
// DIGITAL RAIN (Matrixstyle) added after the existing script
// --------------------------------------------------------------
// Canvas & context for the rain effect
const rainCanvas = document.querySelector('.digital-rain');
const rainCtx = rainCanvas.getContext('2d');
// Resize canvas to fill the viewport
function resizeRainCanvas() {
rainCanvas.width = window.innerWidth;
rainCanvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeRainCanvas);
resizeRainCanvas();
// Characters used for the rain (feel free to customise)
const RAIN_CHARS = 'アァカサタナハマヤラワガザダバパイィキシチニヒミリヰギジヂビピウゥクスツヌフムユルヲグズブポエェケセテネヘメレヱゲゼデベペオォコソトホモヨロヲゴゾボポ01'.split('');
// Rain drop definition
class RainDrop {
constructor(x) {
this.x = x; // column (pixel)
this.y = Math.random() * -rainCanvas.height; // start above view
this.speed = 2 + Math.random() * 3; // fall speed
this.char = RAIN_CHARS[Math.floor(Math.random()*RAIN_CHARS.length)];
}
update(windAngle) {
// Apply wind: slight horizontal drift
this.x += Math.sin(windAngle) * 0.5;
this.y += this.speed + Math.cos(windAngle) * 0.5;
// Reset when offscreen
if (this.y > rainCanvas.height) {
this.y = Math.random() * -100;
this.x = Math.random() * rainCanvas.width;
this.char = RAIN_CHARS[Math.floor(Math.random()*RAIN_CHARS.length)];
}
}
draw(ctx) {
ctx.fillStyle = '#0ff';
ctx.font = '16px monospace';
ctx.fillText(this.char, this.x, this.y);
}
}
// Initialise a pool of drops (one per ~20px column)
let rainDrops = [];
function initRain() {
rainDrops = [];
const cols = Math.floor(rainCanvas.width / 20);
for (let i = 0; i < cols; i++) {
rainDrops.push(new RainDrop(i * 20));
}
}
initRain();
// Wind handling
let windAngle = 0; // radians
function changeWind() {
// Random new direction between -30° and +30°
windAngle = (Math.random() - 0.5) * (Math.PI / 3);
triggerGlitchFlash(); // flash when wind shifts
}
// Change wind every 812 seconds
setInterval(changeWind, 8000 + Math.random() * 4000);
// Glitch flash helper
function triggerGlitchFlash() {
document.body.classList.add('glitch-flash');
setTimeout(() => document.body.classList.remove('glitch-flash'), 300);
}
// Rain animation loop (runs alongside the game loop)
let rainRunning = false;
function rainLoop() {
if (!rainRunning) return; // stop when game ends
rainCtx.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
// Update & draw each drop
rainDrops.forEach(drop => {
drop.update(windAngle);
drop.draw(rainCtx);
});
requestAnimationFrame(rainLoop);
}
// --- 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();
// ---- NEW: start rain ----
rainRunning = true;
rainLoop();
}
function gameOver() {
document.getElementById('game-over-screen').style.display = 'flex';
cancelAnimationFrame(update);
// ---- NEW: stop rain ----
rainRunning = false;
}
function resetGame() {
startGame();
}
</script>
</body>
</html>