diff --git a/astro.config.mjs b/astro.config.mjs index d55582c..81ba39a 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -16,8 +16,9 @@ let proxy = { '/api': apiProxy, }; +const basename = isDev ? undefined : pkgs.basename || '/'; export default defineConfig({ - base: isDev ? undefined : pkgs.basename, + base: basename, integrations: [ mdx(), react(), // @@ -26,6 +27,9 @@ export default defineConfig({ vite: { plugins: [tailwindcss()], + define: { + basename: JSON.stringify(basename||''), + }, server: { port: 7008, host: '0.0.0.0', diff --git a/package.json b/package.json index 4fb94cd..7ae7cd7 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { - "name": "@kevisual/astro-simplate-template", + "name": "@kevisual/whack-mole-game", "version": "0.0.1", "description": "", "main": "index.js", - "basename": "/root/astro-simplate-template", + "basename": "/root/whack-mole-game", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "pub": "envision deploy ./dist -k astro-simplate-template -v 0.0.1 -u", + "pub": "envision deploy ./dist -k whack-mole-game -v 0.0.1 -u", "sn": "pnpm dlx shadcn@latest add " }, "keywords": [], @@ -30,6 +30,7 @@ "lodash-es": "^4.17.21", "lucide-react": "^0.545.0", "nanoid": "^5.1.6", + "phaser": "^3.90.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-toastify": "^11.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee097bd..f00034c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: nanoid: specifier: ^5.1.6 version: 5.1.6 + phaser: + specifier: ^3.90.0 + version: 3.90.0 react: specifier: ^19.2.0 version: 19.2.0 @@ -1795,6 +1798,9 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + phaser@3.90.0: + resolution: {integrity: sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4399,6 +4405,10 @@ snapshots: path-parse@1.0.7: {} + phaser@3.90.0: + dependencies: + eventemitter3: 5.0.1 + picocolors@1.1.1: {} picomatch@2.3.1: {} diff --git a/public/assets/hit.wav b/public/assets/hit.wav new file mode 100644 index 0000000..9a26c38 Binary files /dev/null and b/public/assets/hit.wav differ diff --git a/public/hole.png b/public/hole.png new file mode 100644 index 0000000..63932ce Binary files /dev/null and b/public/hole.png differ diff --git a/public/mole.png b/public/mole.png new file mode 100644 index 0000000..25a341b Binary files /dev/null and b/public/mole.png differ diff --git a/src/modules/game.tsx b/src/modules/game.tsx new file mode 100644 index 0000000..a418e6d --- /dev/null +++ b/src/modules/game.tsx @@ -0,0 +1,408 @@ +import { useEffect, useRef, useState } from 'react'; +import Phaser from 'phaser'; +console.log('Phaser version:', basename); +const base = basename || ''; +export const Game = () => { + const gameRef = useRef(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 ( +
+

+ 🎯 打地鼠游戏 +

+ +
+ + 得分: {score} + + + 剩余时间: {timeLeft}秒 + +
+ +
+ +
+ {!gameStarted && !gameOver && ( + + )} + + {gameOver && ( + + )} +
+ +
+

📖 游戏规则:

+

• 点击冒出来的地鼠得分

+

• 每击中一只地鼠得 10 分

+

• 游戏时长 30 秒

+

• 快速反应,争取高分!

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 3e848df..c37b8d6 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,47 +1,33 @@ --- -// import { query } from '@/modules/query.ts'; -console.log('Hello from index.astro'); import '../styles/global.css'; +import { Game } from '../modules/game'; --- - + + - My Homepage + + + 打地鼠游戏 - Phaser 3 + -

Welcome to my website!

-
-
- - +