| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Matrix Snake 3D - Enhanced</title> |
| <style> |
| body { |
| margin: 0; |
| overflow: hidden; |
| background-color: #000; |
| color: #0f0; |
| font-family: 'Courier New', Courier, monospace; |
| } |
| canvas { |
| display: block; |
| } |
| .game-ui { |
| position: absolute; |
| padding: 10px; |
| background-color: rgba(0, 20, 0, 0.8); |
| border: 1px solid #0f0; |
| border-radius: 5px; |
| font-size: 1.2em; |
| pointer-events: none; |
| } |
| #info { |
| top: 10px; |
| left: 10px; |
| } |
| #combo { |
| top: 10px; |
| right: 10px; |
| color: #0ff; |
| opacity: 0; |
| transition: opacity 0.3s; |
| } |
| #gameScreen { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| align-items: center; |
| background-color: rgba(0, 10, 0, 0.8); |
| z-index: 10; |
| } |
| #startScreen, #gameOverScreen { |
| padding: 30px; |
| background-color: rgba(0, 30, 0, 0.9); |
| border: 2px solid #0f0; |
| border-radius: 10px; |
| text-align: center; |
| max-width: 500px; |
| } |
| #gameOverScreen { |
| border-color: #f00; |
| } |
| .title { |
| font-size: 2.5em; |
| margin-bottom: 20px; |
| text-shadow: 0 0 10px #0f0; |
| } |
| .subtitle { |
| font-size: 1.2em; |
| margin-bottom: 30px; |
| } |
| .button { |
| display: inline-block; |
| padding: 10px 20px; |
| margin: 10px; |
| background-color: rgba(0, 80, 0, 0.8); |
| border: 1px solid #0f0; |
| border-radius: 5px; |
| color: #0f0; |
| cursor: pointer; |
| transition: all 0.2s; |
| pointer-events: auto; |
| } |
| .button:hover { |
| background-color: rgba(0, 120, 0, 0.9); |
| transform: scale(1.05); |
| } |
| .controls { |
| margin-top: 20px; |
| font-size: 0.9em; |
| opacity: 0.8; |
| } |
| #highScores { |
| margin-top: 20px; |
| text-align: left; |
| width: 100%; |
| } |
| #highScores table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| #highScores th, #highScores td { |
| padding: 5px; |
| border-bottom: 1px solid rgba(0, 255, 0, 0.5); |
| } |
| #touchControls { |
| position: absolute; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| display: none; |
| } |
| .touchBtn { |
| width: 60px; |
| height: 60px; |
| background-color: rgba(0, 50, 0, 0.5); |
| border: 1px solid #0f0; |
| border-radius: 50%; |
| margin: 5px; |
| display: inline-flex; |
| justify-content: center; |
| align-items: center; |
| font-size: 20px; |
| cursor: pointer; |
| pointer-events: auto; |
| } |
| |
| #matrixCanvas { |
| position: fixed; |
| top: 0; |
| left: 0; |
| z-index: -1; |
| } |
| </style> |
| </head> |
| <body> |
| |
| <canvas id="matrixCanvas"></canvas> |
| |
| |
| <canvas id="gameCanvas"></canvas> |
| |
| |
| <div id="info" class="game-ui">Score: 0 | High: 0</div> |
| <div id="combo" class="game-ui">Combo x1!</div> |
| |
| |
| <div id="touchControls"> |
| <div class="touchBtn" id="upBtn">↑</div> |
| <div style="display: flex;"> |
| <div class="touchBtn" id="leftBtn">←</div> |
| <div class="touchBtn" id="downBtn">↓</div> |
| <div class="touchBtn" id="rightBtn">→</div> |
| </div> |
| </div> |
| |
| |
| <div id="gameScreen"> |
| <div id="startScreen"> |
| <div class="title">MATRIX SNAKE 3D</div> |
| <div class="subtitle">Navigate the digital realm. Collect data packets. Avoid system firewalls.</div> |
| <div class="button" id="startBtn">START GAME</div> |
| <div class="button" id="difficultyBtn">DIFFICULTY: NORMAL</div> |
| <div class="controls"> |
| Use Arrow Keys to change direction<br> |
| Press P to pause the game |
| </div> |
| <div id="highScores"> |
| <h3>HIGH SCORES</h3> |
| <table id="scoresTable"> |
| <tr><th>RANK</th><th>SCORE</th><th>DIFFICULTY</th></tr> |
| </table> |
| </div> |
| </div> |
| <div id="gameOverScreen" style="display: none;"> |
| <div class="title" style="color: #f00;">SYSTEM FAILURE</div> |
| <div id="finalScore" class="subtitle">Final Score: 0</div> |
| <div class="button" id="restartBtn">RESTART</div> |
| <div class="button" id="menuBtn">MAIN MENU</div> |
| </div> |
| <div id="pauseScreen" style="display: none;"> |
| <div class="title">PAUSED</div> |
| <div class="subtitle">Press P to resume</div> |
| <div class="button" id="resumeBtn">RESUME</div> |
| <div class="button" id="quitBtn">QUIT</div> |
| </div> |
| </div> |
|
|
| |
| <audio id="eatSound" preload="auto"> |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAANIAqJWUEQAFO+gRc5TRJIkiRJEiL///////////8RERERERERVVVVVVVVVVVVVVJEREREREVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/jGMQJA/Aa1flBABBTpGX9hDGMYxw7/+MMYxd/4wxIiI9////jDEQ7/jdEiJERERBaIiIzMzMzIiIiP//MzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM/+MYxB4AAANIAAAAADMzMzMzMzMzMzMzMzMzMzMzM" type="audio/mpeg"> |
| </audio> |
| <audio id="gameOverSound" preload="auto"> |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAKMFqZVQEwAhGKzc+FSIiIiIiIiIj4+Pj4+Pj4+Pj4+JIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jIMQNAAAP8AEAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIv/jEMQQAAAP8AAAAAI+IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg==" type="audio/mpeg"> |
| </audio> |
| <audio id="bgMusic" loop preload="auto"> |
| <source src="data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAFgAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAWMBaq2QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAJcAKRWQEQAFNfQRc5znOc5znP/////////uc5znOc5znOc5znEREREREREREREMYxjGMY/+MYxBEJkFahX4wwAjGMYxjGMYxjGMYxERERERERESIiIiL//////////////+MYxBQG4AqlX8MQAu/////////////////////jIMQVBVwCqVfwBAC/////////////////" type="audio/mpeg"> |
| </audio> |
|
|
|
|
| // |
| |
| |
| |
| |
| |
| |
| |
|
|
| <script type="module"> |
| import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js'; |
| import { EffectComposer } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js'; |
| import { RenderPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/RenderPass.js'; |
| import { UnrealBloomPass } from 'https://unpkg.com/three@0.160.0/examples/jsm/postprocessing/UnrealBloomPass.js'; |
| |
| |
| const CONFIG = { |
| GRID_SIZE: 25, |
| CELL_SIZE: 1, |
| BASE_SPEED: 150, |
| DIFFICULTY_LEVELS: { |
| 'EASY': { speedMultiplier: 1.3, obstacleMultiplier: 0.5 }, |
| 'NORMAL': { speedMultiplier: 1.0, obstacleMultiplier: 1.0 }, |
| 'HARD': { speedMultiplier: 0.7, obstacleMultiplier: 1.5 } |
| }, |
| MAX_OBSTACLE_COUNT: 10, |
| FOOD_TYPES: [ |
| { type: 'regular', color: 0x00ff00, points: 1, speedEffect: 0 }, |
| { type: 'special', color: 0x00ffff, points: 5, speedEffect: -10 }, |
| { type: 'rare', color: 0xff00ff, points: 10, speedEffect: 10 } |
| ], |
| COMBO_TIMEOUT: 5000, |
| HIGH_SCORES_COUNT: 5 |
| }; |
| |
| |
| class ParticleSystem { |
| constructor(scene) { |
| this.scene = scene; |
| this.particles = []; |
| |
| |
| this.geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2); |
| } |
| |
| createFoodEffect(position, color) { |
| const count = 20; |
| |
| for (let i = 0; i < count; i++) { |
| |
| const material = new THREE.MeshBasicMaterial({ |
| color: color || 0x00ff00, |
| transparent: true, |
| opacity: 0.9 |
| }); |
| |
| const particle = new THREE.Mesh(this.geometry, material); |
| |
| |
| particle.position.copy(position); |
| |
| |
| const velocity = new THREE.Vector3( |
| (Math.random() - 0.5) * 0.1, |
| (Math.random()) * 0.1, |
| (Math.random() - 0.5) * 0.1 |
| ); |
| |
| |
| this.scene.add(particle); |
| |
| |
| this.particles.push({ |
| mesh: particle, |
| velocity: velocity, |
| life: 1.0, |
| decay: 0.02 + Math.random() * 0.03 |
| }); |
| } |
| } |
| |
| update() { |
| |
| for (let i = this.particles.length - 1; i >= 0; i--) { |
| const particle = this.particles[i]; |
| |
| |
| particle.mesh.position.add(particle.velocity); |
| |
| |
| particle.velocity.y -= 0.003; |
| |
| |
| particle.life -= particle.decay; |
| |
| |
| particle.mesh.material.opacity = particle.life; |
| |
| |
| if (particle.life <= 0) { |
| this.scene.remove(particle.mesh); |
| particle.mesh.material.dispose(); |
| this.particles.splice(i, 1); |
| } |
| } |
| } |
| |
| clear() { |
| |
| for (const particle of this.particles) { |
| this.scene.remove(particle.mesh); |
| particle.mesh.material.dispose(); |
| particle.mesh.geometry.dispose(); |
| } |
| this.particles = []; |
| } |
| } |
| |
| |
| const GameState = { |
| MENU: 'menu', |
| PLAYING: 'playing', |
| PAUSED: 'paused', |
| GAME_OVER: 'gameOver', |
| currentState: 'menu', |
| |
| changeState(newState) { |
| this.currentState = newState; |
| |
| |
| switch(newState) { |
| case this.MENU: |
| document.getElementById('gameScreen').style.display = 'flex'; |
| document.getElementById('startScreen').style.display = 'block'; |
| document.getElementById('gameOverScreen').style.display = 'none'; |
| break; |
| case this.PLAYING: |
| document.getElementById('gameScreen').style.display = 'none'; |
| break; |
| case this.PAUSED: |
| document.getElementById('gameScreen').style.display = 'flex'; |
| document.getElementById('startScreen').style.display = 'none'; |
| document.getElementById('gameOverScreen').style.display = 'none'; |
| document.getElementById('pauseScreen').style.display = 'block'; |
| break; |
| case this.GAME_OVER: |
| document.getElementById('gameScreen').style.display = 'flex'; |
| document.getElementById('startScreen').style.display = 'none'; |
| document.getElementById('gameOverScreen').style.display = 'block'; |
| |
| document.getElementById('gameOverSound').play(); |
| break; |
| } |
| } |
| }; |
| |
| |
| class MatrixRain { |
| constructor() { |
| this.canvas = document.getElementById('matrixCanvas'); |
| this.ctx = this.canvas.getContext('2d'); |
| this.resize(); |
| |
| this.fontSize = 14; |
| this.columns = Math.floor(this.canvas.width / this.fontSize); |
| this.drops = []; |
| this.characters = '01アイウエオカキクケコサシスセソタチツテトナニヌネ<>{}[]()+-*/%=#@&?*:・゚✧ ≡ ░▒░▒░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█░▒▓█║│·▓▒░█䷀ ▙⁞ ░▒▓█║│ ·▓▒░█▄▀■■▄▬▌▐ ⁞▏▄▀■■▄▬▌▐ ▄▀■■▄▬▌▐ . ▛ ⁞▏ ▏ ⁚⁝ .'; |
| |
| this.resetDrops(); |
| |
| this.animate = this.animate.bind(this); |
| this.animate(); |
| |
| window.addEventListener('resize', this.handleResize.bind(this)); |
| } |
| |
| handleResize() { |
| this.resize(); |
| this.columns = Math.floor(this.canvas.width / this.fontSize); |
| this.resetDrops(); |
| } |
| |
| resize() { |
| this.canvas.width = window.innerWidth; |
| this.canvas.height = window.innerHeight; |
| } |
| |
| resetDrops() { |
| this.drops = []; |
| for(let i = 0; i < this.columns; i++) { |
| |
| this.drops[i] = Math.floor(Math.random() * -100); |
| } |
| } |
| |
| animate() { |
| if (GameState.currentState === GameState.PLAYING) { |
| |
| this.ctx.fillStyle = 'rgba(0, 0, 0, 0.05)'; |
| this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); |
| |
| this.ctx.fillStyle = '#0f0'; |
| this.ctx.font = this.fontSize + 'px monospace'; |
| |
| for(let i = 0; i < this.drops.length; i++) { |
| |
| const text = this.characters.charAt(Math.floor(Math.random() * this.characters.length)); |
| |
| |
| this.ctx.fillText(text, i * this.fontSize, this.drops[i] * this.fontSize); |
| |
| |
| if(this.drops[i] * this.fontSize > this.canvas.height && Math.random() > 0.975) { |
| this.drops[i] = 0; |
| } |
| this.drops[i]++; |
| } |
| } |
| requestAnimationFrame(this.animate); |
| } |
| } |
| |
| |
| class ObjectPool { |
| constructor(createFunc, initialCount = 10) { |
| this.pool = []; |
| this.createFunc = createFunc; |
| |
| |
| for (let i = 0; i < initialCount; i++) { |
| this.pool.push(this.createFunc()); |
| } |
| } |
| |
| get() { |
| if (this.pool.length > 0) { |
| return this.pool.pop(); |
| } |
| return this.createFunc(); |
| } |
| |
| release(object) { |
| this.pool.push(object); |
| } |
| |
| clear() { |
| this.pool = []; |
| } |
| } |
| |
| |
| class SnakeGame { |
| constructor() { |
| |
| this.scene = null; |
| this.camera = null; |
| this.renderer = null; |
| this.snake = []; |
| this.food = null; |
| this.obstacles = []; |
| this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); |
| this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); |
| this.score = 0; |
| this.highScore = this.loadHighScores()[0]?.score || 0; |
| this.gameSpeed = CONFIG.BASE_SPEED; |
| this.lastUpdateTime = 0; |
| this.isGameOver = false; |
| this.isPaused = false; |
| this.gameLoopId = null; |
| this.bounds = Math.floor(CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE; |
| this.obstacleCount = CONFIG.MAX_OBSTACLE_COUNT; |
| this.comboCount = 0; |
| this.lastFoodTime = 0; |
| this.currentDifficulty = 'NORMAL'; |
| this.particleSystem = null; |
| this.headLight = null; |
| |
| |
| this.materials = { |
| snakeHead: new THREE.MeshStandardMaterial({ |
| color: 0x0000ff, |
| emissive: 0x39FF14, |
| roughness: 0.8, |
| metalness: 0.22 |
| }), |
| snakeBody: new THREE.MeshStandardMaterial({ |
| color: 0x00ff00, |
| emissive: 0x005500, |
| roughness: 0.3, |
| metalness: 0.72 |
| }), |
| food: new THREE.MeshBasicMaterial({ |
| color: 0x00ff00, |
| wireframe: true |
| }), |
| obstacle: new THREE.MeshBasicMaterial({ |
| color: 0x008800, |
| wireframe: true |
| }), |
| specialFood: new THREE.MeshBasicMaterial({ |
| color: 0x00ffff, |
| wireframe: true |
| }), |
| rareFood: new THREE.MeshBasicMaterial({ |
| color: 0xff00ff, |
| wireframe: true |
| }) |
| }; |
| |
| |
| this.geometries = { |
| segment: new THREE.BoxGeometry( |
| CONFIG.CELL_SIZE, |
| CONFIG.CELL_SIZE, |
| CONFIG.CELL_SIZE |
| ), |
| foodBox: new THREE.BoxGeometry( |
| CONFIG.CELL_SIZE * 0.8, |
| CONFIG.CELL_SIZE * 0.8, |
| CONFIG.CELL_SIZE * 0.8 |
| ), |
| foodSphere: new THREE.SphereGeometry( |
| CONFIG.CELL_SIZE * 0.5, |
| 16, |
| 12 |
| ), |
| foodTetrahedron: new THREE.TetrahedronGeometry( |
| CONFIG.CELL_SIZE * 0.6, |
| 0 |
| ), |
| obstacle: new THREE.BoxGeometry( |
| CONFIG.CELL_SIZE, |
| CONFIG.CELL_SIZE * 1.5, |
| CONFIG.CELL_SIZE |
| ) |
| }; |
| |
| |
| this.segmentPool = new ObjectPool(() => { |
| return new THREE.Mesh(this.geometries.segment, this.materials.snakeBody.clone()); |
| }, 20); |
| |
| this.obstaclePool = new ObjectPool(() => { |
| return new THREE.Mesh(this.geometries.obstacle, this.materials.obstacle); |
| }, CONFIG.MAX_OBSTACLE_COUNT * 1.5); |
| |
| |
| this.setupEventListeners(); |
| this.init(); |
| |
| |
| this.matrixRain = new MatrixRain(); |
| |
| |
| this.updateHighScoresTable(); |
| } |
| |
| |
| placeFood() { |
| let foodPos; |
| let validPosition = false; |
| let attempts = 0; |
| const maxAttempts = 100; |
| |
| while (!validPosition && attempts < maxAttempts) { |
| foodPos = new THREE.Vector3( |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE, |
| 0, |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE |
| ); |
| |
| |
| let collisionWithSnake = this.snake.some(segment => |
| segment.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9 |
| ); |
| |
| |
| let collisionWithObstacle = this.obstacles.some(obstacle => |
| obstacle.position.distanceTo(foodPos) < CONFIG.CELL_SIZE * 0.9 |
| ); |
| |
| validPosition = !collisionWithSnake && !collisionWithObstacle; |
| attempts++; |
| } |
| |
| if (validPosition) { |
| this.food.position.copy(foodPos); |
| } else { |
| |
| console.warn("Could not find valid position for food after max attempts"); |
| this.food.position.set(0, 5, 0); |
| } |
| } |
| |
| |
| createObstacles() { |
| |
| for (const obstacle of this.obstacles) { |
| this.scene.remove(obstacle); |
| } |
| this.obstacles = []; |
| |
| |
| for (let i = 0; i < this.obstacleCount; i++) { |
| let obstaclePos; |
| let validPosition = false; |
| let attempts = 0; |
| const maxAttempts = 50; |
| |
| while (!validPosition && attempts < maxAttempts) { |
| obstaclePos = new THREE.Vector3( |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE, |
| 0, |
| Math.floor(Math.random() * CONFIG.GRID_SIZE - CONFIG.GRID_SIZE / 2) * CONFIG.CELL_SIZE |
| ); |
| |
| |
| let tooCloseToStart = obstaclePos.length() < CONFIG.CELL_SIZE * 3; |
| |
| |
| let collisionWithSnake = this.snake.some(segment => |
| segment.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE * 2 |
| ); |
| |
| let collisionWithObstacle = this.obstacles.some(obstacle => |
| obstacle.position.distanceTo(obstaclePos) < CONFIG.CELL_SIZE |
| ); |
| |
| validPosition = !tooCloseToStart && !collisionWithSnake && !collisionWithObstacle; |
| attempts++; |
| } |
| |
| if (validPosition) { |
| const obstacle = new THREE.Mesh(this.geometries.segment, this.materials.obstacle); |
| obstacle.position.copy(obstaclePos); |
| this.obstacles.push(obstacle); |
| this.scene.add(obstacle); |
| } |
| } |
| } |
| |
| |
| clearGameObjects() { |
| |
| for (const segment of this.snake) { |
| this.scene.remove(segment); |
| this.segmentPool.release(segment); |
| } |
| this.snake = []; |
| |
| |
| if (this.food) { |
| this.scene.remove(this.food); |
| this.food = null; |
| } |
| |
| |
| for (const obstacle of this.obstacles) { |
| this.scene.remove(obstacle); |
| } |
| this.obstacles = []; |
| |
| |
| this.particleSystem.clear(); |
| } |
| |
| |
| update(time) { |
| |
| if (this.isPaused || this.isGameOver || GameState.currentState !== GameState.PLAYING) { |
| return; |
| } |
| |
| |
| if (time - this.lastUpdateTime < this.gameSpeed) { |
| return; |
| } |
| this.lastUpdateTime = time; |
| |
| |
| this.direction = this.nextDirection.clone(); |
| |
| |
| const head = this.snake[0]; |
| const newHeadPos = head.position.clone().add( |
| this.direction.clone().multiplyScalar(CONFIG.CELL_SIZE) |
| ); |
| |
| |
| const halfGrid = CONFIG.GRID_SIZE / 2; |
| if ( |
| newHeadPos.x > halfGrid * CONFIG.CELL_SIZE || |
| newHeadPos.x < -halfGrid * CONFIG.CELL_SIZE || |
| newHeadPos.z > halfGrid * CONFIG.CELL_SIZE || |
| newHeadPos.z < -halfGrid * CONFIG.CELL_SIZE |
| ) { |
| this.triggerGameOver(); |
| return; |
| } |
| |
| |
| for (let i = 1; i < this.snake.length; i++) { |
| if (newHeadPos.distanceTo(this.snake[i].position) < CONFIG.CELL_SIZE * 0.25) { |
| this.triggerGameOver(); |
| return; |
| } |
| } |
| |
| |
| for (const obstacle of this.obstacles) { |
| if (newHeadPos.distanceTo(obstacle.position) < CONFIG.CELL_SIZE * 0.5) { |
| this.triggerGameOver(); |
| return; |
| } |
| } |
| |
| |
| const newHead = this.segmentPool.get(); |
| newHead.position.copy(newHeadPos); |
| this.snake.unshift(newHead); |
| this.scene.add(newHead); |
| |
| |
| if (this.food && newHeadPos.distanceTo(this.food.position) < CONFIG.CELL_SIZE * 0.5) { |
| |
| const foodType = this.food.userData; |
| |
| |
| const basePoints = foodType.points || 1; |
| |
| |
| const currentTime = performance.now(); |
| if (currentTime - this.lastFoodTime < CONFIG.COMBO_TIMEOUT) { |
| this.comboCount++; |
| } else { |
| this.comboCount = 1; |
| } |
| this.lastFoodTime = currentTime; |
| |
| |
| const points = basePoints * this.comboCount; |
| this.score += points; |
| |
| |
| if (this.comboCount > 1) { |
| const comboElement = document.getElementById('combo'); |
| comboElement.textContent = `Combo x${this.comboCount}! +${points}`; |
| comboElement.style.opacity = 1; |
| |
| |
| setTimeout(() => { |
| comboElement.style.opacity = 0; |
| }, 2000); |
| } |
| |
| |
| document.getElementById('info').textContent = `Score: ${this.score} | High: ${Math.max(this.score, this.highScore)}`; |
| |
| |
| if (foodType.speedEffect) { |
| this.gameSpeed = Math.max(50, this.gameSpeed - foodType.speedEffect); |
| } |
| |
| |
| document.getElementById('eatSound').play(); |
| |
| |
| this.particleSystem.createFoodEffect(this.food.position.clone(), foodType.color); |
| |
| |
| this.chooseFoodType(); |
| this.placeFood(); |
| } else { |
| |
| const tail = this.snake.pop(); |
| this.scene.remove(tail); |
| this.segmentPool.release(tail); |
| } |
| |
| |
| this.particleSystem.update(); |
| |
| |
| for (let i = 0; i < this.snake.length; i++) { |
| const segment = this.snake[i]; |
| segment.rotation.y = Math.sin(time * 0.001 + i * 0.2) * 0.1; |
| segment.position.y = Math.sin(time * 0.002 + i * 0.1) * 0.2; |
| } |
| |
| |
| if (this.food) { |
| this.food.rotation.y += 0.05; |
| this.food.position.y = Math.sin(time * 0.002) * 0.3; |
| } |
| } |
| |
| |
| chooseFoodType() { |
| |
| const rand = Math.random(); |
| let foodType; |
| |
| if (rand < 0.05) { |
| foodType = CONFIG.FOOD_TYPES[2]; |
| } else if (rand < 0.25) { |
| foodType = CONFIG.FOOD_TYPES[1]; |
| } else { |
| foodType = CONFIG.FOOD_TYPES[0]; |
| } |
| |
| |
| let material; |
| switch(foodType.type) { |
| case 'special': |
| material = this.materials.specialFood; |
| break; |
| case 'rare': |
| material = this.materials.rareFood; |
| break; |
| default: |
| material = this.materials.food; |
| } |
| |
| |
| if (!this.food) { |
| this.food = new THREE.Mesh( |
| this.geometries.segment, |
| material |
| ); |
| this.scene.add(this.food); |
| } else { |
| this.food.material = material; |
| } |
| |
| |
| this.food.userData = foodType; |
| } |
| |
| |
| resetGame() { |
| |
| this.clearGameObjects(); |
| |
| |
| document.getElementById('eatSound').pause(); |
| document.getElementById('gameOverSound').pause(); |
| document.getElementById('bgMusic').pause(); |
| |
| |
| this.direction = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); |
| this.nextDirection = new THREE.Vector3(CONFIG.CELL_SIZE, 0, 0); |
| this.score = 0; |
| this.gameSpeed = CONFIG.BASE_SPEED * CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].speedMultiplier; |
| this.isGameOver = false; |
| this.isPaused = false; |
| this.comboCount = 0; |
| this.lastFoodTime = 0; |
| |
| |
| this.obstacleCount = Math.floor(CONFIG.MAX_OBSTACLE_COUNT * |
| CONFIG.DIFFICULTY_LEVELS[this.currentDifficulty].obstacleMultiplier); |
| |
| |
| const startSegment = this.segmentPool.get(); |
| startSegment.position.set(0, 0, 0); |
| this.snake.push(startSegment); |
| this.scene.add(startSegment); |
| |
| |
| this.chooseFoodType(); |
| this.placeFood(); |
| |
| |
| this.createObstacles(); |
| |
| |
| document.getElementById('info').textContent = `Score: ${this.score} | High: ${this.highScore}`; |
| |
| |
| const music = document.getElementById('bgMusic'); |
| music.volume = 0.3; |
| music.play(); |
| } |
| |
| |
| startGame() { |
| this.resetGame(); |
| GameState.changeState(GameState.PLAYING); |
| this.gameLoop(); |
| } |
| |
| |
| triggerGameOver() { |
| this.isGameOver = true; |
| |
| |
| document.getElementById('finalScore').textContent = `Final Score: ${this.score}`; |
| |
| |
| const highScores = this.loadHighScores(); |
| if (this.score > 0) { |
| |
| highScores.push({ |
| score: this.score, |
| difficulty: this.currentDifficulty, |
| date: new Date().toLocaleDateString() |
| }); |
| |
| |
| highScores.sort((a, b) => b.score - a.score); |
| |
| |
| const topScores = highScores.slice(0, CONFIG.HIGH_SCORES_COUNT); |
| |
| |
| localStorage.setItem('snakeHighScores', JSON.stringify(topScores)); |
| |
| |
| this.highScore = Math.max(this.score, this.highScore); |
| } |
| |
| |
| this.updateHighScoresTable(); |
| |
| |
| document.getElementById('bgMusic').pause(); |
| |
| |
| GameState.changeState(GameState.GAME_OVER); |
| } |
| |
| |
| gameLoop(time) { |
| |
| if (!time) time = 0; |
| |
| |
| this.update(time); |
| |
| |
| this.render(); |
| |
| |
| this.gameLoopId = requestAnimationFrame(this.gameLoop.bind(this)); |
| } |
| |
| |
| render() { |
| this.renderer.render(this.scene, this.camera); |
| } |
| |
| |
| init() { |
| |
| this.scene = new THREE.Scene(); |
| this.scene.background = new THREE.Color(0x000000); |
| |
| |
| this.scene.fog = new THREE.Fog(0x000500, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 2.5); |
| |
| |
| this.camera = new THREE.PerspectiveCamera( |
| 65, window.innerWidth / window.innerHeight, 0.1, 1000 |
| ); |
| this.camera.position.set(0, CONFIG.GRID_SIZE * 0.8, CONFIG.GRID_SIZE * 0.9); |
| this.camera.lookAt(0, -CONFIG.GRID_SIZE * 0.1, 0); |
| |
| |
| this.renderer = new THREE.WebGLRenderer({ |
| canvas: document.getElementById('gameCanvas'), |
| antialias: true, |
| alpha: true |
| }); |
| this.renderer.setSize(window.innerWidth, window.innerHeight); |
| this.renderer.setPixelRatio(window.devicePixelRatio); |
| |
| |
| const gridHelper = new THREE.GridHelper( |
| CONFIG.GRID_SIZE * CONFIG.CELL_SIZE, |
| CONFIG.GRID_SIZE, |
| 0x005500, |
| 0x003300 |
| ); |
| gridHelper.position.y = -CONFIG.CELL_SIZE / 2; |
| this.scene.add(gridHelper); |
| |
| |
| const ambientLight = new THREE.AmbientLight(0x404060); |
| this.scene.add(ambientLight); |
| |
| |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 1); |
| directionalLight.position.set(5, 10, 7); |
| this.scene.add(directionalLight); |
| |
| |
| this.headLight = new THREE.PointLight(0x00ff00, 1, CONFIG.CELL_SIZE * 3); |
| this.scene.add(this.headLight); |
| |
| |
| this.particleSystem = new ParticleSystem(this.scene); |
| |
| |
| window.addEventListener('resize', () => { |
| this.camera.aspect = window.innerWidth / window.innerHeight; |
| this.camera.updateProjectionMatrix(); |
| this.renderer.setSize(window.innerWidth, window.innerHeight); |
| }); |
| } |
| |
| |
| setupEventListeners() { |
| |
| document.addEventListener('keydown', this.handleKeyDown.bind(this)); |
| |
| |
| const touchControls = document.getElementById('touchControls'); |
| const preventDefault = (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
| }; |
| |
| document.getElementById('upBtn').addEventListener('touchstart', (e) => { |
| preventDefault(e); |
| this.handleDirectionChange(0, 0, -1); |
| }); |
| document.getElementById('downBtn').addEventListener('touchstart', (e) => { |
| preventDefault(e); |
| this.handleDirectionChange(0, 0, 1); |
| }); |
| document.getElementById('leftBtn').addEventListener('touchstart', (e) => { |
| preventDefault(e); |
| this.handleDirectionChange(-1, 0, 0); |
| }); |
| document.getElementById('rightBtn').addEventListener('touchstart', (e) => { |
| preventDefault(e); |
| this.handleDirectionChange(1, 0, 0); |
| }); |
| |
| |
| document.getElementById('gameCanvas').addEventListener('touchstart', preventDefault, { passive: false }); |
| document.getElementById('gameCanvas').addEventListener('touchmove', preventDefault, { passive: false }); |
| |
| |
| if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { |
| touchControls.style.display = 'block'; |
| } |
| |
| |
| document.getElementById('startBtn').addEventListener('click', () => { |
| this.startGame(); |
| }); |
| |
| document.getElementById('restartBtn').addEventListener('click', () => { |
| this.startGame(); |
| }); |
| |
| document.getElementById('menuBtn').addEventListener('click', () => { |
| GameState.changeState(GameState.MENU); |
| }); |
| |
| document.getElementById('difficultyBtn').addEventListener('click', () => { |
| this.cycleDifficulty(); |
| }); |
| |
| |
| document.getElementById('resumeBtn').addEventListener('click', () => { |
| this.togglePause(); |
| }); |
| |
| document.getElementById('quitBtn').addEventListener('click', () => { |
| GameState.changeState(GameState.MENU); |
| }); |
| } |
| |
| |
| cycleDifficulty() { |
| const difficulties = Object.keys(CONFIG.DIFFICULTY_LEVELS); |
| const currentIndex = difficulties.indexOf(this.currentDifficulty); |
| const nextIndex = (currentIndex + 1) % difficulties.length; |
| this.currentDifficulty = difficulties[nextIndex]; |
| |
| document.getElementById('difficultyBtn').textContent = `DIFFICULTY: ${this.currentDifficulty}`; |
| } |
| |
| |
| handleKeyDown(event) { |
| if (GameState.currentState === GameState.PLAYING) { |
| switch(event.key) { |
| case 'ArrowUp': |
| this.handleDirectionChange(0, 0, -1); |
| event.preventDefault(); |
| break; |
| case 'ArrowDown': |
| this.handleDirectionChange(0, 0, 1); |
| event.preventDefault(); |
| break; |
| case 'ArrowLeft': |
| this.handleDirectionChange(-1, 0, 0); |
| event.preventDefault(); |
| break; |
| case 'ArrowRight': |
| this.handleDirectionChange(1, 0, 0); |
| event.preventDefault(); |
| break; |
| case 'p': |
| case 'P': |
| this.togglePause(); |
| event.preventDefault(); |
| break; |
| } |
| } else if (GameState.currentState === GameState.GAME_OVER || |
| GameState.currentState === GameState.MENU) { |
| if (event.key === 'Enter') { |
| this.startGame(); |
| event.preventDefault(); |
| } |
| } |
| } |
| |
| |
| handleDirectionChange(x, y, z) { |
| const newDirection = new THREE.Vector3(x, y, z).normalize().multiplyScalar(CONFIG.CELL_SIZE); |
| |
| |
| if (this.direction.dot(newDirection) === -CONFIG.CELL_SIZE * CONFIG.CELL_SIZE) { |
| return; |
| } |
| |
| this.nextDirection = newDirection; |
| } |
| |
| |
| togglePause() { |
| this.isPaused = !this.isPaused; |
| |
| if (this.isPaused) { |
| |
| document.getElementById('bgMusic').pause(); |
| } else { |
| document.getElementById('bgMusic').play(); |
| } |
| } |
| |
| |
| loadHighScores() { |
| const scores = localStorage.getItem('snakeHighScores'); |
| return scores ? JSON.parse(scores) : []; |
| } |
| |
| |
| updateHighScoresTable() { |
| const highScores = this.loadHighScores(); |
| const table = document.getElementById('scoresTable'); |
| |
| |
| while (table.rows.length > 1) { |
| table.deleteRow(1); |
| } |
| |
| |
| for (let i = 0; i < highScores.length; i++) { |
| const row = table.insertRow(-1); |
| |
| const rankCell = row.insertCell(0); |
| rankCell.textContent = i + 1; |
| |
| const scoreCell = row.insertCell(1); |
| scoreCell.textContent = highScores[i].score; |
| |
| const difficultyCell = row.insertCell(2); |
| difficultyCell.textContent = highScores[i].difficulty; |
| } |
| } |
| } |
| |
| |
| const game = new SnakeGame(); |
| |
| |
| window.addEventListener('load', () => { |
| GameState.changeState(GameState.MENU); |
| }); |
| </script> |
| </body> |
| </html> |