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; } | |
} | |
/* 添加在CSS部分 */ | |
@keyframes newTargetFlash { | |
0% { transform: scale(0.5); opacity: 0; } | |
50% { transform: scale(1.2); opacity: 1; } | |
100% { transform: scale(1); opacity: 1; } | |
} | |
.new-target-flash { | |
animation: newTargetFlash 0.5s forwards; | |
} | |
.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="300"> | |
<div class="weapon-price">300</div> | |
</div> | |
<div class="weapon locked" data-weapon="shotgun" data-price="800"> | |
<div class="weapon-price">800</div> | |
</div> | |
<div class="weapon locked" data-weapon="bouncy" data-price="1600"> | |
<div class="weapon-price">1600</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: 300, fireRate: 300, projectileCount: 2, spread: 10, bouncy: false }, | |
shotgun: { price: 800, fireRate: 500, projectileCount: 5, spread: 20, bouncy: false }, | |
bouncy: { price: 1600, fireRate: 100, projectileCount: 18, spread: 360, bouncy: true } | |
}, | |
lastShot: 0, | |
colors: ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'], | |
currentColorIndex: 0, | |
targetSpeed: 2, | |
targetCount: 1, | |
gameOver: false, | |
knockbackDistance: -30 // Added knockback distance | |
}; | |
// 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) { | |
// Flash screen when adding new targets | |
const screenFlash = document.createElement('div'); | |
screenFlash.className = 'screen-flash'; | |
screenFlash.style.backgroundColor = state.colors[state.currentColorIndex]; | |
document.body.appendChild(screenFlash); | |
setTimeout(() => { | |
screenFlash.remove(); | |
}, 300); | |
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 and not near center | |
const maxX = window.innerWidth - 250; | |
const maxY = window.innerHeight - 250; | |
const centerX = window.innerWidth / 2; | |
const centerY = window.innerHeight / 2; | |
const minDistanceFromCenter = 300; // Minimum distance from center | |
let x, y; | |
let attempts = 0; | |
const maxAttempts = 100; | |
do { | |
x = Math.max(0, Math.min(maxX, Math.random() * maxX)); | |
y = Math.max(0, Math.min(maxY, Math.random() * maxY)); | |
attempts++; | |
// Check distance from center | |
const distance = Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)); | |
// If we've tried too many times, just place it anywhere | |
if (attempts >= maxAttempts) break; | |
} while (Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) < minDistanceFromCenter); | |
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 | |
if (state.currentWeapon === 'bouncy') { | |
// Special handling for bouncy weapon - 360 degree spread | |
for (let i = 0; i < weapon.projectileCount; i++) { | |
const angle = (i / weapon.projectileCount) * Math.PI * 2; | |
createBouncyProjectile(angle); | |
} | |
} else { | |
// Normal weapons | |
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 createBouncyProjectile(angle) { | |
const projectile = document.createElement('div'); | |
projectile.className = 'projectile'; | |
const centerX = window.innerWidth / 2; | |
const centerY = window.innerHeight / 2; | |
// 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); | |
// Calculate direction based on angle | |
const dx = Math.cos(angle); | |
const dy = Math.sin(angle); | |
// Store projectile data with higher speed for bouncy weapon | |
const projectileData = { | |
element: projectile, | |
x: centerX, | |
y: centerY, | |
vx: dx * 15, // Faster speed for bouncy weapon | |
vy: dy * 15, // Faster speed for bouncy weapon | |
bounces: 0, | |
maxBounces: 3 | |
}; | |
state.projectiles.push(projectileData); | |
} | |
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 = 1; | |
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); | |
// 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 with increased knockback | |
const knockbackDirectionX = (projectile.x - (target.x + 125)) / 125; | |
const knockbackDirectionY = (projectile.y - (target.y + 125)) / 125; | |
// Apply knockback with increasing distance based on hits | |
const knockbackMultiplier = state.knockbackDistance + (state.hits * 0.5); | |
target.vx = knockbackDirectionX * knockbackMultiplier; | |
target.vy = knockbackDirectionY * knockbackMultiplier; | |
// 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'); | |
} | |
}); | |
} | |
// 修改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); | |
} | |
} | |
} | |
// 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; | |
state.knockbackDistance = -30; | |
// 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> |