import '../types/matter.d.ts';
import { uiFeedbackService } from './UiFeedbackService';
import { logger } from './ConsoleLogger';
/**
* 水果配置接口
*/
interface FruitConfig {
readonly level: number;
readonly key: string;
readonly label: string;
readonly radius: number;
readonly img: string;
}
/**
* 合并事件接口
*/
interface MergeEvent {
readonly bodyA: Matter.Body;
readonly bodyB: Matter.Body;
readonly level: number;
}
/**
* 合成大西瓜游戏类
*/
export class WatermelonGame {
/**
* 游戏面板宽度
*/
private readonly PANEL_WIDTH = 440;
/**
* 游戏面板高度
*/
private readonly PANEL_HEIGHT = 640;
/**
* 水果等级配置数组
*/
private readonly FRUITS: ReadonlyArray<FruitConfig> = [
{
level: 0,
key: 'grape',
label: '葡萄',
radius: 20,
img: '/images/suika/grape.png',
},
{
level: 1,
key: 'cherry',
label: '樱桃',
radius: 30,
img: '/images/suika/cherry.png',
},
{
level: 2,
key: 'orange',
label: '橘子',
radius: 40,
img: '/images/suika/orange.png',
},
{
level: 3,
key: 'lemon',
label: '柠檬',
radius: 50,
img: '/images/suika/lemon.png',
},
{
level: 4,
key: 'kiwi',
label: '猕猴桃',
radius: 60,
img: '/images/suika/kiwi.png',
},
{
level: 5,
key: 'tomato',
label: '番茄',
radius: 70,
img: '/images/suika/tomato.png',
},
{
level: 6,
key: 'peach',
label: '桃子',
radius: 80,
img: '/images/suika/peach.png',
},
{
level: 7,
key: 'pineapple',
label: '菠萝',
radius: 95,
img: '/images/suika/pineapple.png',
},
{
level: 8,
key: 'coconut',
label: '椰子',
radius: 110,
img: '/images/suika/coconut.png',
},
{
level: 9,
key: 'half-watermelon',
label: '半西瓜',
radius: 130,
img: '/images/suika/half-watermelon.png',
},
{
level: 10,
key: 'watermelon',
label: '大西瓜',
radius: 150,
img: '/images/suika/watermelon.png',
},
];
/**
* 游戏是否已打开
*/
private _isOpen: boolean = false;
/**
* 遮罩层元素
*/
private _overlay: HTMLElement | null = null;
/**
* Matter.js 引擎实例
*/
private _engine: Matter.Engine | null = null;
/**
* Matter.js 渲染器实例
*/
private _render: Matter.Render | null = null;
/**
* Matter.js 运行器实例
*/
private _runner: Matter.Runner | null = null;
/**
* 当前悬停的水果 Body
*/
private _currentBody: Matter.Body | null = null;
/**
* 下一个水果等级
*/
private _nextFruitLevel: number = 0;
/**
* 是否可以投放水果
*/
private _canDrop: boolean = true;
/**
* 游戏是否已结束
*/
private _isGameOver: boolean = false;
/**
* Matter.js 模块引用
*/
private _Matter: typeof Matter | null = null;
public constructor() {
this.bindConsoleCommand();
}
/**
* 绑定控制台命令
*/
private bindConsoleCommand(): void {
window.openWatermelonGame = async (): Promise<void> => {
if (this._isOpen) {
logger.outPutInfo('合成大西瓜已经打开了');
return;
}
await this.open();
};
logger.outPutInfo('🍉 合成大西瓜 (Matter.js 版)');
logger.outPutInfo('输入 window.openWatermelonGame() 启动游戏');
}
/**
* 加载 Matter.js 库
*/
private async loadMatterJs(): Promise<void> {
if (window.Matter) {
this._Matter = window.Matter;
return;
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js';
script.onload = (): void => {
this._Matter = window.Matter;
resolve();
};
script.onerror = (): void => {
logger.outPutInfo('Matter.js 加载失败,请检查网络');
reject(new Error('Failed to load Matter.js'));
};
document.body.appendChild(script);
});
}
/**
* 打开游戏
*/
public async open(): Promise<void> {
if (this._isOpen) {
return;
}
try {
await this.loadMatterJs();
} catch (e) {
console.error(e);
await uiFeedbackService.alert('加载游戏引擎失败');
return;
}
this._isOpen = true;
this.createUI();
this.initGame();
logger.outPutSuccess('合成大西瓜已启动');
}
/**
* 创建游戏UI界面
*/
private createUI(): void {
const overlay = document.createElement('div');
overlay.className = 'watermelon-game';
overlay.innerHTML = `
<div class="watermelon-game__panel">
<div class="watermelon-game__header">
<h3 class="watermelon-game__title">合成大西瓜</h3>
<div class="watermelon-game__actions">
<button class="watermelon-game__action" id="wg-restart">
重开
</button>
<button
class="watermelon-game__action"
id="wg-close"
aria-label="关闭">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="watermelon-game__content">
<aside class="watermelon-game__side watermelon-game__side--left">
<h4>合成路径</h4>
<div class="watermelon-game__path" id="watermelon-path"></div>
</aside>
<main
class="watermelon-game__center"
id="game-container"
style="position: relative;
padding: 0;
overflow: hidden;
background: #ffe8cd;">
<div
id="game-over-mask"
style="display:none;
position:absolute;
inset:0;
background:rgba(0,0,0,0.7);
color:white;
flex-direction:column;
justify-content:center;
align-items:center;
z-index:10;">
<h2 style="font-size: 2rem; margin-bottom: 1rem;">
游戏结束
</h2>
<button class="watermelon-game__action" id="wg-retry">
再来一局
</button>
</div>
</main>
<aside class="watermelon-game__side watermelon-game__side--right">
<h4>下一个</h4>
<div style="width: 140px;
height: 140px;
display: flex;
justify-content: center;
align-items: center;
background: white;
border-radius: 8px;
border: 1px solid #e2e8f0;">
<img
id="next-fruit-img"
alt="icon"
src=""
style="max-width: 80%;
max-height: 80%;
object-fit: contain;">
</div>
<p class="watermelon-game__hint">随机生成前 5 种水果</p>
</aside>
</div>
</div>
`;
document.body.appendChild(overlay);
this._overlay = overlay;
const closeBtn = overlay.querySelector('#wg-close') as HTMLElement;
if (closeBtn) {
closeBtn.onclick = (): void => this.close();
}
const restartBtn = overlay.querySelector('#wg-restart') as HTMLElement;
if (restartBtn) {
restartBtn.onclick = (): void => this.restart();
}
const retryBtn = overlay.querySelector('#wg-retry') as HTMLElement;
if (retryBtn) {
retryBtn.onclick = (): void => this.restart();
}
this.renderPath();
}
/**
* 渲染水果合成路径
*/
private renderPath(): void {
const container = document.getElementById('watermelon-path');
if (!container) {
return;
}
container.innerHTML = '';
for (let i = 0; i < this.FRUITS.length - 1; i++) {
const row = document.createElement('div');
row.className = 'watermelon-game__path-row';
row.textContent =
`${this.FRUITS[i].label} + ${this.FRUITS[i].label} → ` +
`${this.FRUITS[i + 1].label}`;
container.appendChild(row);
}
}
/**
* 初始化游戏逻辑
*/
private initGame(): void {
if (!this._Matter) {
return;
}
const { Engine, Render, Runner, World, Bodies, Events, Composite } = this._Matter;
this._engine = Engine.create();
const container = document.getElementById('game-container');
if (!container) {
return;
}
const width = 440;
const height = 640;
this._render = Render.create({
element: container,
engine: this._engine,
options: {
width: width,
height: height,
wireframes: false,
background: '#ffe8cd',
pixelRatio: window.devicePixelRatio,
},
});
const ground = Bodies.rectangle(width / 2, height + 30, width, 60, {
isStatic: true,
label: 'Ground',
render: { fillStyle: '#e6aac3' },
});
const leftWall = Bodies.rectangle(-30, height / 2, 60, height * 2, {
isStatic: true,
label: 'Wall',
});
const rightWall = Bodies.rectangle(width + 30, height / 2, 60, height * 2, {
isStatic: true,
label: 'Wall',
});
World.add(this._engine.world, [ground, leftWall, rightWall]);
this._runner = Runner.create();
Runner.run(this._runner, this._engine);
Render.run(this._render);
Events.on(
this._engine,
'collisionStart',
(event: Matter.IEventCollision<Matter.Engine>): void => {
this.handleCollision(event);
}
);
const canvas = this._render.canvas;
const moveHandler = (e: MouseEvent | TouchEvent): void => {
e.preventDefault();
this.inputMove(e);
};
const endHandler = (e: MouseEvent | TouchEvent): void => {
e.preventDefault();
this.inputClick(e);
};
canvas.addEventListener('mousemove', moveHandler);
canvas.addEventListener('touchmove', moveHandler, { passive: false });
canvas.addEventListener('click', endHandler);
canvas.addEventListener('touchend', endHandler);
this._nextFruitLevel = Math.floor(Math.random() * 5);
this.createNewReadyFruit();
this.updateNextPreview();
}
/**
* 处理鼠标/触摸移动事件
*/
private inputMove(e: MouseEvent | TouchEvent): void {
if (!this._canDrop || this._isGameOver || !this._currentBody) {
return;
}
if (!this._render) {
return;
}
const rect = this._render.canvas.getBoundingClientRect();
const isTouchEvent = 'touches' in e;
const clientX = isTouchEvent ? e.touches[0].clientX : e.clientX;
let x = clientX - rect.left;
const radius = this.FRUITS[this._currentBody.plugin.level].radius;
if (x < radius) {
x = radius;
}
if (x > this.PANEL_WIDTH - radius) {
x = this.PANEL_WIDTH - radius;
}
if (this._Matter) {
this._Matter.Body.setPosition(this._currentBody, {
x: x,
y: this._currentBody.position.y,
});
}
}
/**
* 处理点击/触摸结束事件
*/
private inputClick(e: MouseEvent | TouchEvent): void {
if (!this._canDrop || this._isGameOver || !this._currentBody) {
return;
}
this._canDrop = false;
const body = this._currentBody;
if (this._Matter) {
this._Matter.Body.setStatic(body, false);
}
this._currentBody = null;
setTimeout(() => {
if (!this._isGameOver) {
this.createNewReadyFruit();
this._canDrop = true;
}
}, 1000);
}
/**
* 创建顶部待命的水果
*/
private createNewReadyFruit(): void {
const level = this._nextFruitLevel;
this._nextFruitLevel = Math.floor(Math.random() * 5);
this.updateNextPreview();
const x = this.PANEL_WIDTH / 2;
const y = 50;
const body = this.createFruitBody(x, y, level, true);
if (this._Matter && this._engine) {
this._Matter.Composite.add(this._engine.world, body);
}
this._currentBody = body;
}
/**
* 创建水果实体
*/
private createFruitBody(
x: number,
y: number,
level: number,
isStatic: boolean = false
): Matter.Body {
if (!this._Matter) {
throw new Error('Matter.js not loaded');
}
const fruitConf = this.FRUITS[level];
const body = this._Matter.Bodies.circle(x, y, fruitConf.radius, {
isStatic: isStatic,
label: 'Fruit',
restitution: 0.2,
friction: 0.1,
render: {
sprite: {
texture: fruitConf.img,
},
},
});
body.plugin = { level: level };
const img = new Image();
img.src = fruitConf.img;
img.onload = (): void => {
const scale = (fruitConf.radius * 2) / img.width;
body.render.sprite.xScale = scale;
body.render.sprite.yScale = scale;
};
return body;
}
/**
* 更新下一个水果预览
*/
private updateNextPreview(): void {
const img = document.getElementById('next-fruit-img') as HTMLImageElement;
if (img) {
img.src = this.FRUITS[this._nextFruitLevel].img;
}
}
/**
* 处理碰撞事件
*/
private handleCollision(event: Matter.IEventCollision<Matter.Engine>): void {
if (this._isGameOver || !this._Matter || !this._engine) {
return;
}
const { pairs } = event;
const { Composite } = this._Matter;
const processedBodies = new Set<number>();
const merges: MergeEvent[] = [];
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
const bodyA = pair.bodyA;
const bodyB = pair.bodyB;
if (bodyA.label === 'Fruit' && bodyB.label === 'Fruit') {
const levelA = bodyA.plugin.level;
const levelB = bodyB.plugin.level;
if (levelA === levelB && levelA < this.FRUITS.length - 1) {
if (processedBodies.has(bodyA.id) || processedBodies.has(bodyB.id)) {
continue;
}
processedBodies.add(bodyA.id);
processedBodies.add(bodyB.id);
merges.push({ bodyA, bodyB, level: levelA });
}
}
}
for (const merge of merges) {
const { bodyA, bodyB, level } = merge;
Composite.remove(this._engine.world, [bodyA, bodyB]);
const x = (bodyA.position.x + bodyB.position.x) / 2;
const y = (bodyA.position.y + bodyB.position.y) / 2;
const newLevel = level + 1;
const newBody = this.createFruitBody(x, y, newLevel, false);
Composite.add(this._engine.world, newBody);
if (newLevel === this.FRUITS.length - 1) {
logger.outPutSuccess('太强了!合成大西瓜!');
}
}
}
/**
* 重新开始游戏
*/
private restart(): void {
if (!this._engine || !this._Matter) {
return;
}
const { Composite } = this._Matter;
const bodies = Composite.allBodies(this._engine.world);
const fruits = bodies.filter((b) => b.label === 'Fruit');
Composite.remove(this._engine.world, fruits);
this.cleanGameOver();
this._nextFruitLevel = 0;
this._canDrop = true;
this.createNewReadyFruit();
this.updateNextPreview();
}
/**
* 清理游戏结束状态
*/
private cleanGameOver(): void {
this._isGameOver = false;
const mask = document.getElementById('game-over-mask');
if (mask) {
mask.style.display = 'none';
}
}
/**
* 关闭游戏
*/
public close(): void {
if (!this._isOpen) {
return;
}
if (this._runner && this._Matter) {
this._Matter.Runner.stop(this._runner);
}
if (this._render && this._Matter) {
this._Matter.Render.stop(this._render);
if (this._render.canvas) {
this._render.canvas.remove();
}
}
if (this._overlay) {
this._overlay.remove();
this._overlay = null;
}
this._isOpen = false;
this._engine = null;
this._render = null;
}
}
export const watermelonGame = new WatermelonGame();
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这是一个使用 Matter.js 物理引擎实现的"合成大西瓜"游戏的完整代码。让我详细解释一下这个代码的结构和功能:
代码结构概述
1. 类型定义
interface FruitConfig {
readonly level: number;
readonly key: string;
readonly label: string;
readonly radius: number;
readonly img: string;
}
interface MergeEvent {
readonly bodyA: Matter.Body;
readonly bodyB: Matter.Body;
readonly level: number;
}
定义了水果配置和合并事件的接口。
2. 主要类 WatermelonGame
核心属性
FRUITS: 包含11种水果的配置数组,从葡萄到大西瓜,每种水果有不同的等级、半径和图片- 游戏状态管理:
_isOpen、_isGameOver、_canDrop等 - Matter.js 相关实例:
_engine、_render、_runner - 游戏元素:
_currentBody(当前悬停的水果)、_nextFruitLevel(下一个水果等级)
主要功能方法
1. 游戏初始化
loadMatterJs(): 动态加载 Matter.js 库open(): 启动游戏,创建UI和初始化物理世界createUI(): 创建游戏界面HTML结构
2. 物理世界设置
initGame(): 初始化 Matter.js 引擎、渲染器和物理边界- 创建地面和左右墙壁作为静态物体
- 设置碰撞检测事件监听
3. 游戏逻辑
createFruitBody(): 创建水果物理实体,包含图片纹理和物理属性createNewReadyFruit(): 在顶部创建待投放的水果handleCollision(): 处理水果碰撞和合并逻辑
4. 交互控制
inputMove(): 处理鼠标/触摸移动,控制水果水平位置inputClick(): 处理点击/触摸结束,投放水果
5. 游戏管理
restart(): 重新开始游戏close(): 关闭游戏并清理资源
游戏机制
水果合成规则
- 相同等级的两个水果碰撞时会合成为下一等级的水果
- 从葡萄(level 0)开始,最终目标是合成大西瓜(level 10)
- 玩家只能投放前5种水果(level 0-4)
物理特性
- 使用 Matter.js 提供真实的重力和碰撞效果
- 水果有弹性(restitution)和摩擦力(friction)属性
- 游戏区域有物理边界防止水果掉出
UI特性
- 显示合成路径指南
- 预览下一个要投放的水果
- 游戏结束检测和重开功能
- 响应式设计支持鼠标和触摸操作
技术特点
- 模块化设计: 使用 TypeScript 接口和类,代码结构清晰
- 动态加载: 按需加载 Matter.js 库,减少初始加载时间
- 事件驱动: 基于 Matter.js 的碰撞事件系统处理游戏逻辑
- 资源管理: 妥善处理图片加载和物理实体的创建/销毁
- 用户体验: 支持多种输入方式,提供视觉反馈
这是一个完整且功能丰富的物理引擎游戏实现,展现了现代前端游戏开发的最佳实践。
AI 正在分析代码…
评论加载中...