Games / block-blast.html
Azivandor's picture
Update block-blast.html
6a42e6f verified
Raw History Blame Contribute Delete
15.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Block Blast</title>
<style>
* { -webkit-tap-highlight-color: transparent; user-select: none; touch-action: none; }
html, body {
margin: 0; padding: 0; min-height: 100%;
background: radial-gradient(circle at 50% 10%, #eaf6ff, #bfe0fb 70%);
display: flex; align-items: center; justify-content: center;
font-family: 'Segoe UI', Arial, sans-serif;
}
#wrap { text-align: center; padding: 12px 0; }
canvas {
background: #eaf6ff;
border: 3px solid #2a8fd8;
box-shadow: 0 0 24px #2a8fd855, inset 0 0 20px #ffffffaa;
border-radius: 10px;
}
#hud {
display: flex; justify-content: space-between; align-items: center;
color: #16406b; font-size: 16px; letter-spacing: 1px;
width: 360px; margin: 0 auto 8px auto;
text-shadow: none;
}
#hud .score {
font-family: 'Courier New', monospace;
font-size: 26px; font-weight: bold; color: #ff8a3d; text-shadow: 0 0 6px #ffcaa3;
}
#best { font-size: 13px; color: #3d76ad; }
#msg {
position: absolute; color: #16406b; font-size: 28px; font-weight: bold;
text-shadow: none;
background: rgba(255,255,255,0.85);
padding: 16px 24px; border-radius: 12px;
box-shadow: 0 0 20px #2a8fd855;
display: none; text-align: center; pointer-events: none;
}
#msg span { display:block; font-size: 15px; font-weight: normal; margin-top: 10px; color: #3d76ad; }
</style>
</head>
<body>
<a href="index.html" style="position:fixed; top:10px; left:10px; z-index:999; display:inline-flex; align-items:center; gap:5px; padding:7px 12px; border-radius:20px; background:rgba(10,10,20,0.75); color:#fff; font-family:'Segoe UI', Arial, sans-serif; font-size:13px; font-weight:bold; text-decoration:none; border:1px solid rgba(255,255,255,0.25); backdrop-filter: blur(3px);">← Home</a>
<div id="wrap">
<div id="hud">
<div>
<div class="score" id="score">0</div>
<div id="best">best: <span id="bestVal">0</span></div>
</div>
<div id="comboLabel" style="font-size:13px; color:#1e9e4f; min-height:16px; font-weight:bold;"></div>
</div>
<canvas id="game" width="360" height="560"></canvas>
</div>
<div id="msg"></div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('bestVal');
const comboLabelEl = document.getElementById('comboLabel');
const msgEl = document.getElementById('msg');
const GRID = 8;
const CELL = 34;
const boardX = (W - GRID*CELL) / 2;
const boardY = 56;
const COLORS = ['#ff4ecb', '#4ecbff', '#ffe14e', '#63ff8f', '#ff8a3d', '#a273ff', '#ff5a5a'];
const SHAPES = [
[[0,0]],
[[0,0],[1,0]], [[0,0],[0,1]],
[[0,0],[1,0],[2,0]], [[0,0],[0,1],[0,2]],
[[0,0],[1,0],[0,1]], [[0,0],[1,0],[1,1]], [[0,1],[1,1],[1,0]], [[0,0],[0,1],[1,1]],
[[0,0],[1,0],[0,1],[1,1]],
[[0,0],[1,0],[2,0],[3,0]], [[0,0],[0,1],[0,2],[0,3]],
[[0,0],[0,1],[0,2],[1,2]], [[0,0],[1,0],[2,0],[0,1]], [[0,0],[1,0],[1,1],[1,2]], [[2,0],[0,1],[1,1],[2,1]],
[[1,0],[1,1],[1,2],[0,2]], [[0,0],[0,1],[1,1],[2,1]], [[0,0],[1,0],[0,1],[0,2]], [[0,0],[1,0],[2,0],[2,1]],
[[0,0],[1,0],[2,0],[1,1]], [[1,0],[0,1],[1,1],[1,2]], [[1,0],[0,1],[1,1],[2,1]], [[0,0],[0,1],[0,2],[1,1]],
[[1,0],[2,0],[0,1],[1,1]], [[0,0],[1,0],[1,1],[2,1]],
[[1,0],[0,1],[1,1],[2,1],[1,2]],
[[0,0],[1,0],[2,0],[3,0],[4,0]], [[0,0],[0,1],[0,2],[0,3],[0,4]],
[[0,0],[1,0],[2,0],[0,1],[1,1],[2,1]],
[[0,0],[0,1],[0,2],[1,2],[2,2]],
];
let score = 0, best = 0, displayScore = 0;
let board = Array.from({length: GRID}, () => Array(GRID).fill(null));
let tray = [null, null, null];
let dragging = null; // { trayIndex, shape, color, px, py }
let flashCells = []; // legacy, unused now but kept safe
let clearingCells = []; // { r, c, color, timer } — shrink/fade animation for cleared cells
let placingCells = []; // { r, c, timer } — pop-in bounce for freshly placed cells
let particles = []; // { x, y, vx, vy, life, color, size }
let shakeTimer = 0, shakeMag = 0;
let gameOver = false;
function randShape(){
const shape = SHAPES[Math.floor(Math.random()*SHAPES.length)];
const color = COLORS[Math.floor(Math.random()*COLORS.length)];
return { shape, color };
}
function refillTrayIfEmpty(){
if (tray.every(t => t === null)){
tray = [randShape(), randShape(), randShape()];
if (checkGameOver()) triggerGameOver();
}
}
refillTrayIfEmpty();
function setScore(v){
score = v;
if (score > best) best = score;
}
function updateScoreDisplay(){
if (displayScore < score){
const step = Math.max(1, Math.ceil((score - displayScore) / 10));
displayScore = Math.min(score, displayScore + step);
}
scoreEl.textContent = displayScore;
bestEl.textContent = best;
}
function bbox(shape){
let maxX = 0, maxY = 0;
for (const [x,y] of shape){ maxX = Math.max(maxX,x); maxY = Math.max(maxY,y); }
return { w: maxX+1, h: maxY+1 };
}
function canPlace(shape, col, row){
for (const [dx,dy] of shape){
const c = col+dx, r = row+dy;
if (c < 0 || c >= GRID || r < 0 || r >= GRID) return false;
if (board[r][c]) return false;
}
return true;
}
function anyPlacement(shape){
for (let r = 0; r < GRID; r++)
for (let c = 0; c < GRID; c++)
if (canPlace(shape, c, r)) return true;
return false;
}
function checkGameOver(){
return tray.every(t => t === null || !anyPlacement(t.shape));
}
function triggerGameOver(){
gameOver = true;
msgEl.style.display = 'block';
msgEl.innerHTML = `GAME OVER<span>Score: ${score} — click to try again</span>`;
}
function newGame(){
score = 0; displayScore = 0; setScore(0);
board = Array.from({length: GRID}, () => Array(GRID).fill(null));
tray = [null, null, null];
gameOver = false;
clearingCells = []; placingCells = []; particles = []; shakeTimer = 0;
msgEl.style.display = 'none';
comboLabelEl.textContent = '';
refillTrayIfEmpty();
}
function spawnParticles(x, y, color, count){
for (let i = 0; i < count; i++){
const angle = Math.random() * Math.PI * 2;
const speed = 1.5 + Math.random() * 3.5;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
life: 1,
color,
size: 3 + Math.random() * 3
});
}
}
function placePiece(trayIndex, shape, color, col, row){
for (const [dx,dy] of shape){
board[row+dy][col+dx] = color;
placingCells.push({ r: row+dy, c: col+dx, timer: 1 });
}
setScore(score + shape.length * 2);
tray[trayIndex] = null;
const fullRows = [], fullCols = [];
for (let r = 0; r < GRID; r++) if (board[r].every(v => v)) fullRows.push(r);
for (let c = 0; c < GRID; c++) if (board.every(row => row[c])) fullCols.push(c);
const linesCleared = fullRows.length + fullCols.length;
if (linesCleared > 0){
const clearedCoords = new Set();
for (const r of fullRows) for (let c = 0; c < GRID; c++) clearedCoords.add(r + ',' + c);
for (const c of fullCols) for (let r = 0; r < GRID; r++) clearedCoords.add(r + ',' + c);
clearedCoords.forEach(key => {
const [r, c] = key.split(',').map(Number);
const cellColor = board[r][c] || '#ffffff';
clearingCells.push({ r, c, color: cellColor, timer: 1 });
const cx = boardX + c*CELL + CELL/2, cy = boardY + r*CELL + CELL/2;
spawnParticles(cx, cy, cellColor, 5);
board[r][c] = null;
});
shakeTimer = 10 + linesCleared * 5;
shakeMag = 3 + linesCleared * 2.5;
const bonus = linesCleared * 80 + (linesCleared > 1 ? (linesCleared-1) * 60 : 0);
setScore(score + bonus);
comboLabelEl.textContent = linesCleared > 1 ? `${linesCleared}x COMBO! +${bonus}` : `LINE! +${bonus}`;
setTimeout(() => { if (comboLabelEl.textContent.includes(String(bonus))) comboLabelEl.textContent = ''; }, 1200);
}
refillTrayIfEmpty();
if (!gameOver && checkGameOver()) triggerGameOver();
}
// ---- Input ----
function getPos(e){
const rect = canvas.getBoundingClientRect();
const scaleX = W/rect.width, scaleY = H/rect.height;
const t = e.touches ? e.touches[0] : e;
return { x: (t.clientX-rect.left)*scaleX, y: (t.clientY-rect.top)*scaleY };
}
function trayImgSlots(){
const slotW = 108, gap = 10;
const totalW = slotW*3 + gap*2;
const startX = (W - totalW)/2;
const y = boardY + GRID*CELL + 20;
return [0,1,2].map(i => ({ x: startX + i*(slotW+gap), y, w: slotW, h: 100 }));
}
function pointerDown(e){
if (gameOver){ newGame(); return; }
const pos = getPos(e);
const slots = trayImgSlots();
for (let i = 0; i < 3; i++){
const piece = tray[i];
if (!piece) continue;
const s = slots[i];
if (pos.x >= s.x && pos.x <= s.x+s.w && pos.y >= s.y && pos.y <= s.y+s.h){
dragging = { trayIndex: i, shape: piece.shape, color: piece.color, px: pos.x, py: pos.y };
e.preventDefault && e.preventDefault();
return;
}
}
}
function pointerMove(e){
if (!dragging) return;
const pos = getPos(e);
dragging.px = pos.x; dragging.py = pos.y;
e.preventDefault && e.preventDefault();
}
function currentPreview(){
if (!dragging) return null;
const { w, h } = bbox(dragging.shape);
const anchorX = dragging.px, anchorY = dragging.py - 46; // lift above finger/cursor
const centerCol = Math.round((anchorX - boardX) / CELL - (w-1)/2);
const centerRow = Math.round((anchorY - boardY) / CELL - (h-1)/2);
return { col: centerCol, row: centerRow };
}
function pointerUp(e){
if (!dragging) return;
const preview = currentPreview();
const valid = preview && canPlace(dragging.shape, preview.col, preview.row);
if (valid) placePiece(dragging.trayIndex, dragging.shape, dragging.color, preview.col, preview.row);
dragging = null;
}
canvas.addEventListener('mousedown', pointerDown);
canvas.addEventListener('mousemove', pointerMove);
canvas.addEventListener('mouseup', pointerUp);
canvas.addEventListener('mouseleave', () => { dragging = null; });
canvas.addEventListener('touchstart', pointerDown, { passive: false });
canvas.addEventListener('touchmove', pointerMove, { passive: false });
canvas.addEventListener('touchend', pointerUp);
// ---- Draw ----
function drawBoard(){
ctx.save();
ctx.shadowColor = '#2a8fd8';
ctx.shadowBlur = 6;
ctx.fillStyle = '#d5ecfb';
ctx.fillRect(boardX-4, boardY-4, GRID*CELL+8, GRID*CELL+8);
ctx.shadowBlur = 0;
ctx.restore();
for (let r = 0; r < GRID; r++){
for (let c = 0; c < GRID; c++){
const x = boardX + c*CELL, y = boardY + r*CELL;
const val = board[r][c];
ctx.fillStyle = val ? val : ((r+c)%2===0 ? '#eef8ff' : '#e2f0fc');
if (val){ ctx.shadowColor = val; ctx.shadowBlur = 6; }
const placing = placingCells.find(p => p.r === r && p.c === c);
if (placing){
const t = placing.timer; // 1 -> 0
const scale = 1 + 0.35 * Math.sin(t * Math.PI); // bounce out then settle
const size = (CELL-2) * scale;
const off = (CELL-2 - size) / 2;
ctx.fillRect(x+1+off, y+1+off, size, size);
} else {
ctx.fillRect(x+1, y+1, CELL-2, CELL-2);
}
ctx.shadowBlur = 0;
if (val && !placing){
ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.fillRect(x+2, y+2, CELL-4, (CELL-4)*0.35);
}
}
}
placingCells.forEach(p => p.timer -= 0.12);
placingCells = placingCells.filter(p => p.timer > 0);
// shrinking / fading cells for lines that just cleared
clearingCells.forEach(f => {
const x = boardX + f.c*CELL + CELL/2, y = boardY + f.r*CELL + CELL/2;
const t = f.timer; // 1 -> 0
const size = (CELL-2) * t;
ctx.save();
ctx.globalAlpha = t;
ctx.fillStyle = f.color;
ctx.shadowColor = f.color; ctx.shadowBlur = 10;
ctx.fillRect(x - size/2, y - size/2, size, size);
ctx.restore();
f.timer -= 0.1;
});
clearingCells = clearingCells.filter(f => f.timer > 0);
// particle burst
particles.forEach(p => {
p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life -= 0.035;
ctx.save();
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.color;
ctx.shadowColor = p.color; ctx.shadowBlur = 6;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
ctx.restore();
});
particles = particles.filter(p => p.life > 0);
ctx.strokeStyle = '#2a8fd855';
ctx.lineWidth = 1;
for (let i = 0; i <= GRID; i++){
ctx.beginPath(); ctx.moveTo(boardX, boardY+i*CELL); ctx.lineTo(boardX+GRID*CELL, boardY+i*CELL); ctx.stroke();
ctx.beginPath(); ctx.moveTo(boardX+i*CELL, boardY); ctx.lineTo(boardX+i*CELL, boardY+GRID*CELL); ctx.stroke();
}
}
function drawPreview(){
if (!dragging) return;
const preview = currentPreview();
if (!preview) return;
const valid = canPlace(dragging.shape, preview.col, preview.row);
for (const [dx,dy] of dragging.shape){
const c = preview.col+dx, r = preview.row+dy;
if (c<0 || c>=GRID || r<0 || r>=GRID) continue;
const x = boardX + c*CELL, y = boardY + r*CELL;
ctx.fillStyle = valid ? 'rgba(99,255,143,0.45)' : 'rgba(255,90,90,0.45)';
ctx.fillRect(x+2, y+2, CELL-4, CELL-4);
ctx.strokeStyle = valid ? '#63ff8f' : '#ff5a5a';
ctx.lineWidth = 2;
ctx.strokeRect(x+2, y+2, CELL-4, CELL-4);
}
}
function drawPieceInBox(shape, color, box, blockSize){
const { w, h } = bbox(shape);
const offX = box.x + (box.w - w*blockSize)/2;
const offY = box.y + (box.h - h*blockSize)/2;
for (const [dx,dy] of shape){
const x = offX + dx*blockSize, y = offY + dy*blockSize;
ctx.fillStyle = color;
ctx.shadowColor = color; ctx.shadowBlur = 6;
ctx.fillRect(x+1, y+1, blockSize-2, blockSize-2);
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.fillRect(x+2, y+2, blockSize-4, (blockSize-4)*0.35);
}
}
function drawTray(){
const slots = trayImgSlots();
for (let i = 0; i < 3; i++){
const s = slots[i];
ctx.strokeStyle = '#2a8fd855';
ctx.lineWidth = 1.5;
ctx.strokeRect(s.x, s.y, s.w, s.h);
const piece = tray[i];
if (piece && !(dragging && dragging.trayIndex === i)){
drawPieceInBox(piece.shape, piece.color, s, 18);
}
}
}
function drawDraggedPiece(){
if (!dragging) return;
const { w, h } = bbox(dragging.shape);
const blockSize = CELL;
const anchorX = dragging.px, anchorY = dragging.py - 46;
const offX = anchorX - (w*blockSize)/2;
const offY = anchorY - (h*blockSize)/2;
ctx.save();
ctx.globalAlpha = 0.9;
for (const [dx,dy] of dragging.shape){
const x = offX + dx*blockSize, y = offY + dy*blockSize;
ctx.fillStyle = dragging.color;
ctx.shadowColor = dragging.color; ctx.shadowBlur = 10;
ctx.fillRect(x+2, y+2, blockSize-4, blockSize-4);
}
ctx.restore();
ctx.shadowBlur = 0;
}
function drawTitle(){
ctx.save();
ctx.font = 'bold 18px Segoe UI';
ctx.textAlign = 'center';
ctx.fillStyle = '#1565c0';
ctx.shadowColor = '#7fc4f0'; ctx.shadowBlur = 8;
ctx.fillText('B L O C K B L A S T', W/2, 30);
ctx.restore();
}
function render(){
ctx.clearRect(0,0,W,H);
const bg = ctx.createLinearGradient(0,0,0,H);
bg.addColorStop(0, '#f2faff'); bg.addColorStop(1, '#cfe8fb');
ctx.fillStyle = bg; ctx.fillRect(0,0,W,H);
ctx.save();
if (shakeTimer > 0){
const amt = shakeMag * (shakeTimer / 25);
ctx.translate((Math.random()-0.5)*amt, (Math.random()-0.5)*amt);
shakeTimer--;
}
drawTitle();
drawBoard();
drawPreview();
drawTray();
drawDraggedPiece();
ctx.restore();
}
function loop(){
updateScoreDisplay();
render();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>