Compare commits
2 Commits
7b5119d266
...
ed94e946e9
| Author | SHA1 | Date |
|---|---|---|
|
|
ed94e946e9 | |
|
|
6a633956a4 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,802 @@
|
|||
<!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 8 rows
|
||||
if (linesClearedTotal % 6 === 0) {
|
||||
levelUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue