init games

This commit is contained in:
2025-10-18 00:36:49 +08:00
parent 3291d507f5
commit e3a0c6d439
8 changed files with 451 additions and 42 deletions

View File

@@ -16,8 +16,9 @@ let proxy = {
'/api': apiProxy, '/api': apiProxy,
}; };
const basename = isDev ? undefined : pkgs.basename || '/';
export default defineConfig({ export default defineConfig({
base: isDev ? undefined : pkgs.basename, base: basename,
integrations: [ integrations: [
mdx(), mdx(),
react(), // react(), //
@@ -26,6 +27,9 @@ export default defineConfig({
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],
define: {
basename: JSON.stringify(basename||''),
},
server: { server: {
port: 7008, port: 7008,
host: '0.0.0.0', host: '0.0.0.0',

View File

@@ -1,14 +1,14 @@
{ {
"name": "@kevisual/astro-simplate-template", "name": "@kevisual/whack-mole-game",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"basename": "/root/astro-simplate-template", "basename": "/root/whack-mole-game",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "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 " "sn": "pnpm dlx shadcn@latest add "
}, },
"keywords": [], "keywords": [],
@@ -30,6 +30,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"phaser": "^3.90.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",

10
pnpm-lock.yaml generated
View File

@@ -50,6 +50,9 @@ importers:
nanoid: nanoid:
specifier: ^5.1.6 specifier: ^5.1.6
version: 5.1.6 version: 5.1.6
phaser:
specifier: ^3.90.0
version: 3.90.0
react: react:
specifier: ^19.2.0 specifier: ^19.2.0
version: 19.2.0 version: 19.2.0
@@ -1795,6 +1798,9 @@ packages:
path-parse@1.0.7: path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
phaser@3.90.0:
resolution: {integrity: sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -4399,6 +4405,10 @@ snapshots:
path-parse@1.0.7: {} path-parse@1.0.7: {}
phaser@3.90.0:
dependencies:
eventemitter3: 5.0.1
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}

BIN
public/assets/hit.wav Normal file

Binary file not shown.

BIN
public/hole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
public/mole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

408
src/modules/game.tsx Normal file
View File

@@ -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<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>
);
}

View File

@@ -1,47 +1,33 @@
--- ---
// import { query } from '@/modules/query.ts';
console.log('Hello from index.astro');
import '../styles/global.css'; import '../styles/global.css';
import { Game } from '../modules/game';
--- ---
<html lang='en'> <!doctype html>
<html lang='zh'>
<head> <head>
<title>My Homepage</title> <meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>打地鼠游戏 - Phaser 3</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
align-items: center;
}
#game-container {
border: 4px solid #333;
border-radius: 10px;
overflow: hidden;
}
</style>
</head> </head>
<body> <body>
<h1 onclick="{onClick}">Welcome to my website!</h1> <Game client:only="react" />
<div class='bg-amber-50 w-20 h-20 rounded-full'></div>
<div id='root'></div>
<script type='importmap' data-vite-ignore is:inline>
{
"imports": {
"react": "https://esm.sh/react@19.1.0",
"react-dom": "https://esm.sh/react-dom@19.1.0/client.js",
"react-toastify": "https://esm.sh/react-toastify@11.0.5"
}
}
</script>
<script type='module' data-vite-ignore is:inline>
import { Button, message } from 'https://esm.sh/antd?standalone';
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { createRoot } from 'react-dom';
setTimeout(() => {
toast.loading('Hello from index.astro');
window.toast = toast;
console.log('message', toast);
}, 1000);
console.log('Hello from index.astro', Button);
const root = document.getElementById('root');
const render = createRoot(root);
const App = () => {
const button = React.createElement(Button, null, 'Hello');
const messageEl = React.createElement(ToastContainer, null, 'Hello');
const wrapperMessage = React.createElement('div', null, [button, messageEl]);
return wrapperMessage;
};
// render.render(React.createElement(Button, null, 'Hello'), root);
render.render(App(), root);
</script>
</body> </body>
</html> </html>