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