|
|
<!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> |