NeonBlocks/index.html

802 lines
24 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;
}
</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 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
}
}
}
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 its still there)
// Force reflow so the removal takes effect
void overlay.offsetWidth;
overlay.classList.add('active');
// Remove the class after the animation finishes (1.5s)
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>