Files
whack-a-mole-game/src/modules/game.tsx
2025-10-18 00:36:49 +08:00

408 lines
11 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import Phaser from 'phaser';
console.log('Phaser version:', basename);
const base = basename || '';
export const Game = () => {
const gameRef = useRef<Phaser.Game | null>(null);
const [score, setScore] = useState(0);
const [timeLeft, setTimeLeft] = useState(30);
const [gameStarted, setGameStarted] = useState(false);
const [gameOver, setGameOver] = useState(false);
useEffect(() => {
if (!gameStarted) return;
class GameScene extends Phaser.Scene {
private holes: Phaser.GameObjects.Image[] = [];
private moles: Phaser.GameObjects.Image[] = [];
private moleTimers: Phaser.Time.TimerEvent[] = [];
private currentScore = 0;
private gameTime = 30;
private timerText?: Phaser.GameObjects.Text;
private gameTimer?: Phaser.Time.TimerEvent;
constructor() {
super({ key: 'GameScene' });
}
preload() {
// 加载图片资源
this.load.image('hole', `${base}/assets/hole.png`);
this.load.image('mole', `${base}/assets/mole.png`);
// 加载音频资源
this.load.audio('hit', `${base}/assets/hit.wav`);
}
create() {
// 设置背景颜色
this.cameras.main.setBackgroundColor('#84f20b');
// 创建 3x3 的地洞网格
const cols = 3;
const rows = 3;
const spacing = 150;
const startX = 150;
const startY = 100;
// 限制图片大小
const holeSize = 80; // 地洞显示大小
const moleSize = 70; // 地鼠显示大小
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const x = startX + col * spacing;
const y = startY + row * spacing;
// 创建地洞
const hole = this.add.image(x, y, 'hole');
hole.setDisplaySize(holeSize, holeSize);
this.holes.push(hole);
// 创建地鼠(初始隐藏)
const mole = this.add.image(x, y - 20, 'mole');
mole.setDisplaySize(moleSize, moleSize);
mole.setVisible(false);
mole.setInteractive({ cursor: 'pointer' });
// 点击地鼠的事件
mole.on('pointerdown', () => {
if (mole.visible) {
this.hitMole(mole);
}
});
this.moles.push(mole);
}
}
// 创建计时器文本
this.timerText = this.add.text(250, 30, `时间: ${this.gameTime}`, {
fontSize: '24px',
color: '#ffffff',
backgroundColor: '#333',
padding: { x: 10, y: 5 }
});
this.timerText.setOrigin(0.5);
// 启动游戏计时器
this.gameTimer = this.time.addEvent({
delay: 1000,
callback: this.updateTimer,
callbackScope: this,
loop: true
});
// 开始随机显示地鼠
this.startMoleSpawning();
}
startMoleSpawning() {
// 每隔一段时间随机显示地鼠
this.time.addEvent({
delay: 800,
callback: this.showRandomMole,
callbackScope: this,
loop: true
});
}
showRandomMole() {
if (this.gameTime <= 0) return;
// 随机选择一个地鼠
const availableMoles = this.moles.filter(mole => !mole.visible);
if (availableMoles.length === 0) return;
const randomMole = Phaser.Utils.Array.GetRandom(availableMoles);
randomMole.setVisible(true);
// 地鼠弹出动画
this.tweens.add({
targets: randomMole,
y: randomMole.y - 30,
duration: 200,
yoyo: false,
ease: 'Back.easeOut'
});
// 设置地鼠自动隐藏
const hideTimer = this.time.delayedCall(1500, () => {
this.hideMole(randomMole);
});
this.moleTimers.push(hideTimer);
}
hideMole(mole: Phaser.GameObjects.Image) {
if (!mole.visible) return;
this.tweens.add({
targets: mole,
y: mole.y + 30,
duration: 200,
ease: 'Back.easeIn',
onComplete: () => {
mole.setVisible(false);
}
});
}
hitMole(mole: Phaser.GameObjects.Image) {
this.currentScore += 10;
setScore(this.currentScore);
// 播放击中音效
this.sound.play('hit');
const moleSize = 70; // 地鼠显示大小
// 击中效果 - 缩小再恢复
this.tweens.add({
targets: mole,
displayWidth: moleSize * 0.7,
displayHeight: moleSize * 0.7,
duration: 100,
yoyo: true,
ease: 'Power2',
onComplete: () => {
mole.setDisplaySize(moleSize, moleSize);
this.hideMole(mole);
}
});
// 显示得分文本
const scoreText = this.add.text(mole.x, mole.y - 50, '+10', {
fontSize: '28px',
color: '#ffeb3b',
fontStyle: 'bold'
});
scoreText.setOrigin(0.5);
this.tweens.add({
targets: scoreText,
y: scoreText.y - 50,
alpha: 0,
duration: 800,
onComplete: () => {
scoreText.destroy();
}
});
}
updateTimer() {
this.gameTime--;
setTimeLeft(this.gameTime);
if (this.timerText) {
this.timerText.setText(`时间: ${this.gameTime}`);
}
if (this.gameTime <= 0) {
this.endGame();
}
}
endGame() {
// 停止所有计时器
this.moleTimers.forEach(timer => timer.remove());
this.moleTimers = [];
if (this.gameTimer) {
this.gameTimer.remove();
}
// 隐藏所有地鼠
this.moles.forEach(mole => mole.setVisible(false));
// 显示游戏结束文本
const gameOverText = this.add.text(250, 250, '游戏结束!', {
fontSize: '48px',
color: '#ffffff',
backgroundColor: '#e91e63',
padding: { x: 20, y: 10 }
});
gameOverText.setOrigin(0.5);
const finalScoreText = this.add.text(250, 320, `最终得分: ${this.currentScore}`, {
fontSize: '32px',
color: '#ffffff',
backgroundColor: '#333',
padding: { x: 15, y: 8 }
});
finalScoreText.setOrigin(0.5);
setGameOver(true);
}
}
// 配置 Phaser 游戏
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 600,
height: 550,
parent: 'game-container',
backgroundColor: '#8bc34a',
scene: GameScene,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0, x: 0 },
debug: false
}
}
};
// 创建游戏实例
gameRef.current = new Phaser.Game(config);
// 清理函数
return () => {
if (gameRef.current) {
gameRef.current.destroy(true);
gameRef.current = null;
}
};
}, [gameStarted]);
const startGame = () => {
setScore(0);
setTimeLeft(30);
setGameStarted(true);
setGameOver(false);
};
const restartGame = () => {
if (gameRef.current) {
gameRef.current.destroy(true);
gameRef.current = null;
}
setScore(0);
setTimeLeft(30);
setGameStarted(false);
setGameOver(false);
setTimeout(() => {
setGameStarted(true);
}, 100);
};
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<h1 style={{
color: '#333',
marginBottom: '20px',
fontSize: '36px',
textShadow: '2px 2px 4px rgba(0,0,0,0.2)'
}}>
🎯
</h1>
<div style={{
marginBottom: '20px',
fontSize: '24px',
fontWeight: 'bold',
color: '#333'
}}>
<span style={{
backgroundColor: '#4caf50',
color: 'white',
padding: '10px 20px',
borderRadius: '8px',
marginRight: '15px',
display: 'inline-block'
}}>
: {score}
</span>
<span style={{
backgroundColor: '#ff9800',
color: 'white',
padding: '10px 20px',
borderRadius: '8px',
display: 'inline-block'
}}>
: {timeLeft}
</span>
</div>
<div id="game-container" style={{
margin: '0 auto',
borderRadius: '10px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)'
}}></div>
<div style={{ marginTop: '20px' }}>
{!gameStarted && !gameOver && (
<button
onClick={startGame}
style={{
fontSize: '20px',
padding: '15px 40px',
backgroundColor: '#2196f3',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold',
boxShadow: '0 4px 6px rgba(0,0,0,0.2)',
transition: 'all 0.3s'
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = '#1976d2';
e.currentTarget.style.transform = 'scale(1.05)';
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = '#2196f3';
e.currentTarget.style.transform = 'scale(1)';
}}
>
🎮
</button>
)}
{gameOver && (
<button
onClick={restartGame}
style={{
fontSize: '20px',
padding: '15px 40px',
backgroundColor: '#4caf50',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 'bold',
boxShadow: '0 4px 6px rgba(0,0,0,0.2)',
transition: 'all 0.3s'
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = '#45a049';
e.currentTarget.style.transform = 'scale(1.05)';
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = '#4caf50';
e.currentTarget.style.transform = 'scale(1)';
}}
>
🔄
</button>
)}
</div>
<div style={{
marginTop: '20px',
fontSize: '14px',
color: '#666',
backgroundColor: '#fff',
padding: '15px',
borderRadius: '8px',
maxWidth: '600px',
margin: '20px auto'
}}>
<p style={{ margin: '5px 0' }}>📖 :</p>
<p style={{ margin: '5px 0' }}> </p>
<p style={{ margin: '5px 0' }}> 10 </p>
<p style={{ margin: '5px 0' }}> 30 </p>
<p style={{ margin: '5px 0' }}> ,!</p>
</div>
</div>
);
}