Last active
September 30, 2025 06:11
-
-
Save lardratboy/8828c519770b510043fb5c6a5497e41d to your computer and use it in GitHub Desktop.
some progress purgatory game
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
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Space Shooter</title> | |
| <style> | |
| body { margin: 0; overflow: hidden; } | |
| #score { position: fixed; top: 20px; right: 20px; color: white; font-family: Arial; } | |
| #restart { position: fixed; top: 20px; left: 20px; padding: 10px; } | |
| #instructions { position: fixed; bottom: 20px; left: 20px; color: white; font-family: Arial; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="score"> | |
| Score: <span id="scoreValue">0</span><br> | |
| Level: <span id="levelValue">1</span><br> | |
| Lives: <span id="livesValue">3</span> | |
| </div> | |
| <button id="restart">Restart</button> | |
| <div id="instructions"> | |
| WASD or Arrow Keys to move<br> | |
| Space to shoot<br> | |
| Extra life every 10,000 points! | |
| </div> | |
| <div id="gameOver" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: Arial; font-size: 48px; text-align: center;"> | |
| GAME OVER<br> | |
| <span style="font-size: 24px;">Press Restart to try again</span> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script> | |
| // Enemy type definitions | |
| const ENEMY_TYPES = { | |
| SPHERE: { | |
| name: 'Sphere', | |
| color: 0xff0000, // Red | |
| geometry: () => new THREE.SphereGeometry(0.5, 16, 16), | |
| pattern: 0, | |
| points: 50 | |
| }, | |
| TETRAHEDRON: { | |
| name: 'Tetrahedron', | |
| color: 0x00ff00, // Green | |
| geometry: () => new THREE.TetrahedronGeometry(0.6), | |
| pattern: 1, | |
| points: 100 | |
| }, | |
| CUBE: { | |
| name: 'Cube', | |
| color: 0x0000ff, // Blue | |
| geometry: () => new THREE.BoxGeometry(0.8, 0.8, 0.8), | |
| pattern: 2, | |
| points: 150 | |
| }, | |
| OCTAHEDRON: { | |
| name: 'Octahedron', | |
| color: 0xffff00, // Yellow | |
| geometry: () => new THREE.OctahedronGeometry(0.6), | |
| pattern: 3, | |
| points: 200 | |
| }, | |
| DODECAHEDRON: { | |
| name: 'Dodecahedron', | |
| color: 0xff00ff, // Magenta | |
| geometry: () => new THREE.DodecahedronGeometry(0.5), | |
| pattern: 4, | |
| points: 250 | |
| }, | |
| ICOSAHEDRON: { | |
| name: 'Icosahedron', | |
| color: 0x888888, // Medium Gray | |
| geometry: () => new THREE.IcosahedronGeometry(0.6), | |
| pattern: 5, | |
| points: 300, | |
| slowDown: true // Move slower than other enemies | |
| } | |
| }; | |
| class GameObject { | |
| constructor(geometry, material, position = { x: 0, y: 0, z: 0 }) { | |
| this.mesh = new THREE.Mesh(geometry, material); | |
| this.mesh.position.set(position.x, position.y, position.z); | |
| this.velocity = { x: 0, y: 0, z: 0 }; | |
| } | |
| update() { | |
| this.mesh.position.x += this.velocity.x; | |
| this.mesh.position.y += this.velocity.y; | |
| } | |
| } | |
| class Player extends GameObject { | |
| constructor() { | |
| const geometry = new THREE.ConeGeometry(0.5, 1, 3); | |
| const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
| super(geometry, material); | |
| this.mesh.rotation.z = 0; | |
| this.speed = 0.15; | |
| this.isInvulnerable = false; | |
| this.invulnerableTime = 0; | |
| this.blinkTime = 0; | |
| } | |
| update(deltaTime) { | |
| if (this.isInvulnerable) { | |
| this.invulnerableTime += deltaTime; | |
| this.blinkTime += deltaTime; | |
| // Blink effect | |
| if (this.blinkTime > 0.1) { | |
| this.mesh.visible = !this.mesh.visible; | |
| this.blinkTime = 0; | |
| } | |
| // End invulnerability after 3 seconds | |
| if (this.invulnerableTime > 3) { | |
| this.isInvulnerable = false; | |
| this.mesh.visible = true; | |
| } | |
| } | |
| } | |
| makeInvulnerable() { | |
| this.isInvulnerable = true; | |
| this.invulnerableTime = 0; | |
| this.blinkTime = 0; | |
| } | |
| } | |
| class Enemy extends GameObject { | |
| constructor(position, enemyType, enemyIndex, level, groupParams) { | |
| const geometry = enemyType.geometry(); | |
| const material = new THREE.MeshPhongMaterial({ | |
| color: enemyType.color, | |
| shininess: 60, | |
| specular: 0x444444 | |
| }); | |
| super(geometry, material, position); | |
| this.type = enemyType; | |
| this.pattern = enemyType.pattern; | |
| this.enemyIndex = enemyIndex; | |
| this.level = level; | |
| this.groupParams = groupParams; // Parameters for group center movement | |
| // Increase movement speed and radius with level | |
| this.radius = 1 + (level - 1) * 0.15; | |
| // Some enemy types move slower | |
| if (enemyType.slowDown) { | |
| this.speedMultiplier = (1 + (level - 1) * 0.12) * 0.5; // Half speed | |
| } else { | |
| this.speedMultiplier = 1 + (level - 1) * 0.12; | |
| } | |
| this.formationTime = 0; | |
| this.groupTime = 0; // Separate time for group center movement | |
| // Dive attack state | |
| this.isDiving = false; | |
| this.isReturning = false; | |
| this.diveTimer = Math.random() * 10 + 5; // Random delay before first dive | |
| this.returnProgress = 0; | |
| this.returnStartX = 0; | |
| this.returnStartY = 0; | |
| // Set initial offscreen position | |
| const angle = Math.random() * Math.PI * 2; | |
| const distance = 15; | |
| this.startX = Math.cos(angle) * distance; | |
| this.startY = Math.sin(angle) * distance; | |
| this.mesh.position.set(this.startX, this.startY, 0); | |
| // Initial target position | |
| this.initialCenterX = position.x; | |
| this.initialCenterY = position.y; | |
| this.centerX = position.x; | |
| this.centerY = position.y; | |
| // Entry animation | |
| this.entryProgress = 0; | |
| this.isEntering = true; | |
| } | |
| update() { | |
| // Update group center position using ping pong horizontal movement | |
| if (!this.isEntering && !this.isDiving && !this.isReturning) { | |
| this.groupTime += 0.015 * this.speedMultiplier; | |
| const gp = this.groupParams; | |
| // Horizontal ping pong movement | |
| const cycleLength = gp.cycleLength; | |
| const cycleProgress = (this.groupTime % cycleLength) / cycleLength; | |
| const cycleNumber = Math.floor(this.groupTime / cycleLength); | |
| const movingRight = cycleNumber % 2 === 0; | |
| if (movingRight) { | |
| this.centerX = gp.leftBound + (cycleProgress * (gp.rightBound - gp.leftBound)); | |
| } else { | |
| this.centerX = gp.rightBound - (cycleProgress * (gp.rightBound - gp.leftBound)); | |
| } | |
| // Vertical descent - increases with level | |
| const descentRate = 0.15 + (this.level - 1) * 0.02; | |
| const totalDescent = Math.floor(this.groupTime / cycleLength) * descentRate; | |
| this.centerY = this.initialCenterY - totalDescent; | |
| // Add slight Lissajous wave on top for visual interest | |
| this.centerY += Math.sin(gp.freqY * this.groupTime + gp.phaseY) * gp.amplitudeY; | |
| } | |
| // Entry animation | |
| if (this.isEntering) { | |
| this.entryProgress += 0.05; | |
| if (this.entryProgress >= 1) { | |
| this.isEntering = false; | |
| } | |
| const ease = 1 - Math.pow(1 - this.entryProgress, 3); | |
| this.mesh.position.x = this.startX + (this.centerX - this.startX) * ease; | |
| this.mesh.position.y = this.startY + (this.centerY - this.startY) * ease; | |
| // Orient during entry | |
| const angle = Math.atan2( | |
| this.centerY - this.mesh.position.y, | |
| this.centerX - this.mesh.position.x | |
| ); | |
| this.mesh.rotation.z = angle + Math.PI / 2; | |
| return; | |
| } | |
| // Return animation (wrapping back in from behind camera) | |
| if (this.isReturning) { | |
| this.returnProgress += 0.04; | |
| if (this.returnProgress >= 1) { | |
| this.isReturning = false; | |
| this.isDiving = false; | |
| this.diveTimer = Math.random() * 15 + 8; // Reset dive timer | |
| } | |
| const ease = 1 - Math.pow(1 - this.returnProgress, 3); | |
| this.mesh.position.x = this.returnStartX + (this.centerX - this.returnStartX) * ease; | |
| this.mesh.position.y = this.returnStartY + (this.centerY - this.returnStartY) * ease; | |
| this.mesh.position.z = 5 * (1 - ease); // Come in from behind camera | |
| // Orient toward center | |
| const angle = Math.atan2( | |
| this.centerY - this.mesh.position.y, | |
| this.centerX - this.mesh.position.x | |
| ); | |
| this.mesh.rotation.z = angle + Math.PI / 2; | |
| return; | |
| } | |
| // Dive attack | |
| if (this.isDiving) { | |
| this.mesh.position.y -= 0.25; // Fast downward movement | |
| // When off screen, start return animation | |
| if (this.mesh.position.y < -6) { | |
| this.isReturning = true; | |
| this.returnProgress = 0; | |
| // Set return start position (from behind camera, random angle) | |
| const angle = Math.random() * Math.PI * 2; | |
| const distance = 12; | |
| this.returnStartX = Math.cos(angle) * distance; | |
| this.returnStartY = Math.sin(angle) * distance; | |
| this.mesh.position.set(this.returnStartX, this.returnStartY, 5); | |
| } | |
| // Keep pointing down during dive | |
| this.mesh.rotation.z = Math.PI; | |
| this.mesh.rotation.x += 0.05; | |
| return; | |
| } | |
| // Check for dive initiation | |
| if (!this.isDiving && !this.isReturning) { | |
| this.diveTimer -= 0.016; | |
| if (this.diveTimer <= 0) { | |
| // Random chance to dive (20% per check when timer expires) | |
| if (Math.random() < 0.2) { | |
| this.isDiving = true; | |
| } else { | |
| this.diveTimer = Math.random() * 5 + 3; // Try again soon | |
| } | |
| } | |
| } | |
| this.formationTime += 0.02 * this.speedMultiplier; | |
| const prevX = this.mesh.position.x; | |
| const prevY = this.mesh.position.y; | |
| // Each shape type has its own movement pattern | |
| switch(this.pattern) { | |
| case 0: // Sphere - Simple circular pattern | |
| const t0 = this.formationTime + (this.enemyIndex * Math.PI / 3); | |
| this.mesh.position.x = this.centerX + Math.cos(t0) * this.radius; | |
| this.mesh.position.y = this.centerY + Math.sin(t0) * this.radius; | |
| break; | |
| case 1: // Tetrahedron - Figure-8 pattern | |
| const t1 = this.formationTime + (this.enemyIndex * Math.PI / 4); | |
| this.mesh.position.x = this.centerX + Math.sin(t1) * this.radius * 1.5; | |
| this.mesh.position.y = this.centerY + Math.sin(t1 * 2) * this.radius * 0.8; | |
| break; | |
| case 2: // Cube - Lissajous pattern (2:3 ratio) | |
| const t2 = this.formationTime + this.enemyIndex; | |
| this.mesh.position.x = this.centerX + Math.sin(2 * t2) * this.radius; | |
| this.mesh.position.y = this.centerY + Math.sin(3 * t2) * this.radius; | |
| break; | |
| case 3: // Octahedron - Trefoil knot | |
| const t3 = this.formationTime + (this.enemyIndex * Math.PI / 2); | |
| this.mesh.position.x = this.centerX + (Math.sin(t3) + 2 * Math.sin(2 * t3)) * this.radius * 0.4; | |
| this.mesh.position.y = this.centerY + (Math.cos(t3) - 2 * Math.cos(2 * t3)) * this.radius * 0.4; | |
| break; | |
| case 4: // Dodecahedron - Spiral with dive | |
| const t4 = this.formationTime + this.enemyIndex; | |
| const spiralR = this.radius * (0.5 + 0.5 * Math.sin(t4 * 0.5)); | |
| const dive = Math.sin(t4 * 0.3) * 1.5; | |
| this.mesh.position.x = this.centerX + Math.cos(t4 * 1.5) * spiralR; | |
| this.mesh.position.y = this.centerY + Math.sin(t4 * 1.5) * spiralR + dive; | |
| break; | |
| case 5: // Icosahedron - Space Invader zigzag pattern | |
| const t5 = this.formationTime; | |
| const zigzagSpeed = 0.8; | |
| const zigzagWidth = this.radius * 2; | |
| // Calculate horizontal position (zigzag back and forth) | |
| const cycleLength = 4; // Time for one complete left-right-down cycle | |
| const cycleProgress = (t5 % cycleLength) / cycleLength; | |
| // Determine if moving right or left | |
| const moveRight = Math.floor(t5 / cycleLength) % 2 === 0; | |
| const horizontalProgress = cycleProgress; | |
| if (moveRight) { | |
| this.mesh.position.x = this.centerX - zigzagWidth + (horizontalProgress * zigzagWidth * 2); | |
| } else { | |
| this.mesh.position.x = this.centerX + zigzagWidth - (horizontalProgress * zigzagWidth * 2); | |
| } | |
| // Gradual descent | |
| const dropAmount = Math.floor(t5 / cycleLength) * 0.3; | |
| this.mesh.position.y = this.centerY - dropAmount; | |
| // Keep rotation stiff/minimal for Space Invader feel | |
| this.mesh.rotation.x = 0; | |
| this.mesh.rotation.y += 0.01; | |
| break; | |
| } | |
| // Orient based on movement direction | |
| const dx = this.mesh.position.x - prevX; | |
| const dy = this.mesh.position.y - prevY; | |
| if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) { | |
| const angle = Math.atan2(dy, dx); | |
| this.mesh.rotation.z = angle + Math.PI / 2; | |
| } | |
| // Rotation on multiple axes for visual interest | |
| // Icosahedron (pattern 5) handles its own rotation | |
| if (this.pattern !== 5) { | |
| this.mesh.rotation.x += 0.02 * this.speedMultiplier; | |
| this.mesh.rotation.y += 0.015 * this.speedMultiplier; | |
| } | |
| } | |
| } | |
| class Bullet extends GameObject { | |
| constructor(position) { | |
| const geometry = new THREE.SphereGeometry(0.1); | |
| const material = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
| super(geometry, material, position); | |
| this.velocity.y = 0.3; | |
| } | |
| } | |
| class ExplosionDebris extends GameObject { | |
| constructor(position) { | |
| const size = 0.1 + Math.random() * 0.15; | |
| const geometry = new THREE.BoxGeometry(size, size, size); | |
| const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); | |
| super(geometry, material, position); | |
| // Random velocity in all directions | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 0.1 + Math.random() * 0.15; | |
| this.velocity.x = Math.cos(angle) * speed; | |
| this.velocity.y = Math.sin(angle) * speed; | |
| this.velocity.z = (Math.random() - 0.5) * 0.1; | |
| // Rotation velocities | |
| this.rotationVelocity = { | |
| x: (Math.random() - 0.5) * 0.2, | |
| y: (Math.random() - 0.5) * 0.2, | |
| z: (Math.random() - 0.5) * 0.2 | |
| }; | |
| this.lifetime = 0; | |
| this.maxLifetime = 1.5; | |
| } | |
| update() { | |
| super.update(); | |
| this.mesh.rotation.x += this.rotationVelocity.x; | |
| this.mesh.rotation.y += this.rotationVelocity.y; | |
| this.mesh.rotation.z += this.rotationVelocity.z; | |
| this.lifetime += 0.016; | |
| // Fade out | |
| const alpha = 1 - (this.lifetime / this.maxLifetime); | |
| this.mesh.material.opacity = alpha; | |
| return this.lifetime >= this.maxLifetime; | |
| } | |
| } | |
| class Game { | |
| constructor() { | |
| this.scene = new THREE.Scene(); | |
| this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| this.renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| this.renderer.setSize(window.innerWidth, window.innerHeight); | |
| document.body.appendChild(this.renderer.domElement); | |
| // Add lighting | |
| const ambientLight = new THREE.AmbientLight(0x333333); | |
| const mainLight = new THREE.DirectionalLight(0xffffff, 1); | |
| mainLight.position.set(5, 5, 7); | |
| this.scene.add(ambientLight, mainLight); | |
| // Create starfield | |
| this.stars = []; | |
| const starGeometry = new THREE.SphereGeometry(0.05); | |
| const starMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff }); | |
| for(let i = 0; i < 200; i++) { | |
| const star = new THREE.Mesh(starGeometry, starMaterial); | |
| star.position.set( | |
| (Math.random() - 0.5) * 40, | |
| (Math.random() - 0.5) * 40, | |
| (Math.random() - 0.5) * 20 | |
| ); | |
| this.stars.push(star); | |
| this.scene.add(star); | |
| } | |
| // Add nebula-like effects | |
| const nebulaGeometry = new THREE.PlaneGeometry(40, 40); | |
| const nebulaMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x4444ff, | |
| transparent: true, | |
| opacity: 0.1 | |
| }); | |
| for(let i = 0; i < 3; i++) { | |
| const nebula = new THREE.Mesh(nebulaGeometry, nebulaMaterial.clone()); | |
| nebula.position.z = -10 - i * 5; | |
| nebula.material.color.setHSL(Math.random(), 0.6, 0.6); | |
| this.scene.add(nebula); | |
| } | |
| this.camera.position.z = 10; | |
| this.camera.lookAt(0, 0, 0); | |
| this.player = new Player(); | |
| this.scene.add(this.player.mesh); | |
| this.player.mesh.position.y = -4; | |
| this.enemies = []; | |
| this.bullets = []; | |
| this.explosionDebris = []; | |
| this.score = 0; | |
| this.level = 1; | |
| this.lives = 3; | |
| this.lastScoreForExtraLife = 0; | |
| this.isGameOver = false; | |
| this.createEnemyWave(); | |
| this.setupControls(); | |
| this.animate(); | |
| } | |
| createEnemyWave() { | |
| // Determine which enemy types to spawn based on level | |
| const enemyTypeArray = Object.values(ENEMY_TYPES); | |
| const maxTypeIndex = Math.min(Math.floor((this.level - 1) / 2) + 1, enemyTypeArray.length); | |
| const availableTypes = enemyTypeArray.slice(0, maxTypeIndex); | |
| // Increase number of enemies with level | |
| const enemiesPerGroup = Math.min(3 + Math.floor(this.level / 3), 6); | |
| // Determine number of groups per type (can have multiple groups of same type) | |
| const groupsPerType = Math.min(1 + Math.floor(this.level / 4), 3); | |
| const allGroups = []; | |
| // Create groups for each available enemy type | |
| availableTypes.forEach((enemyType, typeIndex) => { | |
| for (let groupNum = 0; groupNum < groupsPerType; groupNum++) { | |
| // Generate unique parameters for each group's ping pong movement | |
| const levelSeed = this.level * 7 + typeIndex * 13 + groupNum * 19; | |
| // Horizontal bounds for ping pong (varies per group) | |
| const boundOffset = (levelSeed % 3) * 0.5; | |
| const leftBound = -4 + boundOffset; | |
| const rightBound = 4 - boundOffset; | |
| // Cycle length (time to traverse left to right or right to left) | |
| const cycleLength = 5 - (this.level % 3) * 0.5; // Faster at higher levels | |
| // Vertical wave parameters (adds slight bobbing motion) | |
| const freqY = 1 + ((levelSeed + 5) % 4) * 0.5; | |
| const phaseY = ((levelSeed + 3) % 8) * Math.PI / 4; | |
| const amplitudeY = 0.2 + (levelSeed % 3) * 0.1; | |
| allGroups.push({ | |
| type: enemyType, | |
| params: { | |
| leftBound, | |
| rightBound, | |
| cycleLength, | |
| freqY, | |
| phaseY, | |
| amplitudeY | |
| }, | |
| enemiesPerGroup | |
| }); | |
| } | |
| }); | |
| // Space out groups across the screen | |
| const spacing = 2.5; | |
| const totalWidth = (allGroups.length - 1) * spacing; | |
| allGroups.forEach((group, groupIndex) => { | |
| const baseX = (groupIndex * spacing) - (totalWidth / 2); | |
| for (let i = 0; i < group.enemiesPerGroup; i++) { | |
| const enemy = new Enemy({ | |
| x: baseX, | |
| y: 3, | |
| z: 0 | |
| }, group.type, i, this.level, group.params); | |
| this.enemies.push(enemy); | |
| this.scene.add(enemy.mesh); | |
| } | |
| }); | |
| } | |
| setupControls() { | |
| this.keys = {}; | |
| document.addEventListener('keydown', (e) => this.keys[e.code] = true); | |
| document.addEventListener('keyup', (e) => this.keys[e.code] = false); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.code === 'Space' && !this.keys.shooting) { | |
| this.shoot(); | |
| this.keys.shooting = true; | |
| } | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| if (e.code === 'Space') this.keys.shooting = false; | |
| }); | |
| document.getElementById('restart').addEventListener('click', () => this.restart()); | |
| } | |
| shoot() { | |
| if (this.isGameOver) return; | |
| const bullet = new Bullet({ | |
| x: this.player.mesh.position.x, | |
| y: this.player.mesh.position.y + 0.5, | |
| z: 0 | |
| }); | |
| this.bullets.push(bullet); | |
| this.scene.add(bullet.mesh); | |
| } | |
| handleMovement() { | |
| if (this.isGameOver) return; | |
| if (this.keys['ArrowLeft'] || this.keys['KeyA']) | |
| this.player.mesh.position.x = Math.max(-5, this.player.mesh.position.x - this.player.speed); | |
| if (this.keys['ArrowRight'] || this.keys['KeyD']) | |
| this.player.mesh.position.x = Math.min(5, this.player.mesh.position.x + this.player.speed); | |
| } | |
| checkCollisions() { | |
| // Check bullet-enemy collisions | |
| for (let i = this.bullets.length - 1; i >= 0; i--) { | |
| const bullet = this.bullets[i]; | |
| for (let j = this.enemies.length - 1; j >= 0; j--) { | |
| const enemy = this.enemies[j]; | |
| const dx = bullet.mesh.position.x - enemy.mesh.position.x; | |
| const dy = bullet.mesh.position.y - enemy.mesh.position.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 0.6) { | |
| this.scene.remove(bullet.mesh); | |
| this.scene.remove(enemy.mesh); | |
| this.bullets.splice(i, 1); | |
| this.enemies.splice(j, 1); | |
| this.score += enemy.type.points; | |
| document.getElementById('scoreValue').textContent = this.score; | |
| // Check for extra life | |
| if (Math.floor(this.score / 10000) > Math.floor(this.lastScoreForExtraLife / 10000)) { | |
| this.lives++; | |
| document.getElementById('livesValue').textContent = this.lives; | |
| this.lastScoreForExtraLife = this.score; | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| // Check player-enemy collisions | |
| if (!this.player.isInvulnerable && !this.isGameOver) { | |
| for (let i = this.enemies.length - 1; i >= 0; i--) { | |
| const enemy = this.enemies[i]; | |
| const dx = this.player.mesh.position.x - enemy.mesh.position.x; | |
| const dy = this.player.mesh.position.y - enemy.mesh.position.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 0.8) { | |
| this.playerHit(); | |
| this.scene.remove(enemy.mesh); | |
| this.enemies.splice(i, 1); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| playerHit() { | |
| this.lives--; | |
| document.getElementById('livesValue').textContent = this.lives; | |
| // Create explosion | |
| this.explodePlayer(); | |
| if (this.lives <= 0) { | |
| this.gameOver(); | |
| } else { | |
| // Respawn player after delay | |
| setTimeout(() => this.respawnPlayer(), 1500); | |
| } | |
| } | |
| explodePlayer() { | |
| const pos = this.player.mesh.position; | |
| // Hide player temporarily | |
| this.player.mesh.visible = false; | |
| // Create debris | |
| for (let i = 0; i < 20; i++) { | |
| const debris = new ExplosionDebris({ | |
| x: pos.x, | |
| y: pos.y, | |
| z: pos.z | |
| }); | |
| debris.mesh.material.transparent = true; | |
| this.explosionDebris.push(debris); | |
| this.scene.add(debris.mesh); | |
| } | |
| } | |
| respawnPlayer() { | |
| if (this.lives > 0) { | |
| this.player.mesh.position.set(0, -4, 0); | |
| this.player.mesh.visible = true; | |
| this.player.makeInvulnerable(); | |
| } | |
| } | |
| gameOver() { | |
| this.isGameOver = true; | |
| document.getElementById('gameOver').style.display = 'block'; | |
| } | |
| restart() { | |
| this.score = 0; | |
| this.level = 1; | |
| this.lives = 3; | |
| this.lastScoreForExtraLife = 0; | |
| this.isGameOver = false; | |
| document.getElementById('scoreValue').textContent = this.score; | |
| document.getElementById('levelValue').textContent = this.level; | |
| document.getElementById('livesValue').textContent = this.lives; | |
| document.getElementById('gameOver').style.display = 'none'; | |
| // Remove all enemies, bullets, and debris | |
| this.enemies.forEach(enemy => this.scene.remove(enemy.mesh)); | |
| this.bullets.forEach(bullet => this.scene.remove(bullet.mesh)); | |
| this.explosionDebris.forEach(debris => this.scene.remove(debris.mesh)); | |
| this.enemies = []; | |
| this.bullets = []; | |
| this.explosionDebris = []; | |
| // Reset player position | |
| this.player.mesh.position.set(0, -4, 0); | |
| this.player.mesh.visible = true; | |
| this.player.isInvulnerable = false; | |
| // Create new enemy wave | |
| this.createEnemyWave(); | |
| } | |
| animate() { | |
| requestAnimationFrame(() => this.animate()); | |
| // Parallax star movement | |
| this.stars.forEach(star => { | |
| star.position.z += 0.05; | |
| if(star.position.z > 10) star.position.z = -20; | |
| }); | |
| this.handleMovement(); | |
| // Update player | |
| this.player.update(0.016); | |
| this.enemies.forEach(enemy => enemy.update()); | |
| this.bullets.forEach(bullet => bullet.update()); | |
| // Update explosion debris | |
| for (let i = this.explosionDebris.length - 1; i >= 0; i--) { | |
| const shouldRemove = this.explosionDebris[i].update(); | |
| if (shouldRemove) { | |
| this.scene.remove(this.explosionDebris[i].mesh); | |
| this.explosionDebris.splice(i, 1); | |
| } | |
| } | |
| // Remove bullets that are out of bounds | |
| for (let i = this.bullets.length - 1; i >= 0; i--) { | |
| if (this.bullets[i].mesh.position.y > 6) { | |
| this.scene.remove(this.bullets[i].mesh); | |
| this.bullets.splice(i, 1); | |
| } | |
| } | |
| // Remove enemies that have descended too far (but not diving enemies) | |
| for (let i = this.enemies.length - 1; i >= 0; i--) { | |
| if (this.enemies[i].mesh.position.y < -5 && !this.enemies[i].isDiving && !this.enemies[i].isReturning) { | |
| this.scene.remove(this.enemies[i].mesh); | |
| this.enemies.splice(i, 1); | |
| } | |
| } | |
| this.checkCollisions(); | |
| // Check if level is complete | |
| if (this.enemies.length === 0 && !this.isGameOver) { | |
| this.level++; | |
| document.getElementById('levelValue').textContent = this.level; | |
| this.createEnemyWave(); | |
| } | |
| this.renderer.render(this.scene, this.camera); | |
| } | |
| } | |
| // Start the game | |
| window.addEventListener('load', () => { | |
| const game = new Game(); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| game.camera.aspect = window.innerWidth / window.innerHeight; | |
| game.camera.updateProjectionMatrix(); | |
| game.renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment