| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>大鱼吃小鱼</title> |
| <style> |
| :root { |
| --water-top: #006994; |
| --water-bottom: #001e36; |
| --ui-bg: rgba(255, 255, 255, 0.9); |
| --accent-color: #ff9800; |
| --text-color: #333; |
| } |
| |
| body { |
| margin: 0; |
| padding: 0; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| height: 100vh; |
| background-color: #222; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| overflow: hidden; |
| } |
| |
| |
| #game-wrapper { |
| position: relative; |
| width: 800px; |
| height: 600px; |
| box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); |
| border-radius: 12px; |
| overflow: hidden; |
| background: linear-gradient(to bottom, var(--water-top), var(--water-bottom)); |
| } |
| |
| canvas { |
| display: block; |
| width: 100%; |
| height: 100%; |
| } |
| |
| |
| #ui-panel { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| padding: 15px 20px; |
| box-sizing: border-box; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| pointer-events: none; |
| } |
| |
| .score-board { |
| background: rgba(0, 0, 0, 0.4); |
| color: white; |
| padding: 8px 16px; |
| border-radius: 20px; |
| font-size: 18px; |
| font-weight: bold; |
| pointer-events: auto; |
| } |
| |
| .controls-hint { |
| color: rgba(255, 255, 255, 0.7); |
| font-size: 14px; |
| pointer-events: auto; |
| } |
| |
| |
| #status-message { |
| position: absolute; |
| bottom: 20px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: rgba(0, 0, 0, 0.5); |
| color: #fff; |
| padding: 8px 20px; |
| border-radius: 4px; |
| font-size: 14px; |
| opacity: 0; |
| transition: opacity 0.3s; |
| pointer-events: none; |
| } |
| |
| |
| #modal-overlay { |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.6); |
| backdrop-filter: blur(4px); |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| z-index: 10; |
| } |
| |
| .modal-content { |
| background: white; |
| padding: 40px; |
| border-radius: 16px; |
| text-align: center; |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
| max-width: 400px; |
| animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| } |
| |
| @keyframes popIn { |
| from { transform: scale(0.8); opacity: 0; } |
| to { transform: scale(1); opacity: 1; } |
| } |
| |
| h1 { margin: 0 0 10px; color: var(--water-top); } |
| h2 { margin: 0 0 20px; color: var(--text-color); } |
| p { color: #666; margin-bottom: 30px; line-height: 1.5; } |
| |
| .btn { |
| background-color: var(--accent-color); |
| color: white; |
| border: none; |
| padding: 12px 30px; |
| font-size: 18px; |
| border-radius: 30px; |
| cursor: pointer; |
| transition: transform 0.1s, background-color 0.2s; |
| font-weight: bold; |
| outline: none; |
| } |
| |
| .btn:hover { background-color: #e68900; transform: scale(1.05); } |
| .btn:active { transform: scale(0.95); } |
| |
| .hidden { display: none !important; } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div id="game-wrapper"> |
| <canvas id="gameCanvas" width="800" height="600"></canvas> |
|
|
| |
| <div id="ui-panel"> |
| <div class="score-board">得分: <span id="score-display">0</span></div> |
| <div class="controls-hint">使用方向键 ↑ ↓ ← → 控制移动</div> |
| </div> |
|
|
| |
| <div id="status-message">游戏开始!</div> |
|
|
| |
| <div id="modal-overlay"> |
| <div class="modal-content" id="start-screen"> |
| <h1>大鱼吃小鱼</h1> |
| <p> |
| 吃掉比你小的鱼来成长。<br> |
| 躲避比你大的鱼!<br> |
| 目标:达到最大的体型(半径50)。 |
| </p> |
| <button class="btn" onclick="game.start()">开始游戏</button> |
| </div> |
|
|
| <div class="modal-content hidden" id="game-over-screen"> |
| <h2 style="color: #d32f2f;">被大鱼吃掉了!</h2> |
| <p>最终得分: <span id="final-score-loss">0</span></p> |
| <button class="btn" onclick="game.restart()">重新开始</button> |
| </div> |
|
|
| <div class="modal-content hidden" id="victory-screen"> |
| <h2 style="color: #388e3c;">成为最大的鱼!</h2> |
| <p>恭喜你通关成功!<br>你已经是海洋霸主了。</p> |
| <p>最终得分: <span id="final-score-win">0</span></p> |
| <button class="btn" onclick="game.restart()">再玩一次</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| |
| |
| const CONFIG = { |
| friction: 0.95, |
| playerBaseSpeed: 0.5, |
| playerMaxSpeed: 6, |
| enemyBaseSpeed: 2, |
| winRadius: 50, |
| spawnRate: 60, |
| colors: { |
| player: '#FF9800', |
| small: '#4CAF50', |
| big: '#F44336' |
| } |
| }; |
| |
| |
| |
| |
| class Fish { |
| constructor(x, y, radius, color, isPlayer = false) { |
| this.x = x; |
| this.y = y; |
| this.radius = radius; |
| this.color = color; |
| this.isPlayer = isPlayer; |
| this.angle = 0; |
| |
| |
| this.velocity = { x: 0, y: 0 }; |
| |
| if (isPlayer) { |
| this.speed = CONFIG.playerBaseSpeed; |
| } else { |
| this.speed = Math.random() * 1.5 + 0.5; |
| |
| this.angle = Math.random() * Math.PI * 2; |
| this.updateVelocity(); |
| } |
| } |
| |
| updateVelocity() { |
| |
| this.velocity.x = Math.cos(this.angle) * this.speed; |
| this.velocity.y = Math.sin(this.angle) * this.speed; |
| } |
| |
| move() { |
| this.x += this.velocity.x; |
| this.y += this.velocity.y; |
| } |
| |
| |
| checkBounds(width, height) { |
| if (this.x - this.radius < 0) { |
| this.x = this.radius; |
| this.angle = Math.PI - this.angle; |
| this.updateVelocity(); |
| } |
| if (this.x + this.radius > width) { |
| this.x = width - this.radius; |
| this.angle = Math.PI - this.angle; |
| this.updateVelocity(); |
| } |
| if (this.y - this.radius < 0) { |
| this.y = this.radius; |
| this.angle = -this.angle; |
| this.updateVelocity(); |
| } |
| if (this.y + this.radius > height) { |
| this.y = height - this.radius; |
| this.angle = -this.angle; |
| this.updateVelocity(); |
| } |
| } |
| |
| draw(ctx) { |
| ctx.save(); |
| ctx.translate(this.x, this.y); |
| ctx.rotate(this.angle); |
| |
| |
| ctx.beginPath(); |
| ctx.fillStyle = this.color; |
| ctx.arc(0, 0, this.radius, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.fillStyle = 'white'; |
| ctx.arc(this.radius * 0.4, -this.radius * 0.3, this.radius * 0.25, 0, Math.PI * 2); |
| ctx.fill(); |
| ctx.beginPath(); |
| ctx.fillStyle = 'black'; |
| ctx.arc(this.radius * 0.5, -this.radius * 0.3, this.radius * 0.1, 0, Math.PI * 2); |
| ctx.fill(); |
| |
| |
| ctx.beginPath(); |
| ctx.fillStyle = this.color; |
| ctx.moveTo(-this.radius * 0.8, 0); |
| ctx.lineTo(-this.radius * 1.5, -this.radius * 0.6); |
| ctx.lineTo(-this.radius * 1.5, this.radius * 0.6); |
| ctx.fill(); |
| |
| ctx.restore(); |
| } |
| |
| grow(amount) { |
| this.radius += amount; |
| } |
| } |
| |
| |
| |
| |
| class Game { |
| constructor() { |
| this.canvas = document.getElementById('gameCanvas'); |
| this.ctx = this.canvas.getContext('2d'); |
| this.width = this.canvas.width; |
| this.height = this.canvas.height; |
| |
| this.score = 0; |
| this.state = 'START'; |
| this.frameCount = 0; |
| |
| |
| this.player = null; |
| this.enemies = []; |
| this.particles = []; |
| |
| |
| this.keys = { |
| ArrowUp: false, |
| ArrowDown: false, |
| ArrowLeft: false, |
| ArrowRight: false |
| }; |
| |
| this.bindEvents(); |
| } |
| |
| bindEvents() { |
| window.addEventListener('keydown', (e) => { |
| if (this.keys.hasOwnProperty(e.code)) this.keys[e.code] = true; |
| }); |
| window.addEventListener('keyup', (e) => { |
| if (this.keys.hasOwnProperty(e.code)) this.keys[e.code] = false; |
| }); |
| } |
| |
| init() { |
| |
| this.player = new Fish(this.width / 2, this.height / 2, 10, CONFIG.colors.player, true); |
| this.enemies = []; |
| this.score = 0; |
| this.frameCount = 0; |
| this.updateUI(); |
| this.showMessage("准备开始..."); |
| } |
| |
| start() { |
| this.init(); |
| this.state = 'PLAYING'; |
| this.hideModals(); |
| this.animate(); |
| } |
| |
| restart() { |
| this.start(); |
| } |
| |
| spawnEnemy() { |
| |
| let x, y; |
| if (Math.random() < 0.5) { |
| x = Math.random() < 0.5 ? -30 : this.width + 30; |
| y = Math.random() * this.height; |
| } else { |
| x = Math.random() * this.width; |
| y = Math.random() < 0.5 ? -30 : this.height + 30; |
| } |
| |
| |
| |
| let sizeVariation = Math.random(); |
| let radius; |
| if (sizeVariation > 0.5) { |
| |
| radius = Math.max(5, this.player.radius * (0.5 + Math.random() * 0.4)); |
| } else { |
| |
| radius = this.player.radius * (1.2 + Math.random() * 1.5); |
| |
| if (radius > 100) radius = 100; |
| } |
| |
| |
| let color = CONFIG.colors.small; |
| if (radius > this.player.radius) color = CONFIG.colors.big; |
| |
| const enemy = new Fish(x, y, radius, color); |
| |
| |
| const angleToPlayer = Math.atan2(this.player.y - y, this.player.x - x); |
| |
| const variance = (Math.random() - 0.5); |
| enemy.angle = angleToPlayer + variance; |
| enemy.updateVelocity(); |
| |
| this.enemies.push(enemy); |
| } |
| |
| handleInput() { |
| if (this.state !== 'PLAYING') return; |
| |
| |
| if (this.keys.ArrowUp) { |
| this.player.velocity.y -= CONFIG.playerBaseSpeed; |
| this.player.angle = -Math.PI / 2; |
| } |
| if (this.keys.ArrowDown) { |
| this.player.velocity.y += CONFIG.playerBaseSpeed; |
| this.player.angle = Math.PI / 2; |
| } |
| if (this.keys.ArrowLeft) { |
| this.player.velocity.x -= CONFIG.playerBaseSpeed; |
| this.player.angle = Math.PI; |
| } |
| if (this.keys.ArrowRight) { |
| this.player.velocity.x += CONFIG.playerBaseSpeed; |
| this.player.angle = 0; |
| } |
| |
| |
| const speed = Math.sqrt(this.player.velocity.x**2 + this.player.velocity.y**2); |
| if (speed > CONFIG.playerMaxSpeed) { |
| const ratio = CONFIG.playerMaxSpeed / speed; |
| this.player.velocity.x *= ratio; |
| this.player.velocity.y *= ratio; |
| } |
| |
| |
| this.player.velocity.x *= CONFIG.friction; |
| this.player.velocity.y *= CONFIG.friction; |
| } |
| |
| checkCollisions() { |
| for (let i = this.enemies.length - 1; i >= 0; i--) { |
| const enemy = this.enemies[i]; |
| const dx = this.player.x - enemy.x; |
| const dy = this.player.y - enemy.y; |
| const distance = Math.sqrt(dx * dx + dy * dy); |
| |
| |
| if (distance < this.player.radius + enemy.radius) { |
| if (this.player.radius >= enemy.radius) { |
| |
| this.score += Math.floor(enemy.radius); |
| this.player.grow(enemy.radius * 0.05); |
| |
| |
| this.showMessage(`吞噬成功! +${Math.floor(enemy.radius)}分`); |
| |
| |
| this.enemies.splice(i, 1); |
| this.updateUI(); |
| |
| |
| if (this.player.radius >= CONFIG.winRadius) { |
| this.victory(); |
| } |
| |
| } else { |
| |
| this.gameOver(); |
| } |
| } |
| } |
| } |
| |
| update() { |
| if (this.state !== 'PLAYING') return; |
| |
| this.handleInput(); |
| |
| |
| this.player.move(); |
| this.player.checkBounds(this.width, this.height); |
| |
| |
| this.frameCount++; |
| if (this.frameCount % CONFIG.spawnRate === 0) { |
| this.spawnEnemy(); |
| } |
| |
| for (let i = this.enemies.length - 1; i >= 0; i--) { |
| this.enemies[i].move(); |
| |
| |
| this.enemies[i].checkBounds(this.width, this.height); |
| } |
| |
| this.checkCollisions(); |
| } |
| |
| draw() { |
| |
| this.ctx.clearRect(0, 0, this.width, this.height); |
| |
| |
| if (this.player) this.player.draw(this.ctx); |
| |
| |
| this.enemies.forEach(enemy => enemy.draw(this.ctx)); |
| } |
| |
| animate() { |
| if (this.state === 'PLAYING') { |
| this.update(); |
| this.draw(); |
| requestAnimationFrame(() => this.animate()); |
| } |
| } |
| |
| updateUI() { |
| document.getElementById('score-display').innerText = this.score; |
| } |
| |
| showMessage(text) { |
| const el = document.getElementById('status-message'); |
| el.innerText = text; |
| el.style.opacity = 1; |
| |
| |
| if (this.msgTimeout) clearTimeout(this.msgTimeout); |
| |
| this.msgTimeout = setTimeout(() => { |
| el.style.opacity = 0; |
| }, 2000); |
| } |
| |
| gameOver() { |
| this.state = 'GAME_OVER'; |
| document.getElementById('final-score-loss').innerText = this.score; |
| this.showModal('game-over-screen'); |
| this.showMessage("被大鱼吃掉了!游戏失败"); |
| } |
| |
| victory() { |
| this.state = 'VICTORY'; |
| document.getElementById('final-score-win').innerText = this.score; |
| this.showModal('victory-screen'); |
| this.showMessage("成为最大的鱼!通关成功"); |
| } |
| |
| hideModals() { |
| document.querySelectorAll('.modal-content').forEach(el => el.classList.add('hidden')); |
| document.getElementById('modal-overlay').classList.add('hidden'); |
| } |
| |
| showModal(id) { |
| this.hideModals(); |
| document.getElementById(id).classList.remove('hidden'); |
| document.getElementById('modal-overlay').classList.remove('hidden'); |
| } |
| } |
| |
| |
| const game = new Game(); |
| |
| </script> |
| </body> |
| </html> |