Created
July 13, 2025 08:57
-
-
Save tluyben/3b805957c85873530cac4fd0cd39a371 to your computer and use it in GitHub Desktop.
shmup deepseek v3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Nemesis Shmup</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 0; | |
background: #000; | |
overflow: hidden; | |
touch-action: none; | |
font-family: Arial, sans-serif; | |
} | |
#game-container { | |
position: relative; | |
width: 100vw; | |
height: 100vh; | |
overflow: hidden; | |
} | |
#game-canvas { | |
display: block; | |
background: linear-gradient(to bottom, #000033, #000066); | |
} | |
#start-screen, #game-over-screen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
background: rgba(0, 0, 0, 0.7); | |
color: white; | |
z-index: 10; | |
} | |
#game-over-screen { | |
display: none; | |
} | |
h1 { | |
font-size: 3rem; | |
color: #ff0; | |
text-shadow: 0 0 10px #ff0; | |
margin-bottom: 2rem; | |
} | |
button { | |
background: #f00; | |
color: white; | |
border: none; | |
padding: 1rem 2rem; | |
font-size: 1.5rem; | |
border-radius: 5px; | |
cursor: pointer; | |
transition: all 0.3s; | |
} | |
button:hover { | |
background: #ff0; | |
color: #000; | |
transform: scale(1.1); | |
} | |
#score-display { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: white; | |
font-size: 1.5rem; | |
z-index: 5; | |
} | |
#health-display { | |
position: absolute; | |
top: 10px; | |
right: 10px; | |
color: #0f0; | |
font-size: 1.5rem; | |
z-index: 5; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<canvas id="game-canvas"></canvas> | |
<div id="score-display">Score: 0</div> | |
<div id="health-display">Health: 100%</div> | |
<div id="start-screen"> | |
<h1>NEMESIS</h1> | |
<button id="start-button">START GAME</button> | |
</div> | |
<div id="game-over-screen"> | |
<h1>GAME OVER</h1> | |
<div id="final-score" style="font-size: 2rem; margin-bottom: 2rem;"></div> | |
<button id="restart-button">PLAY AGAIN</button> | |
</div> | |
</div> | |
<script> | |
// Game variables | |
const canvas = document.getElementById('game-canvas'); | |
const ctx = canvas.getContext('2d'); | |
const startScreen = document.getElementById('start-screen'); | |
const gameOverScreen = document.getElementById('game-over-screen'); | |
const startButton = document.getElementById('start-button'); | |
const restartButton = document.getElementById('restart-button'); | |
const scoreDisplay = document.getElementById('score-display'); | |
const healthDisplay = document.getElementById('health-display'); | |
const finalScoreDisplay = document.getElementById('final-score'); | |
// Set canvas size | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
// Game state | |
let gameRunning = false; | |
let score = 0; | |
let playerHealth = 100; | |
// Player | |
const player = { | |
x: canvas.width / 4, | |
y: canvas.height / 2, | |
width: 40, | |
height: 40, | |
speed: 6, | |
bullets: [], | |
lastShot: 0, | |
shootDelay: 300, | |
direction: 0 // 0 = right, 1 = left | |
}; | |
// Enemies | |
const enemies = []; | |
const enemyTypes = [ | |
{ width: 30, height: 30, color: '#f00', health: 1, score: 10, speed: 2 }, | |
{ width: 50, height: 30, color: '#ff0', health: 2, score: 20, speed: 1.5 }, | |
{ width: 40, height: 40, color: '#f0f', health: 3, score: 30, speed: 1 } | |
]; | |
// Explosions | |
const explosions = []; | |
// Background stars | |
const stars = []; | |
for (let i = 0; i < 100; i++) { | |
stars.push({ | |
x: Math.random() * canvas.width, | |
y: Math.random() * canvas.height, | |
size: Math.random() * 2 + 1, | |
speed: Math.random() * 2 + 1 | |
}); | |
} | |
// Sprites (using simple shapes for this example) | |
function drawPlayer() { | |
ctx.save(); | |
ctx.translate(player.x, player.y); | |
if (player.direction === 0) { | |
// Right-facing ship | |
ctx.fillStyle = '#0af'; | |
ctx.beginPath(); | |
ctx.moveTo(20, 0); | |
ctx.lineTo(-20, -20); | |
ctx.lineTo(-10, 0); | |
ctx.lineTo(-20, 20); | |
ctx.closePath(); | |
ctx.fill(); | |
// Engine glow | |
ctx.fillStyle = '#ff0'; | |
ctx.beginPath(); | |
ctx.moveTo(-15, -10); | |
ctx.lineTo(-25, 0); | |
ctx.lineTo(-15, 10); | |
ctx.closePath(); | |
ctx.fill(); | |
} else { | |
// Left-facing ship | |
ctx.fillStyle = '#0af'; | |
ctx.beginPath(); | |
ctx.moveTo(-20, 0); | |
ctx.lineTo(20, -20); | |
ctx.lineTo(10, 0); | |
ctx.lineTo(20, 20); | |
ctx.closePath(); | |
ctx.fill(); | |
// Engine glow | |
ctx.fillStyle = '#ff0'; | |
ctx.beginPath(); | |
ctx.moveTo(15, -10); | |
ctx.lineTo(25, 0); | |
ctx.lineTo(15, 10); | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
ctx.restore(); | |
} | |
function drawEnemy(enemy) { | |
ctx.save(); | |
ctx.translate(enemy.x, enemy.y); | |
switch(enemy.type) { | |
case 0: // Small red enemy | |
ctx.fillStyle = '#f00'; | |
ctx.beginPath(); | |
ctx.moveTo(0, -15); | |
ctx.lineTo(15, 15); | |
ctx.lineTo(-15, 15); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
case 1: // Medium yellow enemy | |
ctx.fillStyle = '#ff0'; | |
ctx.beginPath(); | |
ctx.moveTo(0, -15); | |
ctx.lineTo(25, 15); | |
ctx.lineTo(-25, 15); | |
ctx.closePath(); | |
ctx.fill(); | |
// Cockpit | |
ctx.fillStyle = '#000'; | |
ctx.beginPath(); | |
ctx.arc(0, 0, 5, 0, Math.PI * 2); | |
ctx.fill(); | |
break; | |
case 2: // Large purple enemy | |
ctx.fillStyle = '#f0f'; | |
ctx.beginPath(); | |
ctx.moveTo(0, -20); | |
ctx.lineTo(20, 20); | |
ctx.lineTo(-20, 20); | |
ctx.closePath(); | |
ctx.fill(); | |
// Details | |
ctx.strokeStyle = '#000'; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo(-5, -10); | |
ctx.lineTo(5, -10); | |
ctx.moveTo(-8, 0); | |
ctx.lineTo(8, 0); | |
ctx.moveTo(-10, 10); | |
ctx.lineTo(10, 10); | |
ctx.stroke(); | |
break; | |
} | |
ctx.restore(); | |
} | |
function drawBullet(bullet) { | |
ctx.save(); | |
ctx.translate(bullet.x, bullet.y); | |
ctx.fillStyle = bullet.playerBullet ? '#ff0' : '#f00'; | |
ctx.beginPath(); | |
ctx.arc(0, 0, 3, 0, Math.PI * 2); | |
ctx.fill(); | |
// Add glow | |
ctx.fillStyle = bullet.playerBullet ? 'rgba(255, 255, 0, 0.3)' : 'rgba(255, 0, 0, 0.3)'; | |
ctx.beginPath(); | |
ctx.arc(0, 0, 6, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.restore(); | |
} | |
function drawExplosion(explosion) { | |
ctx.save(); | |
ctx.translate(explosion.x, explosion.y); | |
const radius = explosion.radius * (1 - explosion.progress); | |
const alpha = 1 - explosion.progress; | |
ctx.fillStyle = `rgba(255, ${Math.floor(100 + 155 * explosion.progress)}, 0, ${alpha})`; | |
ctx.beginPath(); | |
ctx.arc(0, 0, radius, 0, Math.PI * 2); | |
ctx.fill(); | |
// Add some particles | |
for (let i = 0; i < 5; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const dist = Math.random() * radius * 0.8; | |
const size = Math.random() * 3 + 1; | |
ctx.fillStyle = `rgba(255, ${Math.floor(200 + 55 * explosion.progress)}, 0, ${alpha})`; | |
ctx.beginPath(); | |
ctx.arc(Math.cos(angle) * dist, Math.sin(angle) * dist, size, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
ctx.restore(); | |
} | |
// Game functions | |
function spawnEnemy() { | |
const type = Math.floor(Math.random() * enemyTypes.length); | |
const enemyType = enemyTypes[type]; | |
enemies.push({ | |
x: canvas.width + 50, | |
y: Math.random() * (canvas.height - 100) + 50, | |
width: enemyType.width, | |
height: enemyType.height, | |
speed: enemyType.speed, | |
health: enemyType.health, | |
type: type, | |
lastShot: 0, | |
shootDelay: 1500 + Math.random() * 1000 | |
}); | |
} | |
function shoot(fromPlayer = true) { | |
const now = Date.now(); | |
if (fromPlayer) { | |
if (now - player.lastShot > player.shootDelay) { | |
player.bullets.push({ | |
x: player.x + (player.direction === 0 ? 20 : -20), | |
y: player.y, | |
speed: player.direction === 0 ? 10 : -10, | |
playerBullet: true | |
}); | |
player.lastShot = now; | |
playSound('shoot'); | |
} | |
} else { | |
// Enemy shooting | |
for (let enemy of enemies) { | |
if (now - enemy.lastShot > enemy.shootDelay) { | |
player.bullets.push({ | |
x: enemy.x - 15, | |
y: enemy.y, | |
speed: -7, | |
playerBullet: false | |
}); | |
enemy.lastShot = now; | |
playSound('enemyShoot'); | |
} | |
} | |
} | |
} | |
function createExplosion(x, y, radius = 30) { | |
explosions.push({ | |
x: x, | |
y: y, | |
radius: radius, | |
progress: 0, | |
duration: 500 | |
}); | |
playSound('explosion'); | |
} | |
// Sound effects | |
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
function playSound(type) { | |
const now = audioContext.currentTime; | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
switch(type) { | |
case 'shoot': | |
oscillator.type = 'square'; | |
oscillator.frequency.setValueAtTime(880, now); | |
oscillator.frequency.exponentialRampToValueAtTime(440, now + 0.1); | |
gainNode.gain.setValueAtTime(0.2, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.1); | |
oscillator.start(now); | |
oscillator.stop(now + 0.1); | |
break; | |
case 'enemyShoot': | |
oscillator.type = 'sawtooth'; | |
oscillator.frequency.setValueAtTime(220, now); | |
oscillator.frequency.exponentialRampToValueAtTime(110, now + 0.2); | |
gainNode.gain.setValueAtTime(0.1, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.2); | |
oscillator.start(now); | |
oscillator.stop(now + 0.2); | |
break; | |
case 'explosion': | |
oscillator.type = 'sine'; | |
oscillator.frequency.setValueAtTime(50, now); | |
oscillator.frequency.exponentialRampToValueAtTime(20, now + 0.5); | |
gainNode.gain.setValueAtTime(0.3, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.5); | |
oscillator.start(now); | |
oscillator.stop(now + 0.5); | |
break; | |
case 'hit': | |
oscillator.type = 'triangle'; | |
oscillator.frequency.setValueAtTime(440, now); | |
oscillator.frequency.exponentialRampToValueAtTime(220, now + 0.1); | |
gainNode.gain.setValueAtTime(0.1, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.1); | |
oscillator.start(now); | |
oscillator.stop(now + 0.1); | |
break; | |
} | |
} | |
// Input handling | |
const keys = { | |
ArrowUp: false, | |
ArrowDown: false, | |
ArrowLeft: false, | |
ArrowRight: false, | |
Space: false, | |
z: false | |
}; | |
document.addEventListener('keydown', (e) => { | |
if (e.code === 'ArrowUp') keys.ArrowUp = true; | |
if (e.code === 'ArrowDown') keys.ArrowDown = true; | |
if (e.code === 'ArrowLeft') keys.ArrowLeft = true; | |
if (e.code === 'ArrowRight') keys.ArrowRight = true; | |
if (e.code === 'Space') keys.Space = true; | |
if (e.key === 'z' || e.key === 'Z') keys.z = true; | |
}); | |
document.addEventListener('keyup', (e) => { | |
if (e.code === 'ArrowUp') keys.ArrowUp = false; | |
if (e.code === 'ArrowDown') keys.ArrowDown = false; | |
if (e.code === 'ArrowLeft') keys.ArrowLeft = false; | |
if (e.code === 'ArrowRight') keys.ArrowRight = false; | |
if (e.code === 'Space') keys.Space = false; | |
if (e.key === 'z' || e.key === 'Z') keys.z = false; | |
}); | |
// Touch controls for mobile | |
let touchStartX = 0; | |
let touchStartY = 0; | |
canvas.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
touchStartX = e.touches[0].clientX; | |
touchStartY = e.touches[0].clientY; | |
// Shoot if touching right side of screen | |
if (touchStartX > canvas.width / 2) { | |
keys.Space = true; | |
} | |
}, { passive: false }); | |
canvas.addEventListener('touchmove', (e) => { | |
e.preventDefault(); | |
const touchX = e.touches[0].clientX; | |
const touchY = e.touches[0].clientY; | |
// Left side controls movement | |
if (touchStartX < canvas.width / 2) { | |
const deltaX = touchX - touchStartX; | |
const deltaY = touchY - touchStartY; | |
keys.ArrowLeft = deltaX < -10; | |
keys.ArrowRight = deltaX > 10; | |
keys.ArrowUp = deltaY < -10; | |
keys.ArrowDown = deltaY > 10; | |
} | |
}, { passive: false }); | |
canvas.addEventListener('touchend', (e) => { | |
e.preventDefault(); | |
keys.ArrowUp = false; | |
keys.ArrowDown = false; | |
keys.ArrowLeft = false; | |
keys.ArrowRight = false; | |
keys.Space = false; | |
}, { passive: false }); | |
// Game loop | |
function gameLoop() { | |
if (!gameRunning) return; | |
// Clear canvas | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// Draw background | |
ctx.fillStyle = '#000033'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// Draw stars | |
ctx.fillStyle = '#fff'; | |
for (let star of stars) { | |
star.x -= star.speed; | |
if (star.x < 0) { | |
star.x = canvas.width; | |
star.y = Math.random() * canvas.height; | |
} | |
ctx.beginPath(); | |
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
// Handle player input | |
if (keys.ArrowUp && player.y > player.height / 2) { | |
player.y -= player.speed; | |
} | |
if (keys.ArrowDown && player.y < canvas.height - player.height / 2) { | |
player.y += player.speed; | |
} | |
if (keys.ArrowLeft) { | |
player.x -= player.speed; | |
player.direction = 1; | |
} | |
if (keys.ArrowRight) { | |
player.x += player.speed; | |
player.direction = 0; | |
} | |
if ((keys.Space || keys.z) && gameRunning) { | |
shoot(true); | |
} | |
// Keep player within bounds | |
player.x = Math.max(player.width / 2, Math.min(canvas.width - player.width / 2, player.x)); | |
player.y = Math.max(player.height / 2, Math.min(canvas.height - player.height / 2, player.y)); | |
// Spawn enemies | |
if (Math.random() < 0.02) { | |
spawnEnemy(); | |
} | |
// Update and draw bullets | |
for (let i = player.bullets.length - 1; i >= 0; i--) { | |
const bullet = player.bullets[i]; | |
bullet.x += bullet.speed; | |
// Remove bullets that are off screen | |
if (bullet.x < 0 || bullet.x > canvas.width) { | |
player.bullets.splice(i, 1); | |
continue; | |
} | |
// Check bullet-enemy collisions | |
if (bullet.playerBullet) { | |
for (let j = enemies.length - 1; j >= 0; j--) { | |
const enemy = enemies[j]; | |
const dx = bullet.x - enemy.x; | |
const dy = bullet.y - enemy.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < 20) { | |
enemy.health--; | |
player.bullets.splice(i, 1); | |
playSound('hit'); | |
if (enemy.health <= 0) { | |
score += enemyTypes[enemy.type].score; | |
createExplosion(enemy.x, enemy.y, 40); | |
enemies.splice(j, 1); | |
} | |
break; | |
} | |
} | |
} else { | |
// Check bullet-player collision | |
const dx = bullet.x - player.x; | |
const dy = bullet.y - player.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < 20) { | |
playerHealth -= 10; | |
player.bullets.splice(i, 1); | |
playSound('hit'); | |
if (playerHealth <= 0) { | |
gameOver(); | |
return; | |
} | |
} | |
} | |
} | |
// Update and draw enemies | |
for (let i = enemies.length - 1; i >= 0; i--) { | |
const enemy = enemies[i]; | |
enemy.x -= enemy.speed; | |
// Remove enemies that are off screen | |
if (enemy.x < -50) { | |
enemies.splice(i, 1); | |
continue; | |
} | |
// Enemy shooting | |
if (Math.random() < 0.01) { | |
shoot(false); | |
} | |
// Check player-enemy collision | |
const dx = enemy.x - player.x; | |
const dy = enemy.y - player.y; | |
const distance = Math.sqrt(dx * dx + dy * dy); | |
if (distance < 30) { | |
playerHealth -= 20; | |
playSound('hit'); | |
createExplosion(enemy.x, enemy.y, 50); | |
enemies.splice(i, 1); | |
if (playerHealth <= 0) { | |
gameOver(); | |
return; | |
} | |
} | |
} | |
// Update explosions | |
const now = Date.now(); | |
for (let i = explosions.length - 1; i >= 0; i--) { | |
const explosion = explosions[i]; | |
explosion.progress = (now - explosion.startTime) / explosion.duration; | |
if (explosion.progress >= 1) { | |
explosions.splice(i, 1); | |
} | |
} | |
// Draw game objects | |
for (const bullet of player.bullets) { | |
drawBullet(bullet); | |
} | |
for (const enemy of enemies) { | |
drawEnemy(enemy); | |
} | |
for (const explosion of explosions) { | |
drawExplosion(explosion); | |
} | |
drawPlayer(); | |
// Update UI | |
scoreDisplay.textContent = `Score: ${score}`; | |
healthDisplay.textContent = `Health: ${playerHealth}%`; | |
healthDisplay.style.color = playerHealth > 50 ? '#0f0' : playerHealth > 20 ? '#ff0' : '#f00'; | |
requestAnimationFrame(gameLoop); | |
} | |
// Game state functions | |
function startGame() { | |
// Reset game state | |
score = 0; | |
playerHealth = 100; | |
player.x = canvas.width / 4; | |
player.y = canvas.height / 2; | |
player.bullets = []; | |
enemies.length = 0; | |
explosions.length = 0; | |
// Hide start screen, show game | |
startScreen.style.display = 'none'; | |
gameOverScreen.style.display = 'none'; | |
gameRunning = true; | |
// Start game loop | |
gameLoop(); | |
} | |
function gameOver() { | |
gameRunning = false; | |
finalScoreDisplay.textContent = `Final Score: ${score}`; | |
gameOverScreen.style.display = 'flex'; | |
createExplosion(player.x, player.y, 60); | |
} | |
// Event listeners | |
startButton.addEventListener('click', startGame); | |
restartButton.addEventListener('click', startGame); | |
// Initialize explosions with startTime | |
function initExplosion(explosion) { | |
explosion.startTime = Date.now(); | |
return explosion; | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment