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特性

  • 显示合成路径指南
  • 预览下一个要投放的水果
  • 游戏结束检测和重开功能
  • 响应式设计支持鼠标和触摸操作

技术特点

  1. 模块化设计: 使用 TypeScript 接口和类,代码结构清晰
  2. 动态加载: 按需加载 Matter.js 库,减少初始加载时间
  3. 事件驱动: 基于 Matter.js 的碰撞事件系统处理游戏逻辑
  4. 资源管理: 妥善处理图片加载和物理实体的创建/销毁
  5. 用户体验: 支持多种输入方式,提供视觉反馈

这是一个完整且功能丰富的物理引擎游戏实现,展现了现代前端游戏开发的最佳实践。

评论加载中...