import { dialog } from './dialog.js';
import { outPutInfo, outPutSuccess } from './console-logger.js';
class WatermelonGame {
constructor() {
// 定义水果等级和属性
// 半径单位: px
// 图片路径相对于 webroot
this.FRUITS = [
{ 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' }
];
this.PANEL_WIDTH = 440;
this.PANEL_HEIGHT = 640;
// 游戏状态
this.isOpen = false;
this.overlay = null;
this.engine = null;
this.render = null;
this.runner = null;
// 逻辑控制
this.currentBody = null; // 当前悬停的水果 Body
this.nextFruitLevel = 0;
this.canDrop = true;
this.isGameOver = false;
// Matter.js 模块引用 (懒加载)
this.Matter = null;
this.bindConsoleCommand();
}
bindConsoleCommand() {
window.openWatermelonGame = () => {
if (this.isOpen) {
outPutInfo('合成大西瓜已经打开了');
return;
}
this.open();
};
outPutInfo('🍉 合成大西瓜 (Matter.js 版)');
outPutInfo('输入 window.openWatermelonGame() 启动游戏');
}
async loadMatterJs() {
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 = () => {
this.Matter = window.Matter;
resolve();
};
script.onerror = () => {
outPutInfo('Matter.js 加载失败,请检查网络');
reject(new Error('Failed to load Matter.js'));
};
document.body.appendChild(script);
});
}
async open() {
if (this.isOpen) return;
try {
await this.loadMatterJs();
} catch (e) {
console.error(e);
dialog.alert('加载游戏引擎失败');
return;
}
this.isOpen = true;
this.createUI();
this.initGame();
outPutSuccess('合成大西瓜已启动');
}
createUI() {
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;">
<!-- Canvas will be injected here by Matter.js -->
<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" 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;
// 绑定 UI 事件
overlay.querySelector('#wg-close').onclick = () => this.close();
overlay.querySelector('#wg-restart').onclick = () => this.restart();
overlay.querySelector('#wg-retry').onclick = () => this.restart();
this.renderPath();
}
renderPath() {
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);
}
}
initGame() {
const { Engine, Render, Runner, World, Bodies, Events, Composite, Body } = this.Matter;
// 1. 创建引擎
this.engine = Engine.create();
// 2. 创建渲染器
const container = document.getElementById('game-container');
// 获取容器实际大小,因为 CSS 可能会限制它
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
}
});
// 3. 创建边界墙
const wallOptions = {
isStatic: true,
render: { fillStyle: '#ffd180' }
};
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' });
// 警戒线逻辑:我们在 collisionActive 中检测或简单判定最高水果
// 这里添加一根隐形的线作为参考,或者只是逻辑判断
World.add(this.engine.world, [ground, leftWall, rightWall]);
// 4. 运行引擎
this.runner = Runner.create();
Runner.run(this.runner, this.engine);
Render.run(this.render);
// 5. 事件监听
Events.on(this.engine, 'collisionStart', (event) => this.handleCollision(event));
// 6. 输入绑定
const canvas = this.render.canvas;
// 移动事件处理函数
const moveHandler = (e) => {
e.preventDefault();
this.inputMove(e);
};
// 点击/释放事件处理函数
const endHandler = (e) => {
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); // 随机 0-4
this.createNewReadyFruit();
this.updateNextPreview();
}
inputMove(e) {
if (!this.canDrop || this.isGameOver || !this.currentBody) return;
const rect = this.render.canvas.getBoundingClientRect();
// 兼容 touch 和 mouse
const clientX = e.touches ? 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;
this.Matter.Body.setPosition(this.currentBody, {
x: x,
y: this.currentBody.position.y
});
}
inputClick(e) {
if (!this.canDrop || this.isGameOver || !this.currentBody) return;
this.canDrop = false;
const body = this.currentBody;
this.Matter.Body.setStatic(body, false); // 让它掉落
this.currentBody = null;
// 1秒后生成下一个
setTimeout(() => {
if (!this.isGameOver) {
this.createNewReadyFruit();
this.canDrop = true;
}
}, 1000);
}
// 创建顶部待命的水果
createNewReadyFruit() {
const level = this.nextFruitLevel;
// 随机生成下一个 (0-4级: 葡萄到猕猴桃)
this.nextFruitLevel = Math.floor(Math.random() * 5);
this.updateNextPreview();
const fruitConf = this.FRUITS[level];
const x = this.PANEL_WIDTH / 2;
const y = 50; // 顶部位置
const body = this.createFruitBody(x, y, level, true);
this.Matter.Composite.add(this.engine.world, body);
this.currentBody = body;
}
// 创建水果实体(通用方法)
createFruitBody(x, y, level, isStatic = false) {
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 };
// 异步修正 Scale
const img = new Image();
img.src = fruitConf.img;
img.onload = () => {
// 计算缩放比例:根据半径算出直径,再除以图片原始宽度
const scale = (fruitConf.radius * 2) / img.width;
body.render.sprite.xScale = scale;
body.render.sprite.yScale = scale;
};
return body;
}
updateNextPreview() {
const img = document.getElementById('next-fruit-img');
if (img) {
img.src = this.FRUITS[this.nextFruitLevel].img;
}
}
handleCollision(event) {
if (this.isGameOver) return;
const { pairs } = event;
const { Composite, Body, Vector, World } = this.Matter;
// 找出需要合成的对
// 注意:collisionStart 可能在一步包含多个碰撞
// 我们需要标记已经处理过的 body 防止重复
const processedBodies = new Set();
const merges = [];
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) {
outPutSuccess('太强了!合成大西瓜!');
}
}
}
restart() {
if (!this.engine) return;
const { Composite } = this.Matter;
Composite.clear(this.engine.world, false); // clear all bodies (except static walls if handled carefully, but clear removes everything usually)
// 因为 clear 清除了 walls,需要重建
this.engine.events = {}; // 清除所有事件监听? 不,clear 不清除事件。
// 简单暴力:停止一切,重新 initGame
// 但为了性能,最好只是清空实体
// 更好的 restart:
// 1. 找出所有 label='Fruit' 的 body 并移除
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();
}
cleanGameOver() {
this.isGameOver = false;
const mask = document.getElementById('game-over-mask');
if (mask) mask.style.display = 'none';
}
close() {
if (!this.isOpen) return;
// 停止引擎
if (this.runner) {
this.Matter.Runner.stop(this.runner);
}
if (this.render) {
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();
评论加载中...