Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>НЕОНОВАЯ ВОЙНА: CYBER DEFENSE</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --neon-blue: #00f3ff; | |
| --neon-pink: #ff00ff; | |
| --neon-green: #0aff00; | |
| --neon-yellow: #ffea00; | |
| --bg-color: #050505; | |
| --glass-bg: rgba(255, 255, 255, 0.05); | |
| --glass-border: rgba(255, 255, 255, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| user-select: none; | |
| } | |
| body { | |
| background-color: var(--bg-color); | |
| color: white; | |
| font-family: 'Rajdhani', sans-serif; | |
| overflow: hidden; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* UI Overlay */ | |
| #ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; /* Let clicks pass through to canvas */ | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| padding: 20px; | |
| z-index: 10; | |
| } | |
| /* HUD */ | |
| .hud-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .score-box { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid var(--glass-border); | |
| padding: 10px 25px; | |
| border-radius: 50px; | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.5rem; | |
| text-shadow: 0 0 10px var(--neon-blue); | |
| box-shadow: 0 0 20px rgba(0, 243, 255, 0.2); | |
| transition: transform 0.1s; | |
| } | |
| .health-container { | |
| width: 200px; | |
| height: 10px; | |
| background: rgba(255,255,255,0.1); | |
| border-radius: 5px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .health-bar { | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--neon-green), var(--neon-blue)); | |
| box-shadow: 0 0 10px var(--neon-green); | |
| transition: width 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| /* Menus */ | |
| .menu-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.85); | |
| backdrop-filter: blur(5px); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| pointer-events: auto; | |
| z-index: 20; | |
| transition: opacity 0.5s ease; | |
| } | |
| .hidden { | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| h1 { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 4rem; | |
| text-transform: uppercase; | |
| background: linear-gradient(to right, var(--neon-blue), var(--neon-pink)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-shadow: 0 0 30px rgba(0, 243, 255, 0.5); | |
| margin-bottom: 10px; | |
| text-align: center; | |
| letter-spacing: 5px; | |
| } | |
| p.subtitle { | |
| font-size: 1.2rem; | |
| color: #aaa; | |
| margin-bottom: 40px; | |
| letter-spacing: 2px; | |
| } | |
| .btn { | |
| background: transparent; | |
| color: var(--neon-blue); | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.5rem; | |
| padding: 15px 50px; | |
| border: 2px solid var(--neon-blue); | |
| border-radius: 5px; | |
| cursor: pointer; | |
| text-transform: uppercase; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 0 15px rgba(0, 243, 255, 0.2); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .btn:hover { | |
| background: var(--neon-blue); | |
| color: black; | |
| box-shadow: 0 0 40px rgba(0, 243, 255, 0.8); | |
| transform: scale(1.05); | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent); | |
| transition: 0.5s; | |
| } | |
| .btn:hover::before { | |
| left: 100%; | |
| } | |
| /* Stats in Game Over */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| } | |
| .stat-item h3 { | |
| color: #888; | |
| font-size: 0.9rem; | |
| } | |
| .stat-item span { | |
| font-size: 2rem; | |
| font-weight: bold; | |
| color: white; | |
| } | |
| /* AnyCoder Link */ | |
| .anycoder-link { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 100; | |
| font-family: 'Rajdhani', sans-serif; | |
| font-weight: 700; | |
| font-size: 0.9rem; | |
| color: rgba(255,255,255,0.5); | |
| text-decoration: none; | |
| background: rgba(0,0,0,0.5); | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| transition: color 0.3s; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--neon-pink); | |
| } | |
| /* Mobile Adjustments */ | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.5rem; } | |
| .hud-top { flex-direction: column; gap: 10px; } | |
| .health-container { width: 150px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Canvas Game Layer --> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- UI Layer --> | |
| <div id="ui-layer"> | |
| <div class="hud-top"> | |
| <div class="score-box">СЧЁТ: <span id="scoreEl">0</span></div> | |
| <div class="health-container"> | |
| <div class="health-bar" id="healthEl"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="startScreen" class="menu-screen"> | |
| <h1>НЕОНОВАЯ<br>ВОЙНА</h1> | |
| <p class="subtitle">ЗАЩИТИ ЯДРО ОТ ВИРУСОВ</p> | |
| <button class="btn" id="startBtn">НАЧАТЬ МИССИЮ</button> | |
| <div style="margin-top: 20px; font-size: 0.8rem; color: #666;"> | |
| Управление: Мышь/Тач для прицеливания<br>Клик для стрельбы | |
| </div> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="gameOverScreen" class="menu-screen hidden"> | |
| <h1 style="color: var(--neon-pink); -webkit-text-fill-color: var(--neon-pink);">МИССИЯ ПРОВАЛЕНА</h1> | |
| <div class="stats-grid"> | |
| <div class="stat-item"> | |
| <h3>СЧЁТ</h3> | |
| <span id="finalScore">0</span> | |
| </div> | |
| <div class="stat-item"> | |
| <h3>РЕКОРД</h3> | |
| <span id="highScore">0</span> | |
| </div> | |
| </div> | |
| <button class="btn" id="restartBtn">ПОВТОРИТЬ</button> | |
| </div> | |
| <!-- Footer Link --> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| <script> | |
| /** | |
| * AUDIO SYSTEM (Synthesizer) | |
| * No external files needed. | |
| */ | |
| const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| function playSound(type) { | |
| if (audioCtx.state === 'suspended') audioCtx.resume(); | |
| const osc = audioCtx.createOscillator(); | |
| const gainNode = audioCtx.createGain(); | |
| osc.connect(gainNode); | |
| gainNode.connect(audioCtx.destination); | |
| const now = audioCtx.currentTime; | |
| if (type === 'shoot') { | |
| osc.type = 'square'; | |
| osc.frequency.setValueAtTime(400, now); | |
| osc.frequency.exponentialRampToValueAtTime(100, now + 0.1); | |
| gainNode.gain.setValueAtTime(0.1, now); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.1); | |
| osc.start(now); | |
| osc.stop(now + 0.1); | |
| } else if (type === 'hit') { | |
| osc.type = 'triangle'; | |
| osc.frequency.setValueAtTime(100, now); | |
| osc.frequency.linearRampToValueAtTime(50, now + 0.1); | |
| gainNode.gain.setValueAtTime(0.2, now); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.2); | |
| osc.start(now); | |
| osc.stop(now + 0.2); | |
| } else if (type === 'explosion') { | |
| osc.type = 'sawtooth'; | |
| osc.frequency.setValueAtTime(50, now); | |
| osc.frequency.exponentialRampToValueAtTime(0.01, now + 0.3); | |
| gainNode.gain.setValueAtTime(0.3, now); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3); | |
| osc.start(now); | |
| osc.stop(now + 0.3); | |
| } | |
| } | |
| /** | |
| * GAME ENGINE | |
| */ | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // UI Elements | |
| const scoreEl = document.getElementById('scoreEl'); | |
| const healthEl = document.getElementById('healthEl'); | |
| const startScreen = document.getElementById('startScreen'); | |
| const gameOverScreen = document.getElementById('gameOverScreen'); | |
| const finalScoreEl = document.getElementById('finalScore'); | |
| const highScoreEl = document.getElementById('highScore'); | |
| const startBtn = document.getElementById('startBtn'); | |
| const restartBtn = document.getElementById('restartBtn'); | |
| // Game State | |
| let gameRunning = false; | |
| let score = 0; | |
| let health = 100; | |
| let animationId; | |
| let spawnInterval; | |
| let difficultyMultiplier = 1; | |
| // Resize Canvas | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| window.addEventListener('resize', () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Recenter player | |
| player.x = canvas.width / 2; | |
| player.y = canvas.height / 2; | |
| }); | |
| // Classes | |
| class Player { | |
| constructor(x, y, radius, color) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = radius; | |
| this.color = color; | |
| this.angle = 0; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.translate(this.x, this.y); | |
| ctx.rotate(this.angle); | |
| // Glow effect | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = this.color; | |
| // Main body | |
| ctx.beginPath(); | |
| ctx.arc(0, 0, this.radius, 0, Math.PI * 2, false); | |
| ctx.fillStyle = this.color; | |
| ctx.fill(); | |
| // Turret/Cannon | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, -5, this.radius + 10, 10); | |
| ctx.restore(); | |
| } | |
| update(mouse) { | |
| // Calculate angle to mouse | |
| const dx = mouse.x - this.x; | |
| const dy = mouse.y - this.y; | |
| this.angle = Math.atan2(dy, dx); | |
| this.draw(); | |
| } | |
| } | |
| class Projectile { | |
| constructor(x, y, radius, color, velocity) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = radius; | |
| this.color = color; | |
| this.velocity = velocity; | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false); | |
| ctx.fillStyle = this.color; | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = this.color; | |
| ctx.fill(); | |
| } | |
| update() { | |
| this.draw(); | |
| this.x = this.x + this.velocity.x; | |
| this.y = this.y + this.velocity.y; | |
| } | |
| } | |
| class Enemy { | |
| constructor(x, y, radius, color, velocity) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = radius; | |
| this.color = color; | |
| this.velocity = velocity; | |
| this.type = Math.random() > 0.8 ? 'fast' : 'normal'; | |
| if (this.type === 'fast') { | |
| this.radius = radius * 0.7; | |
| this.color = '#ff00ff'; // Pink | |
| this.velocity = { | |
| x: velocity.x * 1.5, | |
| y: velocity.y * 1.5 | |
| } | |
| } | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| if (this.type === 'fast') { | |
| // Diamond shape for fast enemies | |
| ctx.moveTo(this.x, this.y - this.radius); | |
| ctx.lineTo(this.x + this.radius, this.y); | |
| ctx.lineTo(this.x, this.y + this.radius); | |
| ctx.lineTo(this.x - this.radius, this.y); | |
| } else { | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false); | |
| } | |
| ctx.fillStyle = this.color; | |
| ctx.shadowBlur = 15; | |
| ctx.shadowColor = this.color; | |
| ctx.fill(); | |
| } | |
| update() { | |
| this.draw(); | |
| // Move towards player | |
| const angle = Math.atan2(player.y - this.y, player.x - this.x); | |
| this.velocity = { | |
| x: Math.cos(angle) * (1 * difficultyMultiplier), | |
| y: Math.sin(angle) * (1 * difficultyMultiplier) | |
| }; | |
| this.x = this.x + this.velocity.x; | |
| this.y = this.y + this.velocity.y; | |
| } | |
| } | |
| const friction = 0.98; | |
| class Particle { | |
| constructor(x, y, radius, color, velocity) { | |
| this.x = x; | |
| this.y = y; | |
| this.radius = radius; | |
| this.color = color; | |
| this.velocity = velocity; | |
| this.alpha = 1; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.globalAlpha = this.alpha; | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false); | |
| ctx.fillStyle = this.color; | |
| ctx.fill(); | |
| ctx.restore(); | |
| } | |
| update() { | |
| this.draw(); | |
| this.velocity.x *= friction; | |
| this.velocity.y *= friction; | |
| this.x = this.x + this.velocity.x; | |
| this.y = this.y + this.velocity.y; | |
| this.alpha -= 0.01; | |
| } | |
| } | |
| // Game Variables | |
| const x = canvas.width / 2; | |
| const y = canvas.height / 2; | |
| let player = new Player(x, y, 15, '#00f3ff'); | |
| let projectiles = []; | |
| let enemies = []; | |
| let particles = []; | |
| let mouse = { x: x, y: y }; | |
| // Input Handling | |
| window.addEventListener('mousemove', (event) => { | |
| mouse.x = event.clientX; | |
| mouse.y = event.clientY; | |
| }); | |
| window.addEventListener('touchmove', (event) => { | |
| mouse.x = event.touches[0].clientX; | |
| mouse.y = event.touches[0].clientY; | |
| }); | |
| window.addEventListener('click', () => { | |
| if (!gameRunning) return; | |
| const angle = Math.atan2(mouse.y - player.y, mouse.x - player.x); | |
| const velocity = { | |
| x: Math.cos(angle) * 8, | |
| y: Math.sin(angle) * 8 | |
| }; | |
| projectiles.push(new Projectile(player.x, player.y, 5, 'white', velocity)); | |
| playSound('shoot'); | |
| }); | |
| // Core Functions | |
| function spawnEnemies() { | |
| spawnInterval = setInterval(() => { | |
| const radius = Math.random() * (30 - 10) + 10; | |
| let x; | |
| let y; | |
| if (Math.random() < 0.5) { | |
| x = Math.random() < 0.5 ? 0 - radius : canvas.width + radius; | |
| y = Math.random() * canvas.height; | |
| } else { | |
| x = Math.random() * canvas.width; | |
| y = Math.random() < 0.5 ? 0 - radius : canvas.height + radius; | |
| } | |
| const color = `hsl(${Math.random() * 60 + 180}, 100%, 50%)`; // Cyan/Green range | |
| const angle = Math.atan2(canvas.height / 2 - y, canvas.width / 2 - x); | |
| const velocity = { | |
| x: Math.cos(angle), | |
| y: Math.sin(angle) | |
| }; | |
| enemies.push(new Enemy(x, y, radius, color, velocity)); | |
| }, 1000); | |
| } | |
| function initGame() { | |
| player = new Player(canvas.width / 2, canvas.height / 2, 15, '#00f3ff'); | |
| projectiles = []; | |
| enemies = []; | |
| particles = []; | |
| score = 0; | |
| health = 100; | |
| difficultyMultiplier = 1; | |
| scoreEl.innerHTML = score; | |
| healthEl.style.width = '100%'; | |
| gameRunning = true; | |
| startScreen.classList.add('hidden'); | |
| gameOverScreen.classList.add('hidden'); | |
| spawnEnemies(); | |
| animate(); | |
| } | |
| function endGame() { | |
| gameRunning = false; | |
| cancelAnimationFrame(animationId); | |
| clearInterval(spawnInterval); | |
| // Save High Score | |
| const currentHigh = localStorage.getItem('neonWarHighScore') || 0; | |
| if (score > currentHigh) { | |
| localStorage.setItem('neonWarHighScore', score); | |
| } | |
| finalScoreEl.innerHTML = score; | |
| highScoreEl.innerHTML = Math.max(score, currentHigh); | |
| gameOverScreen.classList.remove('hidden'); | |
| } | |
| function animate() { | |
| if (!gameRunning) return; | |
| animationId = requestAnimationFrame(animate); | |
| // Trail effect (clearing with opacity) | |
| ctx.fillStyle = 'rgba(5, 5, 5, 0.2)'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| player.update(mouse); | |
| // Particles Logic | |
| particles.forEach((particle, index) => { | |
| if (particle.alpha <= 0) { | |
| particles.splice(index, 1); | |
| } else { | |
| particle.update(); | |
| } | |
| }); | |
| // Projectiles Logic | |
| projectiles.forEach((projectile, index) => { | |
| projectile.update(); | |
| // Remove off-screen projectiles | |
| if (projectile.x + projectile.radius < 0 || | |
| projectile.x - projectile.radius > canvas.width || | |
| projectile.y + projectile.radius < 0 || | |
| projectile.y - projectile.radius > canvas.height) { | |
| setTimeout(() => { | |
| projectiles.splice(index, 1); | |
| }, 0); | |
| } | |
| }); | |
| // Enemies Logic | |
| enemies.forEach((enemy, index) => { | |
| enemy.update(); | |
| // Collision: Player vs Enemy | |
| const dist = Math.hypot(player.x - enemy.x, player.y - enemy.y); | |
| if (dist - enemy.radius - player.radius < 1) { | |
| // Player Hit | |
| enemies.splice(index, 1); | |
| health -= 20; | |
| healthEl.style.width = `${health}%`; | |
| playSound('explosion'); | |
| // Screen Shake Effect | |
| canvas.style.transform = `translate(${Math.random()*10 - 5}px, ${Math.random()*10 - 5}px)`; | |
| setTimeout(() => canvas.style.transform = 'none', 100); | |
| if (health <= 0) { | |
| endGame(); | |
| } | |
| } | |
| // Collision: Projectile vs Enemy | |
| projectiles.forEach((projectile, pIndex) => { | |
| const dist = Math.hypot(projectile.x - enemy.x, projectile.y - enemy.y); | |
| if (dist - enemy.radius - projectile.radius < 1) { | |
| // Create Explosion Particles | |
| for (let i = 0; i < enemy.radius * 2; i++) { | |
| particles.push(new Particle( | |
| projectile.x, | |
| projectile.y, | |
| Math.random() * 2, | |
| enemy.color, | |
| { | |
| x: (Math.random() - 0.5) * (Math.random() * 6), | |
| y: (Math.random() - 0.5) * (Math.random() * 6) | |
| } | |
| )); | |
| } | |
| // Shrink enemy or destroy | |
| if (enemy.radius - 10 > 10) { | |
| score += 50; | |
| scoreEl.innerHTML = score; | |
| enemy.radius -= 10; | |
| setTimeout(() => { | |
| projectiles.splice(pIndex, 1); | |
| }, 0); | |
| playSound('hit'); | |
| } else { | |
| score += 150; | |
| scoreEl.innerHTML = score; | |
| playSound('explosion'); | |
| setTimeout(() => { | |
| enemies.splice(index, 1); | |
| projectiles.splice(pIndex, 1); | |
| }, 0); | |
| } | |
| // Increase difficulty slightly | |
| difficultyMultiplier += 0.005; | |
| } | |
| }); | |
| }); | |
| } | |
| // Event Listeners for Buttons | |
| startBtn.addEventListener('click', () => { | |
| // Initialize Audio Context on user interaction | |
| if (audioCtx.state === 'suspended') { | |
| audioCtx.resume(); | |
| } | |
| initGame(); | |
| }); | |
| restartBtn.addEventListener('click', () => { | |
| initGame(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |