Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>射他一脸!Ejection FACE!</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| font-family: 'Press Start 2P', cursive; | |
| background-color: #000; | |
| color: white; | |
| } | |
| #game-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| #starfield { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| } | |
| .star { | |
| position: absolute; | |
| width: 2px; | |
| height: 2px; | |
| background-color: white; | |
| border-radius: 50%; | |
| } | |
| .target { | |
| position: absolute; | |
| width: 250px; | |
| height: 250px; | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| background-position: center; | |
| transition: transform 0.1s ease-out; | |
| z-index: 2; | |
| } | |
| .hit-effect { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| background-position: center; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| z-index: 3; | |
| } | |
| .hit-effect.active { | |
| opacity: 1; | |
| } | |
| #custom-cursor { | |
| position: absolute; | |
| width: 40px; | |
| height: 40px; | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| pointer-events: none; | |
| z-index: 999; | |
| transform-origin: center center; | |
| transform: rotate(0deg); | |
| } | |
| #score-display { | |
| position: absolute; | |
| top: 20px; | |
| left: 0; | |
| right: 0; | |
| text-align: center; | |
| font-size: 24px; | |
| color: #ff0; | |
| text-shadow: 0 0 10px #ff0, 0 0 20px #ff0; | |
| z-index: 10; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| #score-display.active { | |
| opacity: 1; | |
| } | |
| #hud { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 0; | |
| right: 0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 20px; | |
| z-index: 10; | |
| } | |
| .weapon { | |
| width: 60px; | |
| height: 60px; | |
| background-size: contain; | |
| background-repeat: no-repeat; | |
| background-position: center; | |
| border: 2px solid #555; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| position: relative; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| } | |
| .weapon:hover { | |
| transform: scale(1.1); | |
| border-color: #ff0; | |
| } | |
| .weapon.locked { | |
| filter: grayscale(100%); | |
| opacity: 0.5; | |
| } | |
| .weapon.selected { | |
| border-color: #ff0; | |
| box-shadow: 0 0 10px #ff0; | |
| } | |
| .weapon-price { | |
| position: absolute; | |
| top: -15px; | |
| right: -10px; | |
| background-color: #000; | |
| color: #ff0; | |
| padding: 2px 5px; | |
| border-radius: 10px; | |
| font-size: 10px; | |
| border: 1px solid #ff0; | |
| } | |
| .projectile { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| z-index: 5; | |
| transform-origin: center; | |
| } | |
| #hit-sound, #shoot-sound, #bg-music { | |
| display: none; | |
| } | |
| .hit-marker { | |
| position: absolute; | |
| width: 30px; | |
| height: 30px; | |
| background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ff0000"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'); | |
| background-size: contain; | |
| opacity: 0; | |
| animation: hitMarker 1s forwards; | |
| z-index: 4; | |
| } | |
| .hit-flash { | |
| animation: flash 0.3s; | |
| } | |
| @keyframes flash { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| 100% { opacity: 1; } | |
| } | |
| .screen-flash { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(255, 0, 0, 0.5); | |
| pointer-events: none; | |
| opacity: 0; | |
| z-index: 1000; | |
| } | |
| .screen-flash.active { | |
| animation: screenFlash 0.5s; | |
| } | |
| @keyframes screenFlash { | |
| 0% { opacity: 0.7; } | |
| 100% { opacity: 0; } | |
| } | |
| @keyframes hitMarker { | |
| 0% { transform: scale(0.5); opacity: 0; } | |
| 50% { transform: scale(1.5); opacity: 1; } | |
| 100% { transform: scale(1); opacity: 0; } | |
| } | |
| .ejection-text { | |
| position: absolute; | |
| font-size: 48px; | |
| color: #ff0; | |
| text-shadow: 0 0 10px #ff0, 0 0 20px #ff0; | |
| animation: ejectionText 1.5s forwards; | |
| z-index: 10; | |
| opacity: 0; | |
| } | |
| @keyframes ejectionText { | |
| 0% { transform: scale(0.5); opacity: 0; } | |
| 20% { transform: scale(1.2); opacity: 1; } | |
| 40% { transform: scale(0.9); } | |
| 60% { transform: scale(1.1); } | |
| 80% { transform: scale(0.95); } | |
| 100% { transform: scale(1); opacity: 0; } | |
| } | |
| #health-bar { | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| display: flex; | |
| gap: 5px; | |
| z-index: 10; | |
| } | |
| .health-point { | |
| width: 20px; | |
| height: 20px; | |
| background-color: #ff0000; | |
| border: 2px solid #fff; | |
| border-radius: 50%; | |
| } | |
| .health-point.lost { | |
| background-color: #333; | |
| } | |
| #game-over { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.8); | |
| display: none; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 100; | |
| } | |
| #game-over-text { | |
| font-size: 72px; | |
| color: #ff0000; | |
| text-shadow: 0 0 20px #ff0000; | |
| margin-bottom: 40px; | |
| text-align: center; | |
| } | |
| #retry-button { | |
| padding: 15px 30px; | |
| font-size: 24px; | |
| background-color: #ff0; | |
| color: #000; | |
| border: none; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-family: 'Press Start 2P', cursive; | |
| transition: all 0.2s; | |
| } | |
| #retry-button:hover { | |
| transform: scale(1.1); | |
| box-shadow: 0 0 20px #ff0; | |
| } | |
| .dancing { | |
| animation: dance 0.5s infinite alternate; | |
| } | |
| @keyframes dance { | |
| 0% { transform: rotate(-10deg); } | |
| 100% { transform: rotate(10deg); } | |
| } | |
| #center-dot { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background-color: rgba(255, 255, 255, 0.5); | |
| border-radius: 50%; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <!-- 其他元素... --> | |
| <div id="screen-flash" class="screen-flash"></div> | |
| <div id="starfield"></div> | |
| <div id="targets-container"></div> | |
| <div id="projectiles-container"></div> | |
| <div id="hit-markers-container"></div> | |
| <div id="score-display">射他一脸!Ejection FACE!</div> | |
| <div id="health-bar"></div> | |
| <div id="hud"> | |
| <div class="score-box">Hits: <span id="hit-count">0</span> | Score: <span id="score">0</span></div> | |
| <div class="weapon selected" data-weapon="basic" data-price="0"> | |
| <div class="weapon-price">FREE</div> | |
| </div> | |
| <div class="weapon locked" data-weapon="rapid" data-price="50"> | |
| <div class="weapon-price">50</div> | |
| </div> | |
| <div class="weapon locked" data-weapon="double" data-price="150"> | |
| <div class="weapon-price">150</div> | |
| </div> | |
| <div class="weapon locked" data-weapon="shotgun" data-price="300"> | |
| <div class="weapon-price">300</div> | |
| </div> | |
| <div class="weapon locked" data-weapon="bouncy" data-price="500"> | |
| <div class="weapon-price">500</div> | |
| </div> | |
| </div> | |
| <div id="custom-cursor"></div> | |
| <div id="center-dot"></div> | |
| <div id="game-over"> | |
| <div id="game-over-text">Ejection FAILED!<br>Loooser!</div> | |
| <button id="retry-button">RETRY</button> | |
| </div> | |
| </div> | |
| <audio id="hit-sound" preload="auto"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-positive-interface-beep-221.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="shoot-sound" preload="auto"> | |
| <source src="https://assets.mixkit.co/sfx/preview/mixkit-short-laser-gun-shot-1670.mp3" type="audio/mpeg"> | |
| </audio> | |
| <audio id="bg-music" loop> | |
| <source src="https://assets.mixkit.co/music/preview/mixkit-game-show-suspense-waiting-668.mp3" type="audio/mpeg"> | |
| </audio> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Game state | |
| const state = { | |
| score: 0, | |
| hits: 0, | |
| health: 10, | |
| targets: [], | |
| projectiles: [], | |
| stars: [], | |
| currentWeapon: 'basic', | |
| weapons: { | |
| basic: { price: 0, fireRate: 500, projectileCount: 1, spread: 0, bouncy: false }, | |
| rapid: { price: 50, fireRate: 250, projectileCount: 1, spread: 0, bouncy: false }, | |
| double: { price: 150, fireRate: 500, projectileCount: 2, spread: 10, bouncy: false }, | |
| shotgun: { price: 300, fireRate: 800, projectileCount: 5, spread: 20, bouncy: false }, | |
| bouncy: { price: 500, fireRate: 400, projectileCount: 1, spread: 0, bouncy: true } | |
| }, | |
| lastShot: 0, | |
| colors: ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'], | |
| currentColorIndex: 0, | |
| targetSpeed: 2, | |
| targetCount: 1, | |
| gameOver: false | |
| }; | |
| // DOM elements | |
| const gameContainer = document.getElementById('game-container'); | |
| const starfield = document.getElementById('starfield'); | |
| const targetsContainer = document.getElementById('targets-container'); | |
| const projectilesContainer = document.getElementById('projectiles-container'); | |
| const hitMarkersContainer = document.getElementById('hit-markers-container'); | |
| const scoreDisplay = document.getElementById('score-display'); | |
| const hitCountElement = document.getElementById('hit-count'); | |
| const scoreElement = document.getElementById('score'); | |
| const customCursor = document.getElementById('custom-cursor'); | |
| const hitSound = document.getElementById('hit-sound'); | |
| const shootSound = document.getElementById('shoot-sound'); | |
| const bgMusic = document.getElementById('bg-music'); | |
| const weaponElements = document.querySelectorAll('.weapon'); | |
| const healthBar = document.getElementById('health-bar'); | |
| const gameOverScreen = document.getElementById('game-over'); | |
| const gameOverText = document.getElementById('game-over-text'); | |
| const retryButton = document.getElementById('retry-button'); | |
| // Initialize game | |
| initHealthBar(); | |
| initStarfield(); | |
| createTargets(state.targetCount); | |
| setupCursor(); | |
| setupWeapons(); | |
| playMusic(); | |
| // Event listeners | |
| gameContainer.addEventListener('mousemove', handleMouseMove); | |
| gameContainer.addEventListener('click', handleShoot); | |
| retryButton.addEventListener('click', resetGame); | |
| // Game loop | |
| function gameLoop() { | |
| if (!state.gameOver) { | |
| updateTargets(); | |
| updateProjectiles(); | |
| checkCollisions(); | |
| checkCenterCollision(); | |
| requestAnimationFrame(gameLoop); | |
| } | |
| } | |
| gameLoop(); | |
| // Initialize health bar | |
| function initHealthBar() { | |
| healthBar.innerHTML = ''; | |
| for (let i = 0; i < 10; i++) { | |
| const healthPoint = document.createElement('div'); | |
| healthPoint.className = 'health-point'; | |
| healthBar.appendChild(healthPoint); | |
| } | |
| } | |
| // Update health display | |
| function updateHealth() { | |
| const healthPoints = document.querySelectorAll('.health-point'); | |
| healthPoints.forEach((point, index) => { | |
| if (index < state.health) { | |
| point.classList.remove('lost'); | |
| } else { | |
| point.classList.add('lost'); | |
| } | |
| }); | |
| } | |
| // Initialize starfield | |
| function initStarfield() { | |
| const starCount = Math.floor(window.innerWidth * window.innerHeight / 1000); | |
| for (let i = 0; i < starCount; i++) { | |
| createStar(); | |
| } | |
| } | |
| function createStar() { | |
| const star = document.createElement('div'); | |
| star.className = 'star'; | |
| const x = Math.random() * window.innerWidth; | |
| const y = Math.random() * window.innerHeight; | |
| const size = Math.random() * 3; | |
| const opacity = Math.random(); | |
| star.style.left = `${x}px`; | |
| star.style.top = `${y}px`; | |
| star.style.width = `${size}px`; | |
| star.style.height = `${size}px`; | |
| star.style.opacity = opacity; | |
| starfield.appendChild(star); | |
| // Animate star (parallax effect) | |
| const speed = 0.5 + Math.random() * 2; | |
| function animateStar() { | |
| const currentX = parseFloat(star.style.left); | |
| const currentY = parseFloat(star.style.top); | |
| // Move star outward from center | |
| const centerX = window.innerWidth / 2; | |
| const centerY = window.innerHeight / 2; | |
| let dx = currentX - centerX; | |
| let dy = currentY - centerY; | |
| // Normalize direction vector | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| dx /= distance; | |
| dy /= distance; | |
| // Move star | |
| star.style.left = `${currentX + dx * speed}px`; | |
| star.style.top = `${currentY + dy * speed}px`; | |
| // Reset star if it goes out of bounds | |
| if (parseFloat(star.style.left) < 0 || parseFloat(star.style.left) > window.innerWidth || | |
| parseFloat(star.style.top) < 0 || parseFloat(star.style.top) > window.innerHeight) { | |
| star.style.left = `${Math.random() * window.innerWidth}px`; | |
| star.style.top = `${Math.random() * window.innerHeight}px`; | |
| } | |
| requestAnimationFrame(animateStar); | |
| } | |
| animateStar(); | |
| } | |
| // Create targets | |
| function createTargets(count) { | |
| targetsContainer.innerHTML = ''; | |
| state.targets = []; | |
| for (let i = 0; i < count; i++) { | |
| createTarget(i); | |
| } | |
| } | |
| function createTarget(index) { | |
| const target = document.createElement('div'); | |
| target.className = 'target'; | |
| // Use huggingface emoji as target | |
| target.style.backgroundImage = 'url("https://huggingface.co/front/assets/huggingface_logo-noborder.svg")'; | |
| // Position randomly but ensure it's fully visible | |
| const maxX = window.innerWidth - 250; | |
| const maxY = window.innerHeight - 250; | |
| const x = Math.max(0, Math.min(maxX, Math.random() * maxX)); | |
| const y = Math.max(0, Math.min(maxY, Math.random() * maxY)); | |
| target.style.left = `${x}px`; | |
| target.style.top = `${y}px`; | |
| // Add hit effect | |
| const hitEffect = document.createElement('div'); | |
| hitEffect.className = 'hit-effect'; | |
| hitEffect.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><circle cx=\'50\' cy=\'50\' r=\'40\' fill=\'' + state.colors[state.currentColorIndex] + '\' opacity=\'0.7\'/></svg>")'; | |
| target.appendChild(hitEffect); | |
| targetsContainer.appendChild(target); | |
| // Store target data | |
| state.targets.push({ | |
| element: target, | |
| hitEffect: hitEffect, | |
| x: x, | |
| y: y, | |
| vx: (Math.random() - 0.5) * state.targetSpeed, | |
| vy: (Math.random() - 0.5) * state.targetSpeed, | |
| index: index | |
| }); | |
| } | |
| // Update targets position | |
| function updateTargets() { | |
| state.targets.forEach(target => { | |
| // Update position | |
| target.x += target.vx; | |
| target.y += target.vy; | |
| // Bounce off edges | |
| if (target.x <= 0 || target.x >= window.innerWidth - 250) { | |
| target.vx = -target.vx * (0.9 + Math.random() * 0.2); | |
| } | |
| if (target.y <= 0 || target.y >= window.innerHeight - 250) { | |
| target.vy = -target.vy * (0.9 + Math.random() * 0.2); | |
| } | |
| // Random direction changes | |
| if (Math.random() < 0.02) { | |
| target.vx += (Math.random() - 0.5) * 0.5; | |
| target.vy += (Math.random() - 0.5) * 0.5; | |
| // Limit speed | |
| const speed = Math.sqrt(target.vx * target.vx + target.vy * target.vy); | |
| const maxSpeed = state.targetSpeed * 1.5; | |
| if (speed > maxSpeed) { | |
| target.vx = (target.vx / speed) * maxSpeed; | |
| target.vy = (target.vy / speed) * maxSpeed; | |
| } | |
| } | |
| // Apply position | |
| target.element.style.left = `${target.x}px`; | |
| target.element.style.top = `${target.y}px`; | |
| }); | |
| } | |
| // Setup custom cursor | |
| function setupCursor() { | |
| customCursor.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[state.currentColorIndex] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/></svg>")'; | |
| customCursor.style.display = 'block'; | |
| } | |
| function handleMouseMove(e) { | |
| customCursor.style.left = `${e.clientX - 20}px`; | |
| customCursor.style.top = `${e.clientY - 20}px`; | |
| // Rotate cursor slightly for visual feedback | |
| const rotation = Math.sin(Date.now() / 200) * 5; | |
| customCursor.style.transform = `rotate(${rotation}deg)`; | |
| } | |
| // Handle shooting | |
| function handleShoot(e) { | |
| if (state.gameOver) return; | |
| const now = Date.now(); | |
| const weapon = state.weapons[state.currentWeapon]; | |
| if (now - state.lastShot < weapon.fireRate) return; | |
| state.lastShot = now; | |
| // Play shoot sound | |
| shootSound.currentTime = 0; | |
| shootSound.play(); | |
| // Create projectiles based on weapon type | |
| for (let i = 0; i < weapon.projectileCount; i++) { | |
| createProjectile(e.clientX, e.clientY, i); | |
| } | |
| // Animate cursor | |
| customCursor.style.transform = 'scale(0.8)'; | |
| setTimeout(() => { | |
| customCursor.style.transform = 'scale(1)'; | |
| }, 100); | |
| } | |
| function createProjectile(mouseX, mouseY, index) { | |
| const projectile = document.createElement('div'); | |
| projectile.className = 'projectile'; | |
| // Calculate direction from center to mouse | |
| const centerX = window.innerWidth / 2; | |
| const centerY = window.innerHeight / 2; | |
| let dx = mouseX - centerX; | |
| let dy = mouseY - centerY; | |
| // Normalize direction vector | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| dx /= distance; | |
| dy /= distance; | |
| // Apply weapon spread | |
| const weapon = state.weapons[state.currentWeapon]; | |
| const spreadAngle = (index - (weapon.projectileCount - 1) / 2) * weapon.spread * (Math.PI / 180); | |
| // Rotate direction by spread angle | |
| const cos = Math.cos(spreadAngle); | |
| const sin = Math.sin(spreadAngle); | |
| const tx = dx * cos - dy * sin; | |
| const ty = dx * sin + dy * cos; | |
| dx = tx; | |
| dy = ty; | |
| // Set projectile color | |
| projectile.style.backgroundColor = state.colors[state.currentColorIndex]; | |
| // Set initial position at center | |
| projectile.style.left = `${centerX}px`; | |
| projectile.style.top = `${centerY}px`; | |
| projectilesContainer.appendChild(projectile); | |
| // Store projectile data | |
| const projectileData = { | |
| element: projectile, | |
| x: centerX, | |
| y: centerY, | |
| vx: dx * 10, | |
| vy: dy * 10, | |
| bounces: 0, | |
| maxBounces: weapon.bouncy ? 3 : 0 | |
| }; | |
| state.projectiles.push(projectileData); | |
| } | |
| // Update projectiles position | |
| function updateProjectiles() { | |
| state.projectiles.forEach((projectile, index) => { | |
| // Update position | |
| projectile.x += projectile.vx; | |
| projectile.y += projectile.vy; | |
| // Bounce off edges if bouncy | |
| if (projectile.maxBounces > 0) { | |
| if (projectile.x <= 0 || projectile.x >= window.innerWidth) { | |
| projectile.vx = -projectile.vx; | |
| projectile.bounces++; | |
| projectile.x = Math.max(0, Math.min(window.innerWidth, projectile.x)); | |
| } | |
| if (projectile.y <= 0 || projectile.y >= window.innerHeight) { | |
| projectile.vy = -projectile.vy; | |
| projectile.bounces++; | |
| projectile.y = Math.max(0, Math.min(window.innerHeight, projectile.y)); | |
| } | |
| } | |
| // Apply position | |
| projectile.element.style.left = `${projectile.x}px`; | |
| projectile.element.style.top = `${projectile.y}px`; | |
| // Remove if out of bounds or max bounces reached | |
| if (projectile.x < 0 || projectile.x > window.innerWidth || | |
| projectile.y < 0 || projectile.y > window.innerHeight || | |
| projectile.bounces > projectile.maxBounces) { | |
| projectile.element.remove(); | |
| state.projectiles.splice(index, 1); | |
| } | |
| }); | |
| } | |
| // Check for collisions with targets | |
| function checkCollisions() { | |
| state.projectiles.forEach((projectile, pIndex) => { | |
| state.targets.forEach(target => { | |
| // Simple circle-rectangle collision detection | |
| const targetRect = { | |
| x: target.x, | |
| y: target.y, | |
| width: 250, | |
| height: 250 | |
| }; | |
| // Find closest point on rectangle to circle | |
| const closestX = Math.max(targetRect.x, Math.min(projectile.x, targetRect.x + targetRect.width)); | |
| const closestY = Math.max(targetRect.y, Math.min(projectile.y, targetRect.y + targetRect.height)); | |
| // Calculate distance | |
| const distanceX = projectile.x - closestX; | |
| const distanceY = projectile.y - closestY; | |
| const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); | |
| if (distance < 10) { // Projectile radius | |
| // Hit! | |
| handleHit(target, projectile); | |
| // Remove projectile | |
| projectile.element.remove(); | |
| state.projectiles.splice(pIndex, 1); | |
| } | |
| }); | |
| }); | |
| } | |
| // Check for collisions with center | |
| function checkCenterCollision() { | |
| if (state.gameOver) return; | |
| const centerX = window.innerWidth / 2; | |
| const centerY = window.innerHeight / 2; | |
| const centerRadius = 10; | |
| state.targets.forEach(target => { | |
| // Check if any part of target is within center radius | |
| const targetCenterX = target.x + 125; | |
| const targetCenterY = target.y + 125; | |
| const distance = Math.sqrt( | |
| Math.pow(targetCenterX - centerX, 2) + | |
| Math.pow(targetCenterY - centerY, 2) | |
| ); | |
| if (distance < centerRadius + 125) { | |
| handleCenterHit(); | |
| } | |
| }); | |
| } | |
| // 修改handleCenterHit函数 | |
| function handleHit(target, projectile) { | |
| // Play hit sound | |
| hitSound.currentTime = 0; | |
| hitSound.play(); | |
| // 添加击中闪烁效果 | |
| target.element.classList.add('hit-flash'); | |
| setTimeout(() => { | |
| target.element.classList.remove('hit-flash'); | |
| }, 300); | |
| // 其他原有代码保持不变... | |
| // Show hit effect | |
| target.hitEffect.classList.add('active'); | |
| setTimeout(() => { | |
| target.hitEffect.classList.remove('active'); | |
| }, 500); | |
| // ...其余原有代码 | |
| } | |
| // 修改handleCenterHit函数,添加全屏红色闪烁 | |
| function handleCenterHit() { | |
| if (state.health > 0) { | |
| state.health--; | |
| updateHealth(); | |
| // 全屏红色闪烁 | |
| const screenFlash = document.getElementById('screen-flash'); | |
| screenFlash.classList.add('active'); | |
| setTimeout(() => { | |
| screenFlash.classList.remove('active'); | |
| }, 500); | |
| // 将所有目标弹出到画面边缘 | |
| state.targets.forEach(target => { | |
| // 计算从中心到目标的向量 | |
| const centerX = window.innerWidth / 2; | |
| const centerY = window.innerHeight / 2; | |
| const targetCenterX = target.x + 125; | |
| const targetCenterY = target.y + 125; | |
| let dx = targetCenterX - centerX; | |
| let dy = targetCenterY - centerY; | |
| // 标准化向量并放大(弹出效果) | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| dx /= distance; | |
| dy /= distance; | |
| // 设置很大的速度向边缘移动 | |
| target.vx = dx * 20; | |
| target.vy = dy * 20; | |
| // 添加闪烁效果 | |
| target.element.classList.add('hit-flash'); | |
| setTimeout(() => { | |
| target.element.classList.remove('hit-flash'); | |
| }, 300); | |
| }); | |
| if (state.health <= 0) { | |
| // 延迟游戏结束,让玩家看到所有目标被弹出的效果 | |
| setTimeout(() => { | |
| gameOver(); | |
| }, 500); | |
| } | |
| } | |
| } | |
| function handleHit(target, projectile) { | |
| // Play hit sound | |
| hitSound.currentTime = 0; | |
| hitSound.play(); | |
| // Show hit effect | |
| target.hitEffect.classList.add('active'); | |
| setTimeout(() => { | |
| target.hitEffect.classList.remove('active'); | |
| }, 500); | |
| // Create hit marker | |
| const hitMarker = document.createElement('div'); | |
| hitMarker.className = 'hit-marker'; | |
| hitMarker.style.left = `${projectile.x - 15}px`; | |
| hitMarker.style.top = `${projectile.y - 15}px`; | |
| hitMarkersContainer.appendChild(hitMarker); | |
| setTimeout(() => { | |
| hitMarker.remove(); | |
| }, 1000); | |
| // Update score | |
| state.hits++; | |
| state.score += 10; | |
| hitCountElement.textContent = state.hits; | |
| scoreElement.textContent = state.score; | |
| // Show "Ejection FACE!" text | |
| const ejectionText = document.createElement('div'); | |
| ejectionText.className = 'ejection-text'; | |
| ejectionText.textContent = 'Ejection FACE!'; | |
| ejectionText.style.left = `${window.innerWidth / 2 - 150}px`; | |
| ejectionText.style.top = `${window.innerHeight / 2 - 50}px`; | |
| gameContainer.appendChild(ejectionText); | |
| setTimeout(() => { | |
| ejectionText.remove(); | |
| }, 1500); | |
| // Change target direction on hit | |
| target.vx = (Math.random() - 0.5) * state.targetSpeed * 2; | |
| target.vy = (Math.random() - 0.5) * state.targetSpeed * 2; | |
| // Increase difficulty every 10 hits | |
| if (state.hits % 10 === 0) { | |
| state.targetSpeed += 0.2; | |
| // Make targets change direction more often | |
| state.targets.forEach(t => { | |
| t.vx *= 1.1; | |
| t.vy *= 1.1; | |
| }); | |
| } | |
| // Add new target every 10 hits | |
| if (state.hits % 10 === 0) { | |
| state.targetCount++; | |
| state.currentColorIndex = (state.currentColorIndex + 1) % state.colors.length; | |
| setupCursor(); | |
| createTargets(state.targetCount); | |
| } | |
| // Unlock weapons if enough score | |
| weaponElements.forEach(weapon => { | |
| const weaponType = weapon.dataset.weapon; | |
| const price = parseInt(weapon.dataset.price); | |
| if (state.score >= price) { | |
| weapon.classList.remove('locked'); | |
| } | |
| }); | |
| } | |
| // Game over | |
| function gameOver() { | |
| state.gameOver = true; | |
| // Make targets dance | |
| state.targets.forEach(target => { | |
| target.element.classList.add('dancing'); | |
| }); | |
| // Show game over screen | |
| gameOverScreen.style.display = 'flex'; | |
| } | |
| // Reset game | |
| function resetGame() { | |
| // Reset game state | |
| state.score = 0; | |
| state.hits = 0; | |
| state.health = 10; | |
| state.targets = []; | |
| state.projectiles = []; | |
| state.currentWeapon = 'basic'; | |
| state.currentColorIndex = 0; | |
| state.targetSpeed = 2; | |
| state.targetCount = 1; | |
| state.gameOver = false; | |
| // Reset UI | |
| hitCountElement.textContent = '0'; | |
| scoreElement.textContent = '0'; | |
| gameOverScreen.style.display = 'none'; | |
| // Clear containers | |
| targetsContainer.innerHTML = ''; | |
| projectilesContainer.innerHTML = ''; | |
| hitMarkersContainer.innerHTML = ''; | |
| // Reset weapons | |
| weaponElements.forEach(weapon => { | |
| if (weapon.dataset.weapon !== 'basic') { | |
| weapon.classList.add('locked'); | |
| } | |
| weapon.classList.remove('selected'); | |
| }); | |
| document.querySelector('.weapon[data-weapon="basic"]').classList.add('selected'); | |
| // Reset health | |
| initHealthBar(); | |
| // Start new game | |
| createTargets(state.targetCount); | |
| gameLoop(); | |
| } | |
| // Setup weapon selection | |
| function setupWeapons() { | |
| weaponElements.forEach(weapon => { | |
| const weaponType = weapon.dataset.weapon; | |
| // Set weapon icons | |
| switch (weaponType) { | |
| case 'basic': | |
| weapon.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[0] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/></svg>")'; | |
| break; | |
| case 'rapid': | |
| weapon.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[1] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/><circle cx=\'50\' cy=\'50\' r=\'15\' fill=\'none\' stroke=\'' + state.colors[1] + '\' stroke-width=\'3\'/></svg>")'; | |
| break; | |
| case 'double': | |
| weapon.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[2] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/><path d=\'M30 30 L70 70 M30 70 L70 30\' stroke=\'' + state.colors[2] + '\' stroke-width=\'3\'/></svg>")'; | |
| break; | |
| case 'shotgun': | |
| weapon.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[3] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/><path d=\'M50 30 L30 50 M50 30 L70 50 M50 70 L30 50 M50 70 L70 50\' stroke=\'' + state.colors[3] + '\' stroke-width=\'3\'/></svg>")'; | |
| break; | |
| case 'bouncy': | |
| weapon.style.backgroundImage = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 100 100\'><path d=\'M50 10 L50 90 M10 50 L90 50\' stroke=\'' + state.colors[4] + '\' stroke-width=\'5\' stroke-linecap=\'round\'/><path d=\'M20 20 Q50 30 80 20 Q85 50 80 80 Q50 70 20 80 Q15 50 20 20\' fill=\'none\' stroke=\'' + state.colors[4] + '\' stroke-width=\'3\'/></svg>")'; | |
| break; | |
| } | |
| weapon.addEventListener('click', () => { | |
| if (weapon.classList.contains('locked')) return; | |
| // Select weapon | |
| weaponElements.forEach(w => w.classList.remove('selected')); | |
| weapon.classList.add('selected'); | |
| state.currentWeapon = weaponType; | |
| setupCursor(); | |
| }); | |
| }); | |
| } | |
| // Play background music | |
| function playMusic() { | |
| bgMusic.volume = 0.3; | |
| bgMusic.play(); | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=GuangyuanSD/ejectionface" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |