Spaces:
Running
Running
BlockShift Chronicles – A Futuristic Shape-Stacking Puzzle Game Objective: Create a fully responsive HTML5, CSS, and JavaScript puzzle game inspired by Tetris but featuring unique gameplay mechanics and original visuals to ensure no copyright infringement. The game must work seamlessly across desktop, tablet, and mobile browsers with smooth controls and animations. Core Concept Instead of standard falling blocks, players control energy shards that shift and rotate through gravitational zones. The board is hexagonal or circular, unlike the standard rectangular Tetris grid. Players must align matching colors or energy symbols rather than simply filling lines. Matching 3 or more similar shards in connected clusters causes them to detonate in spectacular energy bursts, scoring points and freeing space. Combo chains reward higher multipliers. Difficulty increases by introducing rotating obstacles and gravity reversals after certain levels. Unique Features Gravity Zones: Blocks can fall in multiple directions depending on the zone’s gravitational pull. Color Fusion Mechanic: Merging different color shards creates special power shards with unique effects (e.g., clear nearby blocks, slow time, reverse gravity). Dynamic Board Shapes: Levels shift between hex grids, circular arenas, and spiraling vortex shapes. Combo Scoring: Chain reactions reward skillful planning. Power-Ups: Temporary shields, double-score boosts, and “magnet mode” to attract pieces to certain areas. Art Style & Theme Futuristic neon aesthetic with glowing shards, pulsating energy lines, and smooth particle effects. Background: animated galaxy or energy core that subtly shifts color as levels progress. Minimalistic UI with holographic panels for score, level, and power-up indicators. Technical Requirements Technologies: Pure HTML5 Canvas, CSS3 for responsive layout, vanilla JavaScript (no external game engines). Responsiveness: Auto-scale canvas to fit any screen resolution (mobile, tablet, desktop). Controls: Desktop: Arrow keys (move), Space (rotate), Down (quick drop). Mobile: On-screen swipe controls and tap-to-rotate. Animations: Smooth transitions (60fps), particle effects for explosions, glowing color transitions for combos. Audio: Original retro-futuristic soundtrack + satisfying block placement and explosion sounds. Scoring System: Points for matches, combo multipliers, and time bonuses. Additional Features Level progression with escalating difficulty (speed, new shard types, rotating hazards). Local high score leaderboard stored in browser (IndexedDB/localStorage). Pause, resume, and restart buttons with smooth menu transitions. Optional colorblind mode (patterns instead of colors). Offline play support via service worker (PWA-ready). Final Deliverable A single-page game with clean, modular code separated into index.html, style.css, and game.js. Fully documented code with clear comments for future updates. All assets (shards, effects) must be original vector shapes (no copyrighted textures). Game should feel fast, modern, and addictive, appealing to both casual and hardcore puzzle fans. - Initial Deployment
15bdce5 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>BlockShift Chronicles</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 0.8; } | |
| 50% { opacity: 1; } | |
| } | |
| @keyframes rotate { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .glow { | |
| filter: drop-shadow(0 0 8px currentColor); | |
| } | |
| .particle { | |
| position: absolute; | |
| border-radius: 50%; | |
| pointer-events: none; | |
| animation: fadeOut 1s forwards; | |
| } | |
| @keyframes fadeOut { | |
| to { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(0.5); } | |
| } | |
| .hexagon { | |
| clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); | |
| } | |
| .swipe-area { | |
| touch-action: none; | |
| } | |
| /* Custom scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.3); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.5); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-black text-white font-mono overflow-hidden select-none"> | |
| <div id="game-container" class="relative w-full h-screen flex flex-col items-center justify-center"> | |
| <!-- Main Menu --> | |
| <div id="main-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 transition-opacity duration-500"> | |
| <h1 class="text-6xl md:text-8xl font-bold mb-8 text-transparent bg-clip-text bg-gradient-to-r from-purple-500 to-blue-500 glow"> | |
| BlockShift Chronicles | |
| </h1> | |
| <div class="flex flex-col space-y-4 w-64"> | |
| <button id="start-game" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| New Game | |
| </button> | |
| <button id="continue-game" class="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105 opacity-50" disabled> | |
| Continue | |
| </button> | |
| <button id="high-scores" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| High Scores | |
| </button> | |
| <button id="settings" class="px-6 py-3 bg-yellow-600 hover:bg-yellow-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Settings | |
| </button> | |
| </div> | |
| <div class="mt-12 text-gray-400 text-sm"> | |
| <p>Use arrow keys or swipe to move</p> | |
| <p>Space/Click to rotate</p> | |
| <p>Down arrow/Swipe down to drop</p> | |
| </div> | |
| </div> | |
| <!-- High Scores --> | |
| <div id="high-scores-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 hidden"> | |
| <h2 class="text-4xl mb-8 text-blue-400 glow">High Scores</h2> | |
| <div id="scores-list" class="bg-gray-900 bg-opacity-70 rounded-lg p-4 w-64 max-h-96 overflow-y-auto"> | |
| <!-- Scores will be populated here --> | |
| </div> | |
| <button id="back-to-menu" class="mt-8 px-6 py-3 bg-red-600 hover:bg-red-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Back | |
| </button> | |
| </div> | |
| <!-- Settings --> | |
| <div id="settings-menu" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-50 hidden"> | |
| <h2 class="text-4xl mb-8 text-yellow-400 glow">Settings</h2> | |
| <div class="bg-gray-900 bg-opacity-70 rounded-lg p-6 w-80 max-w-full"> | |
| <div class="mb-6"> | |
| <label class="block text-lg mb-2">Controls</label> | |
| <select id="control-type" class="w-full bg-gray-800 text-white p-2 rounded"> | |
| <option value="keyboard">Keyboard</option> | |
| <option value="touch">Touch Controls</option> | |
| </select> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="block text-lg mb-2">Difficulty</label> | |
| <select id="difficulty" class="w-full bg-gray-800 text-white p-2 rounded"> | |
| <option value="easy">Easy</option> | |
| <option value="medium" selected>Medium</option> | |
| <option value="hard">Hard</option> | |
| </select> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="flex items-center space-x-3"> | |
| <input type="checkbox" id="colorblind-mode" class="form-checkbox h-5 w-5 text-blue-600"> | |
| <span class="text-lg">Colorblind Mode</span> | |
| </label> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="block text-lg mb-2">Sound Volume</label> | |
| <input type="range" id="volume" min="0" max="100" value="70" class="w-full"> | |
| </div> | |
| </div> | |
| <button id="save-settings" class="mt-8 px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Save Settings | |
| </button> | |
| </div> | |
| <!-- Game UI --> | |
| <div id="game-ui" class="absolute inset-0 hidden"> | |
| <!-- Score and Level Display --> | |
| <div class="absolute top-4 left-4 bg-gray-900 bg-opacity-70 rounded-lg p-3"> | |
| <div class="text-lg">Score: <span id="score" class="font-bold">0</span></div> | |
| <div class="text-lg">Level: <span id="level" class="font-bold">1</span></div> | |
| <div class="text-lg">Lines: <span id="lines" class="font-bold">0</span></div> | |
| </div> | |
| <!-- Next Piece Preview --> | |
| <div class="absolute top-4 right-4 bg-gray-900 bg-opacity-70 rounded-lg p-3 flex flex-col items-center"> | |
| <div class="text-lg mb-2">Next:</div> | |
| <canvas id="next-piece" width="100" height="100" class="bg-gray-800 rounded"></canvas> | |
| </div> | |
| <!-- Power-ups --> | |
| <div class="absolute bottom-4 left-4 bg-gray-900 bg-opacity-70 rounded-lg p-3"> | |
| <div class="text-lg mb-2">Power-ups:</div> | |
| <div id="power-ups" class="flex space-x-2"> | |
| <!-- Power-up icons will appear here --> | |
| </div> | |
| </div> | |
| <!-- Game Controls --> | |
| <div id="mobile-controls" class="absolute bottom-4 right-4 hidden"> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button id="rotate-btn" class="bg-blue-600 hover:bg-blue-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold"> | |
| ↻ | |
| </button> | |
| <button id="up-btn" class="bg-purple-600 hover:bg-purple-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold col-start-2"> | |
| ↑ | |
| </button> | |
| <button id="left-btn" class="bg-green-600 hover:bg-green-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2"> | |
| ← | |
| </button> | |
| <button id="down-btn" class="bg-yellow-600 hover:bg-yellow-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2 col-start-2"> | |
| ↓ | |
| </button> | |
| <button id="right-btn" class="bg-red-600 hover:bg-red-700 rounded-full w-16 h-16 flex items-center justify-center text-xl font-bold row-start-2 col-start-3"> | |
| → | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Pause Button --> | |
| <button id="pause-btn" class="absolute top-4 right-1/2 transform translate-x-1/2 bg-gray-900 bg-opacity-70 hover:bg-opacity-90 rounded-lg px-4 py-2 text-lg font-bold"> | |
| Pause | |
| </button> | |
| <!-- Game Canvas --> | |
| <canvas id="game-canvas" class="border border-gray-800 rounded-lg"></canvas> | |
| <!-- Game Over Screen --> | |
| <div id="game-over" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-40 hidden"> | |
| <h2 class="text-5xl mb-6 text-red-500 glow">Game Over!</h2> | |
| <div class="text-2xl mb-8">Final Score: <span id="final-score" class="font-bold">0</span></div> | |
| <div class="flex space-x-4"> | |
| <button id="restart-btn" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Play Again | |
| </button> | |
| <button id="menu-btn" class="px-6 py-3 bg-purple-600 hover:bg-purple-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Main Menu | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Pause Screen --> | |
| <div id="pause-screen" class="absolute inset-0 flex flex-col items-center justify-center bg-black bg-opacity-90 z-40 hidden"> | |
| <h2 class="text-5xl mb-6 text-yellow-400 glow">Paused</h2> | |
| <div class="flex space-x-4"> | |
| <button id="resume-btn" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Resume | |
| </button> | |
| <button id="quit-btn" class="px-6 py-3 bg-red-600 hover:bg-red-700 rounded-lg text-xl font-bold transition-all transform hover:scale-105"> | |
| Quit | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Background Elements --> | |
| <div class="absolute inset-0 overflow-hidden pointer-events-none"> | |
| <div id="particles-container" class="absolute inset-0"></div> | |
| <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-30"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game Constants | |
| const COLORS = [ | |
| '#FF5252', // Red | |
| '#4CAF50', // Green | |
| '#2196F3', // Blue | |
| '#FFC107', // Yellow | |
| '#9C27B0', // Purple | |
| '#00BCD4', // Cyan | |
| '#FF9800', // Orange | |
| '#E91E63' // Pink | |
| ]; | |
| const SHAPES = [ | |
| [[1, 1, 1, 1]], // I | |
| [[1, 1], [1, 1]], // O | |
| [[1, 1, 1], [0, 1, 0]], // T | |
| [[1, 1, 1], [1, 0, 0]], // L | |
| [[1, 1, 1], [0, 0, 1]], // J | |
| [[0, 1, 1], [1, 1, 0]], // S | |
| [[1, 1, 0], [0, 1, 1]] // Z | |
| ]; | |
| const SPECIAL_SHAPES = [ | |
| [[1, 1, 1, 1, 1]], // Long I | |
| [[1, 1, 1], [1, 1, 1], [1, 1, 1]], // Big O | |
| [[1, 0, 1], [1, 1, 1], [1, 0, 1]], // Plus | |
| [[1, 1, 1, 1], [0, 0, 1, 0], [0, 0, 1, 0]], // L with tail | |
| [[1, 1, 1, 1], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]] // Big L | |
| ]; | |
| const POWER_UPS = { | |
| bomb: { name: 'Bomb', color: '#FF5252', effect: 'Clear a 3x3 area' }, | |
| gravity: { name: 'Gravity Shift', color: '#2196F3', effect: 'Reverse gravity direction' }, | |
| slow: { name: 'Time Warp', color: '#9C27B0', effect: 'Slow down pieces for 10 seconds' }, | |
| clear: { name: 'Line Clear', color: '#4CAF50', effect: 'Clear one random line' }, | |
| score: { name: 'Multiplier', color: '#FFC107', effect: '2x score for 15 seconds' } | |
| }; | |
| // Game Variables | |
| let canvas, ctx; | |
| let nextCanvas, nextCtx; | |
| let grid = []; | |
| let currentPiece, nextPiece; | |
| let score = 0; | |
| let level = 1; | |
| let lines = 0; | |
| let gameSpeed = 1000; | |
| let gameInterval; | |
| let isGameOver = false; | |
| let isPaused = false; | |
| let gravityDirection = 'down'; // Can be 'down', 'left', 'right', 'up' | |
| let colorblindMode = false; | |
| let volume = 0.7; | |
| let activePowerUps = []; | |
| let lastUpdateTime = 0; | |
| let touchStartX = 0; | |
| let touchStartY = 0; | |
| let touchEndX = 0; | |
| let touchEndY = 0; | |
| let boardWidth = 10; | |
| let boardHeight = 20; | |
| let cellSize = 30; | |
| let boardOffsetX = 0; | |
| let boardOffsetY = 0; | |
| let gameStarted = false; | |
| let highScores = []; | |
| let settings = { | |
| controls: 'keyboard', | |
| difficulty: 'medium', | |
| colorblind: false, | |
| volume: 70 | |
| }; | |
| // Initialize the game | |
| function init() { | |
| // Load saved data | |
| loadHighScores(); | |
| loadSettings(); | |
| // Set up event listeners | |
| document.getElementById('start-game').addEventListener('click', startGame); | |
| document.getElementById('high-scores').addEventListener('click', showHighScores); | |
| document.getElementById('settings').addEventListener('click', showSettings); | |
| document.getElementById('back-to-menu').addEventListener('click', showMainMenu); | |
| document.getElementById('save-settings').addEventListener('click', saveSettings); | |
| document.getElementById('restart-btn').addEventListener('click', startGame); | |
| document.getElementById('menu-btn').addEventListener('click', showMainMenu); | |
| document.getElementById('resume-btn').addEventListener('click', resumeGame); | |
| document.getElementById('quit-btn').addEventListener('click', quitGame); | |
| document.getElementById('pause-btn').addEventListener('click', togglePause); | |
| // Mobile controls | |
| document.getElementById('rotate-btn').addEventListener('click', () => rotatePiece()); | |
| document.getElementById('up-btn').addEventListener('click', () => movePiece(0, -1)); | |
| document.getElementById('down-btn').addEventListener('click', () => movePiece(0, 1)); | |
| document.getElementById('left-btn').addEventListener('click', () => movePiece(-1, 0)); | |
| document.getElementById('right-btn').addEventListener('click', () => movePiece(1, 0)); | |
| // Touch controls | |
| document.addEventListener('touchstart', handleTouchStart, false); | |
| document.addEventListener('touchmove', handleTouchMove, false); | |
| document.addEventListener('touchend', handleTouchEnd, false); | |
| // Keyboard controls | |
| document.addEventListener('keydown', handleKeyDown); | |
| // Set up canvas | |
| setupCanvas(); | |
| // Create background particles | |
| createBackgroundParticles(); | |
| // Update UI based on settings | |
| updateUIFromSettings(); | |
| } | |
| // Set up the game canvas | |
| function setupCanvas() { | |
| canvas = document.getElementById('game-canvas'); | |
| ctx = canvas.getContext('2d'); | |
| nextCanvas = document.getElementById('next-piece'); | |
| nextCtx = nextCanvas.getContext('2d'); | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| } | |
| // Resize canvas to fit the screen | |
| function resizeCanvas() { | |
| const container = document.getElementById('game-container'); | |
| const width = Math.min(container.clientWidth - 40, 500); | |
| const height = Math.min(container.clientHeight - 40, 800); | |
| // Calculate cell size based on board dimensions | |
| cellSize = Math.min( | |
| Math.floor(width / boardWidth), | |
| Math.floor(height / boardHeight) | |
| ); | |
| // Adjust board dimensions if needed | |
| boardWidth = Math.floor(width / cellSize); | |
| boardHeight = Math.floor(height / cellSize); | |
| // Set canvas dimensions | |
| canvas.width = boardWidth * cellSize; | |
| canvas.height = boardHeight * cellSize; | |
| // Calculate offset to center the board | |
| boardOffsetX = (container.clientWidth - canvas.width) / 2; | |
| boardOffsetY = (container.clientHeight - canvas.height) / 2; | |
| canvas.style.width = `${canvas.width}px`; | |
| canvas.style.height = `${canvas.height}px`; | |
| canvas.style.position = 'absolute'; | |
| canvas.style.left = `${boardOffsetX}px`; | |
| canvas.style.top = `${boardOffsetY}px`; | |
| // Redraw if game is in progress | |
| if (gameStarted && !isGameOver && !isPaused) { | |
| draw(); | |
| } | |
| } | |
| // Start a new game | |
| function startGame() { | |
| // Hide menus | |
| document.getElementById('main-menu').classList.add('hidden'); | |
| document.getElementById('high-scores-menu').classList.add('hidden'); | |
| document.getElementById('settings-menu').classList.add('hidden'); | |
| document.getElementById('game-over').classList.add('hidden'); | |
| // Show game UI | |
| document.getElementById('game-ui').classList.remove('hidden'); | |
| // Reset game state | |
| resetGame(); | |
| // Initialize grid | |
| initGrid(); | |
| // Create first pieces | |
| currentPiece = createPiece(); | |
| nextPiece = createPiece(); | |
| // Start game loop | |
| gameStarted = true; | |
| isGameOver = false; | |
| lastUpdateTime = Date.now(); | |
| gameInterval = setInterval(update, gameSpeed); | |
| // Draw initial state | |
| draw(); | |
| // Show mobile controls if touch device | |
| if (settings.controls === 'touch') { | |
| document.getElementById('mobile-controls').classList.remove('hidden'); | |
| } else { | |
| document.getElementById('mobile-controls').classList.add('hidden'); | |
| } | |
| } | |
| // Reset game variables | |
| function resetGame() { | |
| score = 0; | |
| level = 1; | |
| lines = 0; | |
| gameSpeed = 1000; | |
| gravityDirection = 'down'; | |
| activePowerUps = []; | |
| // Update UI | |
| document.getElementById('score').textContent = score; | |
| document.getElementById('level').textContent = level; | |
| document.getElementById('lines').textContent = lines; | |
| updatePowerUpsDisplay(); | |
| } | |
| // Initialize the game grid | |
| function initGrid() { | |
| grid = []; | |
| for (let y = 0; y < boardHeight; y++) { | |
| grid[y] = []; | |
| for (let x = 0; x < boardWidth; x++) { | |
| grid[y][x] = 0; | |
| } | |
| } | |
| } | |
| // Create a new piece | |
| function createPiece() { | |
| // Random shape and color | |
| const shapeIndex = Math.floor(Math.random() * SHAPES.length); | |
| const colorIndex = Math.floor(Math.random() * COLORS.length); | |
| // 5% chance for a special shape | |
| const isSpecial = Math.random() < 0.05; | |
| const shape = isSpecial ? | |
| SPECIAL_SHAPES[Math.floor(Math.random() * SPECIAL_SHAPES.length)] : | |
| SHAPES[shapeIndex]; | |
| // Starting position (centered at top) | |
| const x = Math.floor(boardWidth / 2) - Math.floor(shape[0].length / 2); | |
| const y = 0; | |
| return { | |
| shape: shape, | |
| color: COLORS[colorIndex], | |
| x: x, | |
| y: y, | |
| rotation: 0 | |
| }; | |
| } | |
| // Game update loop | |
| function update() { | |
| if (isGameOver || isPaused) return; | |
| // Move piece down (or in current gravity direction) | |
| if (gravityDirection === 'down') { | |
| if (!movePiece(0, 1)) { | |
| // Piece couldn't move down, lock it in place | |
| lockPiece(); | |
| // Check for completed lines | |
| const linesCleared = checkLines(); | |
| if (linesCleared > 0) { | |
| updateScore(linesCleared); | |
| // Check for power-up chance (20% per line cleared) | |
| if (Math.random() < 0.2 * linesCleared) { | |
| addRandomPowerUp(); | |
| } | |
| } | |
| // Create new piece | |
| currentPiece = nextPiece; | |
| nextPiece = createPiece(); | |
| // Check if game over (new piece collides immediately) | |
| if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) { | |
| gameOver(); | |
| } | |
| } | |
| } else if (gravityDirection === 'left') { | |
| movePiece(-1, 0); | |
| } else if (gravityDirection === 'right') { | |
| movePiece(1, 0); | |
| } else if (gravityDirection === 'up') { | |
| movePiece(0, -1); | |
| } | |
| // Update power-ups | |
| updatePowerUps(); | |
| // Draw updated game state | |
| draw(); | |
| } | |
| // Move the current piece | |
| function movePiece(dx, dy) { | |
| const newX = currentPiece.x + dx; | |
| const newY = currentPiece.y + dy; | |
| if (!checkCollision(newX, newY, currentPiece.shape)) { | |
| currentPiece.x = newX; | |
| currentPiece.y = newY; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Rotate the current piece | |
| function rotatePiece() { | |
| if (isGameOver || isPaused) return; | |
| const rotated = []; | |
| for (let i = 0; i < currentPiece.shape[0].length; i++) { | |
| rotated[i] = []; | |
| for (let j = currentPiece.shape.length - 1; j >= 0; j--) { | |
| rotated[i][currentPiece.shape.length - 1 - j] = currentPiece.shape[j][i]; | |
| } | |
| } | |
| if (!checkCollision(currentPiece.x, currentPiece.y, rotated)) { | |
| currentPiece.shape = rotated; | |
| currentPiece.rotation = (currentPiece.rotation + 90) % 360; | |
| draw(); | |
| } | |
| } | |
| // Check for collisions | |
| function checkCollision(x, y, shape) { | |
| for (let row = 0; row < shape.length; row++) { | |
| for (let col = 0; col < shape[row].length; col++) { | |
| if (shape[row][col] !== 0) { | |
| const newX = x + col; | |
| const newY = y + row; | |
| // Check boundaries | |
| if (newX < 0 || newX >= boardWidth || newY >= boardHeight) { | |
| return true; | |
| } | |
| // Don't check above the board | |
| if (newY < 0) { | |
| continue; | |
| } | |
| // Check for existing blocks | |
| if (grid[newY] && grid[newY][newX] !== 0) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| // Lock the current piece in place | |
| function lockPiece() { | |
| for (let row = 0; row < currentPiece.shape.length; row++) { | |
| for (let col = 0; col < currentPiece.shape[row].length; col++) { | |
| if (currentPiece.shape[row][col] !== 0) { | |
| const y = currentPiece.y + row; | |
| const x = currentPiece.x + col; | |
| // Only lock if within bounds | |
| if (y >= 0 && x >= 0 && x < boardWidth && y < boardHeight) { | |
| grid[y][x] = currentPiece.color; | |
| } | |
| } | |
| } | |
| } | |
| // Create explosion effect | |
| createExplosion(currentPiece.x, currentPiece.y, currentPiece.color); | |
| } | |
| // Check for completed lines | |
| function checkLines() { | |
| let linesCleared = 0; | |
| for (let y = boardHeight - 1; y >= 0; y--) { | |
| let lineComplete = true; | |
| for (let x = 0; x < boardWidth; x++) { | |
| if (grid[y][x] === 0) { | |
| lineComplete = false; | |
| break; | |
| } | |
| } | |
| if (lineComplete) { | |
| // Remove the line | |
| for (let yy = y; yy > 0; yy--) { | |
| grid[yy] = [...grid[yy - 1]]; | |
| } | |
| // Add empty line at top | |
| grid[0] = Array(boardWidth).fill(0); | |
| linesCleared++; | |
| y++; // Check the same row again (now with the line above) | |
| } | |
| } | |
| if (linesCleared > 0) { | |
| lines += linesCleared; | |
| // Check for level up (every 10 lines) | |
| const newLevel = Math.floor(lines / 10) + 1; | |
| if (newLevel > level) { | |
| level = newLevel; | |
| gameSpeed = Math.max(100, 1000 - (level - 1) * 100); | |
| // Change gravity direction every 3 levels | |
| if (level % 3 === 0) { | |
| const directions = ['down', 'left', 'right', 'up']; | |
| gravityDirection = directions[(level / 3 - 1) % directions.length]; | |
| } | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(update, gameSpeed); | |
| } | |
| } | |
| return linesCleared; | |
| } | |
| // Update the score | |
| function updateScore(linesCleared) { | |
| const points = [0, 40, 100, 300, 1200]; // Points for 0, 1, 2, 3, 4 lines | |
| let multiplier = 1; | |
| // Check for score multiplier power-up | |
| const scoreBoost = activePowerUps.find(p => p.type === 'score'); | |
| if (scoreBoost) { | |
| multiplier = 2; | |
| } | |
| score += points[Math.min(linesCleared, 4)] * level * multiplier; | |
| // Update UI | |
| document.getElementById('score').textContent = score; | |
| document.getElementById('level').textContent = level; | |
| document.getElementById('lines').textContent = lines; | |
| } | |
| // Game over | |
| function gameOver() { | |
| isGameOver = true; | |
| clearInterval(gameInterval); | |
| // Show game over screen | |
| document.getElementById('game-over').classList.remove('hidden'); | |
| document.getElementById('final-score').textContent = score; | |
| // Add to high scores if score is high enough | |
| addHighScore(score); | |
| } | |
| // Draw the game state | |
| function draw() { | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid background | |
| ctx.fillStyle = 'rgba(30, 30, 30, 0.5)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw grid lines | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; | |
| ctx.lineWidth = 0.5; | |
| for (let x = 0; x <= boardWidth; x++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x * cellSize, 0); | |
| ctx.lineTo(x * cellSize, boardHeight * cellSize); | |
| ctx.stroke(); | |
| } | |
| for (let y = 0; y <= boardHeight; y++) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y * cellSize); | |
| ctx.lineTo(boardWidth * cellSize, y * cellSize); | |
| ctx.stroke(); | |
| } | |
| // Draw locked pieces | |
| for (let y = 0; y < boardHeight; y++) { | |
| for (let x = 0; x < boardWidth; x++) { | |
| if (grid[y][x] !== 0) { | |
| drawBlock(x, y, grid[y][x]); | |
| } | |
| } | |
| } | |
| // Draw current piece | |
| if (currentPiece) { | |
| for (let row = 0; row < currentPiece.shape.length; row++) { | |
| for (let col = 0; col < currentPiece.shape[row].length; col++) { | |
| if (currentPiece.shape[row][col] !== 0) { | |
| const x = currentPiece.x + col; | |
| const y = currentPiece.y + row; | |
| // Only draw if within visible area | |
| if (y >= 0) { | |
| drawBlock(x, y, currentPiece.color, true); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // Draw next piece preview | |
| drawNextPiece(); | |
| } | |
| // Draw a single block | |
| function drawBlock(x, y, color, isCurrent = false) { | |
| const padding = isCurrent ? 0 : 2; | |
| const blockX = x * cellSize + padding; | |
| const blockY = y * cellSize + padding; | |
| const blockSize = cellSize - padding * 2; | |
| // Draw block | |
| ctx.fillStyle = color; | |
| ctx.fillRect(blockX, blockY, blockSize, blockSize); | |
| // Add highlight effect | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; | |
| ctx.fillRect(blockX, blockY, blockSize, blockSize / 3); | |
| // Add shadow effect | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; | |
| ctx.fillRect(blockX, blockY + blockSize * 2/3, blockSize, blockSize / 3); | |
| // Add border | |
| ctx.strokeStyle = isCurrent ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.3)'; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(blockX, blockY, blockSize, blockSize); | |
| // Add pattern if colorblind mode | |
| if (colorblindMode) { | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; | |
| // Different patterns based on color | |
| if (color === COLORS[0] || color === COLORS[7]) { // Red or Pink | |
| // Diagonal lines | |
| ctx.beginPath(); | |
| ctx.moveTo(blockX, blockY); | |
| ctx.lineTo(blockX + blockSize, blockY + blockSize); | |
| ctx.moveTo(blockX + blockSize, blockY); | |
| ctx.lineTo(blockX, blockY + blockSize); | |
| ctx.stroke(); | |
| } else if (color === COLORS[1] || color === COLORS[4]) { // Green or Purple | |
| // Dots | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| ctx.beginPath(); | |
| ctx.arc( | |
| blockX + blockSize / 4 + i * blockSize / 3, | |
| blockY + blockSize / 4 + j * blockSize / 3, | |
| blockSize / 10, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| } | |
| } | |
| } else if (color === COLORS[2] || color === COLORS[5]) { // Blue or Cyan | |
| // Horizontal lines | |
| for (let i = 0; i < 3; i++) { | |
| ctx.fillRect( | |
| blockX, | |
| blockY + i * blockSize / 3, | |
| blockSize, | |
| blockSize / 10 | |
| ); | |
| } | |
| } else { // Yellow or Orange | |
| // Vertical lines | |
| for (let i = 0; i < 3; i++) { | |
| ctx.fillRect( | |
| blockX + i * blockSize / 3, | |
| blockY, | |
| blockSize / 10, | |
| blockSize | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| // Draw the next piece preview | |
| function drawNextPiece() { | |
| // Clear canvas | |
| nextCtx.clearRect(0, 0, nextCanvas.width, nextCanvas.height); | |
| // Draw background | |
| nextCtx.fillStyle = 'rgba(30, 30, 30, 0.5)'; | |
| nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height); | |
| if (!nextPiece) return; | |
| // Calculate center position | |
| const centerX = nextCanvas.width / 2 - (nextPiece.shape[0].length * cellSize / 2) / 2; | |
| const centerY = nextCanvas.height / 2 - (nextPiece.shape.length * cellSize / 2) / 2; | |
| // Draw each block of the next piece | |
| for (let row = 0; row < nextPiece.shape.length; row++) { | |
| for (let col = 0; col < nextPiece.shape[row].length; col++) { | |
| if (nextPiece.shape[row][col] !== 0) { | |
| const x = centerX + col * cellSize / 2; | |
| const y = centerY + row * cellSize / 2; | |
| nextCtx.fillStyle = nextPiece.color; | |
| nextCtx.fillRect(x, y, cellSize / 2, cellSize / 2); | |
| // Add highlight effect | |
| nextCtx.fillStyle = 'rgba(255, 255, 255, 0.2)'; | |
| nextCtx.fillRect(x, y, cellSize / 2, cellSize / 6); | |
| // Add shadow effect | |
| nextCtx.fillStyle = 'rgba(0, 0, 0, 0.2)'; | |
| nextCtx.fillRect(x, y + cellSize / 3, cellSize / 2, cellSize / 6); | |
| // Add border | |
| nextCtx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; | |
| nextCtx.lineWidth = 1; | |
| nextCtx.strokeRect(x, y, cellSize / 2, cellSize / 2); | |
| } | |
| } | |
| } | |
| } | |
| // Create explosion effect | |
| function createExplosion(x, y, color) { | |
| const particles = []; | |
| const particleCount = 20; | |
| for (let i = 0; i < particleCount; i++) { | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 1 + Math.random() * 3; | |
| particles.push({ | |
| x: (x + 0.5) * cellSize, | |
| y: (y + 0.5) * cellSize, | |
| vx: Math.cos(angle) * speed, | |
| vy: Math.sin(angle) * speed, | |
| size: 2 + Math.random() * 4, | |
| color: color, | |
| life: 30 + Math.random() * 30 | |
| }); | |
| } | |
| // Animate particles | |
| const animateParticles = () => { | |
| const container = document.getElementById('particles-container'); | |
| container.innerHTML = ''; | |
| for (let i = 0; i < particles.length; i++) { | |
| const p = particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.life--; | |
| if (p.life > 0) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle'; | |
| particle.style.width = `${p.size}px`; | |
| particle.style.height = `${p.size}px`; | |
| particle.style.backgroundColor = p.color; | |
| particle.style.left = `${p.x}px`; | |
| particle.style.top = `${p.y}px`; | |
| particle.style.opacity = p.life / 60; | |
| particle.style.setProperty('--tx', `${p.vx * 10}px`); | |
| particle.style.setProperty('--ty', `${p.vy * 10}px`); | |
| container.appendChild(particle); | |
| } | |
| } | |
| // Remove dead particles | |
| for (let i = particles.length - 1; i >= 0; i--) { | |
| if (particles[i].life <= 0) { | |
| particles.splice(i, 1); | |
| } | |
| } | |
| // Continue animation if particles remain | |
| if (particles.length > 0) { | |
| requestAnimationFrame(animateParticles); | |
| } | |
| }; | |
| animateParticles(); | |
| } | |
| // Create background particles | |
| function createBackgroundParticles() { | |
| const container = document.getElementById('particles-container'); | |
| const particleCount = 30; | |
| for (let i = 0; i < particleCount; i++) { | |
| const particle = document.createElement('div'); | |
| particle.className = 'particle absolute rounded-full'; | |
| // Random properties | |
| const size = 1 + Math.random() * 3; | |
| const x = Math.random() * 100; | |
| const y = Math.random() * 100; | |
| const duration = 10 + Math.random() * 20; | |
| const delay = Math.random() * 20; | |
| const color = COLORS[Math.floor(Math.random() * COLORS.length)]; | |
| particle.style.width = `${size}px`; | |
| particle.style.height = `${size}px`; | |
| particle.style.left = `${x}%`; | |
| particle.style.top = `${y}%`; | |
| particle.style.backgroundColor = color; | |
| particle.style.opacity = 0.3 + Math.random() * 0.3; | |
| particle.style.animation = `pulse ${duration}s ${delay}s infinite`; | |
| container.appendChild(particle); | |
| } | |
| } | |
| // Add a random power-up | |
| function addRandomPowerUp() { | |
| const powerUpTypes = Object.keys(POWER_UPS); | |
| const randomType = powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)]; | |
| activePowerUps.push({ | |
| type: randomType, | |
| duration: 15000, // 15 seconds | |
| startTime: Date.now() | |
| }); | |
| updatePowerUpsDisplay(); | |
| // Special effects for certain power-ups | |
| if (randomType === 'gravity') { | |
| gravityDirection = ['down', 'left', 'right', 'up'][Math.floor(Math.random() * 4)]; | |
| } else if (randomType === 'slow') { | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(update, gameSpeed * 2); | |
| } else if (randomType === 'score') { | |
| // Multiplier already handled in score calculation | |
| } | |
| } | |
| // Update active power-ups | |
| function updatePowerUps() { | |
| const now = Date.now(); | |
| for (let i = activePowerUps.length - 1; i >= 0; i--) { | |
| const powerUp = activePowerUps[i]; | |
| if (now - powerUp.startTime >= powerUp.duration) { | |
| // Power-up expired | |
| if (powerUp.type === 'slow') { | |
| clearInterval(gameInterval); | |
| gameInterval = setInterval(update, gameSpeed); | |
| } else if (powerUp.type === 'gravity') { | |
| gravityDirection = 'down'; | |
| } | |
| activePowerUps.splice(i, 1); | |
| } | |
| } | |
| if (activePowerUps.length > 0) { | |
| updatePowerUpsDisplay(); | |
| } | |
| } | |
| // Update power-ups display | |
| function updatePowerUpsDisplay() { | |
| const container = document.getElementById('power-ups'); | |
| container.innerHTML = ''; | |
| activePowerUps.forEach(powerUp => { | |
| const powerUpEl = document.createElement('div'); | |
| powerUpEl.className = 'flex flex-col items-center'; | |
| powerUpEl.title = `${POWER_UPS[powerUp.type].name}: ${POWER_UPS[powerUp.type].effect}`; | |
| const icon = document.createElement('div'); | |
| icon.className = 'w-8 h-8 rounded-full flex items-center justify-center text-white font-bold'; | |
| icon.style.backgroundColor = POWER_UPS[powerUp.type].color; | |
| icon.textContent = powerUp.type[0].toUpperCase(); | |
| const progress = document.createElement('div'); | |
| progress.className = 'w-full h-1 mt-1 bg-gray-700 rounded-full overflow-hidden'; | |
| const progressBar = document.createElement('div'); | |
| progressBar.className = 'h-full'; | |
| progressBar.style.backgroundColor = POWER_UPS[powerUp.type].color; | |
| progressBar.style.width = `${100 - ((Date.now() - powerUp.startTime) / powerUp.duration * 100)}%`; | |
| progress.appendChild(progressBar); | |
| powerUpEl.appendChild(icon); | |
| powerUpEl.appendChild(progress); | |
| container.appendChild(powerUpEl); | |
| }); | |
| } | |
| // Handle keyboard input | |
| function handleKeyDown(e) { | |
| if (isGameOver) return; | |
| switch (e.key) { | |
| case 'ArrowLeft': | |
| movePiece(-1, 0); | |
| break; | |
| case 'ArrowRight': | |
| movePiece(1, 0); | |
| break; | |
| case 'ArrowDown': | |
| movePiece(0, 1); | |
| break; | |
| case 'ArrowUp': | |
| rotatePiece(); | |
| break; | |
| case ' ': | |
| // Hard drop | |
| while (movePiece(0, 1)) {} | |
| lockPiece(); | |
| const linesCleared = checkLines(); | |
| if (linesCleared > 0) { | |
| updateScore(linesCleared); | |
| } | |
| currentPiece = nextPiece; | |
| nextPiece = createPiece(); | |
| if (checkCollision(currentPiece.x, currentPiece.y, currentPiece.shape)) { | |
| gameOver(); | |
| } | |
| break; | |
| case 'p': | |
| case 'P': | |
| togglePause(); | |
| break; | |
| case 'Escape': | |
| if (isPaused) { | |
| resumeGame(); | |
| } else { | |
| pauseGame(); | |
| } | |
| break; | |
| } | |
| // Prevent default for game controls | |
| if (['ArrowLeft', 'ArrowRight', 'ArrowDown', 'ArrowUp', ' '].includes(e.key)) { | |
| e.preventDefault(); | |
| } | |
| } | |
| // Handle touch start | |
| function handleTouchStart(e) { | |
| if (settings.controls !== 'touch' || isGameOver || isPaused) return; | |
| touchStartX = e.touches[0].clientX; | |
| touchStartY = e.touches[0].clientY; | |
| } | |
| // Handle touch move | |
| function handleTouchMove(e) { | |
| if (settings.controls !== 'touch' || isGameOver || isPaused) return; | |
| e.preventDefault(); | |
| touchEndX = e.touches[0].clientX; | |
| touchEndY = e.touches[0].clientY; | |
| } | |
| // Handle touch end | |
| function handleTouchEnd(e) { | |
| if (settings.controls !== 'touch' || isGameOver || isPaused) return; | |
| const dx = touchEndX - touchStartX; | |
| const dy = touchEndY - touchStartY; | |
| // Determine swipe direction | |
| if (Math.abs(dx) > Math.abs(dy)) { | |
| // Horizontal swipe | |
| if (dx > 30) { | |
| movePiece(1, 0); // Right | |
| } else if (dx < -30) { | |
| movePiece(-1, 0); // Left | |
| } | |
| } else { | |
| // Vertical swipe | |
| if (dy > 30) { | |
| movePiece(0, 1); // Down | |
| } else if (dy < -30) { | |
| rotatePiece(); // Up for rotate | |
| } | |
| } | |
| // Reset touch positions | |
| touchStartX = touchStartY = touchEndX = touchEndY = 0; | |
| } | |
| // Toggle pause | |
| function togglePause() { | |
| if (isPaused) { | |
| resumeGame(); | |
| } else { | |
| pauseGame(); | |
| } | |
| } | |
| // Pause the game | |
| function pauseGame() { | |
| if (isGameOver) return; | |
| isPaused = true; | |
| clearInterval(gameInterval); | |
| document.getElementById('pause-screen').classList.remove('hidden'); | |
| } | |
| // Resume the game | |
| function resumeGame() { | |
| if (isGameOver) return; | |
| isPaused = false; | |
| lastUpdateTime = Date.now(); | |
| gameInterval = setInterval(update, gameSpeed); | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| } | |
| // Quit game and return to menu | |
| function quitGame() { | |
| clearInterval(gameInterval); | |
| isPaused = false; | |
| gameStarted = false; | |
| document.getElementById('pause-screen').classList.add('hidden'); | |
| document.getElementById('game-ui').classList.add('hidden'); | |
| showMainMenu(); | |
| } | |
| // Show main menu | |
| function showMainMenu() { | |
| document.getElementById('main-menu').classList.remove('hidden'); | |
| document.getElementById('high-scores-menu').classList.add('hidden'); | |
| document.getElementById('settings-menu').classList.add('hidden'); | |
| document.getElementById('game-ui').classList.add('hidden'); | |
| } | |
| // Show high scores | |
| function showHighScores() { | |
| document.getElementById('main-menu').classList.add('hidden'); | |
| document.getElementById('high-scores-menu').classList.remove('hidden'); | |
| // Populate scores | |
| const scoresList = document.getElementById('scores-list'); | |
| scoresList.innerHTML = ''; | |
| if (highScores.length === 0) { | |
| scoresList.innerHTML = '<p class="text-center text-gray-400">No scores yet</p>'; | |
| return; | |
| } | |
| highScores.sort((a, b) => b - a).slice(0, 10).forEach((score, index) => { | |
| const scoreEl = document.createElement('div'); | |
| scoreEl.className = 'flex justify-between items-center py-2 border-b border-gray-800'; | |
| const rankEl = document.createElement('span'); | |
| rankEl.className = 'font-bold text-yellow-400'; | |
| rankEl.textContent = `${index + 1}.`; | |
| const scoreValueEl = document.createElement('span'); | |
| scoreValueEl.className = 'font-mono'; | |
| scoreValueEl.textContent = score.toLocaleString(); | |
| scoreEl.appendChild(rankEl); | |
| scoreEl.appendChild(scoreValueEl); | |
| scoresList.appendChild(scoreEl); | |
| }); | |
| } | |
| // Show settings | |
| function showSettings() { | |
| document.getElementById('main-menu').classList.add('hidden'); | |
| document.getElementById('settings-menu').classList.remove('hidden'); | |
| // Set current settings in form | |
| document.getElementById('control-type').value = settings.controls; | |
| document.getElementById('difficulty').value = settings.difficulty; | |
| document.getElementById('colorblind-mode').checked = settings.colorblind; | |
| document.getElementById('volume').value = settings.volume; | |
| } | |
| // Save settings | |
| function saveSettings() { | |
| settings.controls = document.getElementById('control-type').value; | |
| settings.difficulty = document.getElementById('difficulty').value; | |
| settings.colorblind = document.getElementById('colorblind-mode').checked; | |
| settings.volume = parseInt(document.getElementById('volume').value); | |
| // Save to localStorage | |
| localStorage.setItem('blockShiftSettings', JSON.stringify(settings)); | |
| // Update game state | |
| colorblindMode = settings.colorblind; | |
| volume = settings.volume / 100; | |
| // Update UI | |
| updateUIFromSettings(); | |
| // Return to menu | |
| showMainMenu(); | |
| } | |
| // Update UI based on settings | |
| function updateUIFromSettings() { | |
| // Enable/disable continue button based on saved game | |
| const savedGame = localStorage.getItem('blockShiftGame'); | |
| document.getElementById('continue-game').disabled = !savedGame; | |
| // Update colorblind mode | |
| colorblindMode = settings.colorblind; | |
| } | |
| // Add high score | |
| function addHighScore(newScore) { | |
| highScores.push(newScore); | |
| // Keep only top 10 scores | |
| highScores.sort((a, b) => b - a); | |
| if (highScores.length > 10) { | |
| highScores = highScores.slice(0, 10); | |
| } | |
| // Save to localStorage | |
| localStorage.setItem('blockShiftHighScores', JSON.stringify(highScores)); | |
| } | |
| // Load high scores | |
| function loadHighScores() { | |
| const savedScores = localStorage.getItem('blockShiftHighScores'); | |
| if (savedScores) { | |
| highScores = JSON.parse(savedScores); | |
| } else { | |
| highScores = []; | |
| } | |
| } | |
| // Load settings | |
| function loadSettings() { | |
| const savedSettings = localStorage.getItem('blockShiftSettings'); | |
| if (savedSettings) { | |
| settings = JSON.parse(savedSettings); | |
| } | |
| // Apply settings | |
| colorblindMode = settings.colorblind || false; | |
| volume = (settings.volume || 70) / 100; | |
| } | |
| // Initialize the game when the page loads | |
| window.addEventListener('load', init); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Batfly/blockshift-chronicles" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |