fruit.vue 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. <template>
  2. <div class="game-container">
  3. <div id="game"></div>
  4. </div>
  5. </template>
  6. <script setup name="Fruit">
  7. import { onMounted, ref } from 'vue';
  8. import Phaser from 'phaser';
  9. const { proxy } = getCurrentInstance();
  10. const router = useRouter();
  11. // 游戏容器和尺寸相关
  12. const width = document.documentElement.clientWidth;
  13. const height = document.documentElement.clientHeight;
  14. const wRatio = document.documentElement.clientWidth / 640;
  15. const hRatio = document.documentElement.clientHeight / 480;
  16. const gameContainer = ref(null);
  17. let game = null;
  18. // 工具类
  19. const mathTool = {
  20. init() {
  21. },
  22. // 计算延长线,p2往p1延长
  23. calcParallel(p1, p2, L) {
  24. const p = {};
  25. if (p1.x === p2.x) {
  26. if (p1.y - p2.y > 0) {
  27. p.x = p1.x;
  28. p.y = p1.y + L;
  29. } else {
  30. p.x = p1.x;
  31. p.y = p1.y - L;
  32. }
  33. } else {
  34. const k = (p2.y - p1.y) / (p2.x - p1.x);
  35. if (p2.x - p1.x < 0) {
  36. p.x = p1.x + L / Math.sqrt(1 + k * k);
  37. p.y = p1.y + L * k / Math.sqrt(1 + k * k);
  38. } else {
  39. p.x = p1.x - L / Math.sqrt(1 + k * k);
  40. p.y = p1.y - L * k / Math.sqrt(1 + k * k);
  41. }
  42. }
  43. p.x = Math.round(p.x);
  44. p.y = Math.round(p.y);
  45. return new Phaser.Math.Vector2(p.x, p.y);
  46. },
  47. // 计算垂直线,p2点开始垂直
  48. calcVertical(p1, p2, L, isLeft) {
  49. const p = {};
  50. if (p1.y === p2.y) {
  51. p.x = p2.x;
  52. if (isLeft) {
  53. if (p2.x - p1.x > 0) {
  54. p.y = p2.y - L;
  55. } else {
  56. p.y = p2.y + L;
  57. }
  58. } else {
  59. if (p2.x - p1.x > 0) {
  60. p.y = p2.y + L;
  61. } else {
  62. p.y = p2.y - L;
  63. }
  64. }
  65. } else {
  66. const k = -(p2.x - p1.x) / (p2.y - p1.y);
  67. if (isLeft) {
  68. if (p2.y - p1.y > 0) {
  69. p.x = p2.x + L / Math.sqrt(1 + k * k);
  70. p.y = p2.y + L * k / Math.sqrt(1 + k * k);
  71. } else {
  72. p.x = p2.x - L / Math.sqrt(1 + k * k);
  73. p.y = p2.y - L * k / Math.sqrt(1 + k * k);
  74. }
  75. } else {
  76. if (p2.y - p1.y > 0) {
  77. p.x = p2.x - L / Math.sqrt(1 + k * k);
  78. p.y = p2.y - L * k / Math.sqrt(1 + k * k);
  79. } else {
  80. p.x = p2.x + L / Math.sqrt(1 + k * k);
  81. p.y = p2.y + L * k / Math.sqrt(1 + k * k);
  82. }
  83. }
  84. }
  85. p.x = Math.round(p.x);
  86. p.y = Math.round(p.y);
  87. return new Phaser.Math.Vector2(p.x, p.y);
  88. },
  89. // 形成刀光点
  90. generateBlade(points) {
  91. const res = [];
  92. if (points.length <= 0) {
  93. return res;
  94. } else if (points.length === 1) {
  95. const oneLength = 6;
  96. res.push(new Phaser.Math.Vector2(points[0].x - oneLength, points[0].y));
  97. res.push(new Phaser.Math.Vector2(points[0].x, points[0].y - oneLength));
  98. res.push(new Phaser.Math.Vector2(points[0].x + oneLength, points[0].y));
  99. res.push(new Phaser.Math.Vector2(points[0].x, points[0].y + oneLength));
  100. } else {
  101. const tailLength = 10;
  102. const headLength = 20;
  103. const tailWidth = 1;
  104. const headWidth = 6;
  105. res.push(this.calcParallel(points[0], points[1], tailLength));
  106. for (let i = 0; i < points.length - 1; i++) {
  107. res.push(this.calcVertical(
  108. points[i + 1],
  109. points[i],
  110. Math.round((headWidth - tailWidth) * i / (points.length - 1) + tailWidth),
  111. true
  112. ));
  113. }
  114. res.push(this.calcVertical(
  115. points[points.length - 2],
  116. points[points.length - 1],
  117. headWidth,
  118. false
  119. ));
  120. res.push(this.calcParallel(
  121. points[points.length - 1],
  122. points[points.length - 2],
  123. headLength
  124. ));
  125. res.push(this.calcVertical(
  126. points[points.length - 2],
  127. points[points.length - 1],
  128. headWidth,
  129. true
  130. ));
  131. for (let i = points.length - 1; i > 0; i--) {
  132. res.push(this.calcVertical(
  133. points[i],
  134. points[i - 1],
  135. Math.round((headWidth - tailWidth) * (i - 1) / (points.length - 1) + tailWidth),
  136. false
  137. ));
  138. }
  139. }
  140. return res;
  141. },
  142. randomMinMax(min, max) {
  143. return Math.random() * (max - min) + min;
  144. },
  145. randomPosX() {
  146. return this.randomMinMax(-100, width + 100);
  147. },
  148. randomPosY() {
  149. //return this.randomMinMax(100, 200) + height;
  150. return this.randomMinMax(height - 100, height);
  151. },
  152. randomVelocityX(posX) {
  153. if (posX < 0) {
  154. return this.randomMinMax(100, 500);
  155. } else if (posX >= 0 && posX < width / 2) {
  156. return this.randomMinMax(0, 500);
  157. } else if (posX >= width / 2 && posX < width) {
  158. return this.randomMinMax(-500, 0);
  159. } else {
  160. return this.randomMinMax(-500, -100);
  161. }
  162. },
  163. randomVelocityY() {
  164. const myH = height - 480;
  165. return this.randomMinMax(-1000 - myH, -950 - myH);
  166. },
  167. degCos(deg) {
  168. return Math.cos(deg * Math.PI / 180);
  169. },
  170. degSin(deg) {
  171. return Math.sin(deg * Math.PI / 180);
  172. },
  173. shuffle(o) {
  174. for (let j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
  175. return o;
  176. }
  177. };
  178. // 炸弹类
  179. class Bomb {
  180. constructor(envConfig) {
  181. this.env = envConfig;
  182. this.game = envConfig.scene;
  183. this.sprite = null;
  184. this.bombImage = null;
  185. this.bombSmoke = null;
  186. this.bombEmit = null;
  187. this.init();
  188. }
  189. init() {
  190. // 创建容器
  191. this.sprite = this.game.add.container(
  192. this.env.x || 0,
  193. this.env.y || 0
  194. );
  195. // 炸弹图像
  196. this.bombImage = this.game.add.sprite(0, 0, 'bomb');
  197. this.bombImage.setOrigin(0.5, 0.5);
  198. // 烟雾
  199. this.bombSmoke = this.game.add.sprite(-55, -55, 'smoke');
  200. // 创建粒子纹理
  201. const bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
  202. this.generateFlame(bitmap);
  203. const texture = bitmap.generateTexture('flameParticle', 50, 50);
  204. // 粒子发射器
  205. this.bombEmit = this.game.add.particles(0, 0, texture.key, {
  206. x: -30,
  207. y: -30,
  208. speed: { min: -100, max: 100 },
  209. scale: { start: 1, end: 0.8 },
  210. alpha: { start: 1, end: 0.1 },
  211. lifespan: 1500,
  212. frequency: 50,
  213. maxParticles: 20
  214. });
  215. // 添加到容器
  216. this.sprite.add([this.bombImage, this.bombEmit, this.bombSmoke]);
  217. // 物理属性
  218. this.game.physics.add.existing(this.sprite);
  219. this.sprite.body.setCollideWorldBounds(false);
  220. }
  221. generateFlame(bitmap) {
  222. const len = 5;
  223. bitmap.fillStyle(0xffffff);
  224. bitmap.beginPath();
  225. bitmap.moveTo(25 + len, 25 - len);
  226. bitmap.lineTo(25 + len, 25 + len);
  227. bitmap.lineTo(25 - len, 25 + len);
  228. bitmap.lineTo(25 - len, 25 - len);
  229. bitmap.closePath();
  230. bitmap.fill();
  231. }
  232. explode(onWhite, onComplete) {
  233. const lights = [];
  234. const startDeg = Math.floor(Math.random() * 360); // 随机初始角度(备用)
  235. // 1. 创建8个灯光图形(爆炸扩散效果)
  236. for (let i = 0; i < 8; i++) {
  237. const light = this.game.add.graphics({
  238. x: this.sprite.x, // 基于炸弹位置定位
  239. y: this.sprite.y
  240. });
  241. light.fillStyle(0xffffff, 0); // 初始透明
  242. light.beginPath();
  243. light.arc(0, 0, 15 + i * 10, 0, Math.PI * 2); // 半径递增的圆形灯光
  244. light.closePath();
  245. light.fill();
  246. light.setDepth(2000); // 确保在其他元素上方
  247. lights.push(light);
  248. }
  249. // 2. 打乱灯光顺序(与动画顺序匹配)
  250. mathTool.shuffle(lights);
  251. // 3. 构建链式动画配置(每个灯光的闪烁动画)
  252. const tweenConfigs = lights.map(light => ({
  253. targets: light,
  254. alpha: { value: [0, 1, 0], duration: 500 }, // 淡入淡出
  255. scale: { value: 2, duration: 500 }, // 缩放扩散
  256. onComplete: () => {
  257. light.destroy(); // 单个灯光动画结束后销毁
  258. }
  259. }));
  260. // 4. 执行链式动画(按打乱后的顺序播放灯光效果)
  261. this.game.tweens.chain({
  262. tweens: tweenConfigs,
  263. onComplete: () => {
  264. // 5. 所有灯光动画结束后,执行白屏效果
  265. const whiteScreen = this.game.add.graphics({
  266. x: 0,
  267. y: 0,
  268. fillStyle: { color: 0xffffff, alpha: 0 }
  269. });
  270. whiteScreen.fillRect(0, 0, width, height); // 覆盖全屏
  271. whiteScreen.setDepth(3000); // 确保在最上层
  272. // 白屏动画:淡入后淡出
  273. this.game.tweens.add({
  274. targets: whiteScreen,
  275. alpha: { value: [0, 1, 0], duration: 400 }, // 100ms淡入 + 300ms淡出
  276. onUpdate: (tween) => {
  277. // 白屏峰值时触发onWhite回调(替代原分步逻辑)
  278. if (tween.progress >= 0.25 && tween.progress <= 0.3) {
  279. onWhite(); // 白屏最亮时执行(原逻辑中的"白屏显示时")
  280. }
  281. },
  282. onComplete: () => {
  283. whiteScreen.destroy();
  284. // 确保回调有效后执行
  285. if (typeof onComplete === 'function') {
  286. onComplete();
  287. }
  288. }
  289. });
  290. }
  291. });
  292. }
  293. getSprite() {
  294. return this.sprite;
  295. }
  296. }
  297. // 水果类
  298. class Fruit {
  299. constructor(envConfig) {
  300. this.env = envConfig;
  301. this.game = envConfig.scene;
  302. this.sprite = null;
  303. this.emitterMap = {
  304. "apple": 0xFFC3E925,
  305. "banana": 0xFFFFE337,
  306. "basaha": 0xFFEB2D13,
  307. "peach": 0xFFF8C928,
  308. "sandia": 0xFF739E0F
  309. };
  310. this.bitmap = null;
  311. this.emitter = null;
  312. this.halfOne = null;
  313. this.halfTwo = null;
  314. this.init();
  315. }
  316. init() {
  317. this.sprite = this.game.add.sprite(
  318. this.env.x || 0,
  319. this.env.y || 0,
  320. this.env.key
  321. );
  322. this.sprite.setOrigin(0.5, 0.5);
  323. // 物理属性
  324. this.game.physics.add.existing(this.sprite);
  325. this.sprite.body.setCollideWorldBounds(false);
  326. this.sprite.body.onWorldBounds = true;
  327. // 创建粒子纹理
  328. this.bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
  329. // 粒子发射器
  330. this.emitter = this.game.add.particles(0, 0, 'flameParticle', {
  331. });
  332. }
  333. // 水果类中的half方法
  334. half(deg, shouldEmit = false) {
  335. // 计算世界坐标
  336. const transform = this.sprite.getWorldTransformMatrix();
  337. const worldPos = new Phaser.Math.Vector2(
  338. transform.getX(0, 0),
  339. transform.getY(0, 0)
  340. );
  341. // 1. 计算切割方向的垂直向量(用于分离力)
  342. // 刀刃角度转弧度
  343. const rad = Phaser.Math.DegToRad(deg);
  344. // 垂直于刀刃的方向向量(单位向量)
  345. const sepX = Math.sin(rad); // 垂直方向X分量
  346. const sepY = -Math.cos(rad); // 垂直方向Y分量
  347. // 分离力度(可根据水果大小调整)
  348. const sepForce = 300;
  349. // 2. 创建第一半水果
  350. this.halfOne = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-1');
  351. this.halfOne.setOrigin(0.5, 0.5);
  352. this.halfOne.rotation = Phaser.Math.DegToRad(deg + 45); // 初始角度与刀刃匹配
  353. this.game.physics.add.existing(this.halfOne);
  354. // 速度 = 原速度 + 分离速度(向一侧)
  355. this.halfOne.body.velocity.x = this.sprite.body.velocity.x + sepX * sepForce;
  356. this.halfOne.body.velocity.y = this.sprite.body.velocity.y + sepY * sepForce;
  357. this.halfOne.body.gravity.y = 2000;
  358. // 增加旋转(顺时针)
  359. this.halfOne.body.angularVelocity = 500; // 旋转速度(度/秒)
  360. this.halfOne.body.setCollideWorldBounds(false);
  361. this.halfOne.checkWorldBounds = true;
  362. this.halfOne.outOfBoundsKill = true;
  363. // 3. 创建第二半水果(分离方向相反)
  364. this.halfTwo = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-2');
  365. this.halfTwo.setOrigin(0.5, 0.5);
  366. this.halfTwo.rotation = Phaser.Math.DegToRad(deg + 45);
  367. this.game.physics.add.existing(this.halfTwo);
  368. // 速度 = 原速度 - 分离速度(向另一侧)
  369. this.halfTwo.body.velocity.x = this.sprite.body.velocity.x - sepX * sepForce;
  370. this.halfTwo.body.velocity.y = this.sprite.body.velocity.y - sepY * sepForce;
  371. this.halfTwo.body.gravity.y = 2000;
  372. // 增加旋转(逆时针,与第一半相反)
  373. this.halfTwo.body.angularVelocity = -500;
  374. this.halfTwo.body.setCollideWorldBounds(false);
  375. this.halfTwo.checkWorldBounds = true;
  376. this.halfTwo.outOfBoundsKill = true;
  377. // 4. 原水果透明度渐变消失(替代直接销毁)
  378. this.game.tweens.add({
  379. targets: this.sprite,
  380. alpha: 0, // 透明度从1→0
  381. duration: 100, // 100ms内消失
  382. onComplete: () => {
  383. this.sprite.destroy();
  384. }
  385. });
  386. // 5. 优化粒子效果(沿分离方向飞溅)
  387. if (shouldEmit) {
  388. const emitColor = this.emitterMap[this.sprite.texture.key];
  389. this.generateFlame(this.bitmap, emitColor);
  390. const texture = this.bitmap.generateTexture('fruitParticle', 60, 60);
  391. // 粒子发射器:沿分离方向扩散
  392. this.emitter = this.game.add.particles(0, 0, texture.key, {
  393. x: worldPos.x,
  394. y: worldPos.y,
  395. // 速度方向:以分离方向为中心,±30度范围
  396. angle: {
  397. min: deg - 30,
  398. max: deg + 30
  399. },
  400. speed: { min: 100, max: 300 }, // 速度与分离力度匹配
  401. scale: { start: 0.8, end: 0.1 },
  402. alpha: { start: 1, end: 0.1 },
  403. lifespan: 600, // 延长粒子生命周期,增强视觉效果
  404. maxParticles: 15 // 增加粒子数量
  405. });
  406. }
  407. }
  408. generateFlame(bitmap, color) {
  409. // const len = 30;
  410. // bitmap.clear();
  411. // const rgb = Phaser.Display.Color.IntegerToRGB(color);
  412. // const radgrad = bitmap.context.createRadialGradient(len, len, 4, len, len, len);
  413. // radgrad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`);
  414. // radgrad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
  415. // bitmap.fillStyle(radgrad);
  416. // bitmap.fillRect(0, 0, 2 * len, 2 * len);
  417. const len = 30;
  418. bitmap.clear();
  419. // 将 16 进制颜色转换为 RGB 值(0-255 范围)
  420. const rgb = Phaser.Display.Color.IntegerToRGB(color);
  421. const r = rgb.r / 255; // 转换为 0-1 范围
  422. const g = rgb.g / 255;
  423. const b = rgb.b / 255;
  424. // 使用 Phaser 的径向渐变 API:fillGradientStyle
  425. // 参数说明:
  426. // 1. 渐变类型:1 表示径向渐变
  427. // 2-5. 内圆中心坐标 (x1, y1) 和半径 (r1)
  428. // 6-9. 外圆中心坐标 (x2, y2) 和半径 (r2)
  429. // 10-13. 内圆颜色(r, g, b, a)
  430. // 14-17. 外圆颜色(r, g, b, a)
  431. bitmap.fillGradientStyle(
  432. 1, // 径向渐变
  433. len, len, 4, // 内圆:中心 (len, len),半径 4
  434. len, len, len, // 外圆:中心 (len, len),半径 len
  435. r, g, b, 1, // 内圆颜色(不透明)
  436. r, g, b, 0 // 外圆颜色(透明)
  437. );
  438. // 绘制矩形作为粒子纹理
  439. bitmap.fillRect(0, 0, 2 * len, 2 * len);
  440. }
  441. getSprite() {
  442. return this.sprite;
  443. }
  444. }
  445. // 刀身类
  446. class Blade {
  447. constructor(envConfig) {
  448. this.env = envConfig;
  449. this.game = envConfig.scene;
  450. this.points = [];
  451. this.graphics = null;
  452. this.POINTLIFETIME = 200;
  453. this.allowBlade = false;
  454. this.lastPoint = null;
  455. this.init();
  456. }
  457. init() {
  458. this.graphics = this.game.add.graphics({
  459. x: 0,
  460. y: 0
  461. });
  462. }
  463. update() {
  464. if (this.allowBlade) {
  465. this.graphics.clear();
  466. // 清理过期点
  467. const now = Date.now();
  468. this.points = this.points.filter(point => now - point.time < this.POINTLIFETIME);
  469. if (this.game.input.activePointer.isDown) {
  470. const point = {
  471. x: this.game.input.activePointer.x,
  472. y: this.game.input.activePointer.y,
  473. time: Date.now()
  474. };
  475. if (!this.lastPoint) {
  476. this.lastPoint = point;
  477. this.points.push(point);
  478. } else {
  479. const dis = Math.hypot(
  480. point.x - this.lastPoint.x,
  481. point.y - this.lastPoint.y
  482. );
  483. if (dis > Math.sqrt(300)) { // 相当于距离平方>300
  484. this.lastPoint = point;
  485. this.points.push(point);
  486. }
  487. }
  488. }
  489. if (this.points.length > 0) {
  490. const bladePoints = mathTool.generateBlade(this.points);
  491. if (bladePoints.length > 0) {
  492. this.graphics.fillStyle(0xffffff, 0.8);
  493. this.graphics.beginPath();
  494. this.graphics.moveTo(bladePoints[0].x, bladePoints[0].y);
  495. bladePoints.forEach((point, i) => {
  496. if (i > 0) {
  497. this.graphics.lineTo(point.x, point.y);
  498. }
  499. });
  500. this.graphics.closePath();
  501. this.graphics.fill();
  502. }
  503. }
  504. }
  505. }
  506. checkCollide(sprite, onCollide) {
  507. if (this.allowBlade && this.game.input.activePointer.isDown && this.points.length > 2) {
  508. const bounds = sprite.getBounds();
  509. for (const point of this.points) {
  510. if (Phaser.Geom.Rectangle.Contains(bounds, point.x, point.y)) {
  511. onCollide();
  512. break;
  513. }
  514. }
  515. }
  516. }
  517. collideDeg() {
  518. let deg = 0;
  519. const len = this.points.length;
  520. if (len >= 2) {
  521. const p0 = this.points[0];
  522. const p1 = this.points[len - 1];
  523. if (p0.x === p1.x) {
  524. deg = 90;
  525. } else {
  526. const val = (p0.y - p1.y) / (p0.x - p1.x);
  527. deg = Math.round(Math.atan(val) * 180 / Math.PI);
  528. }
  529. if (deg < 0) {
  530. deg += 180;
  531. }
  532. }
  533. return deg;
  534. }
  535. enable() {
  536. this.allowBlade = true;
  537. }
  538. }
  539. // 启动场景
  540. class BootScene extends Phaser.Scene {
  541. constructor() {
  542. super('boot');
  543. }
  544. preload() {
  545. this.load.image('loading', 'static/images/fruit/preloader.gif');
  546. }
  547. create() {
  548. this.scene.start('preload');
  549. }
  550. }
  551. // 预加载场景
  552. class PreloadScene extends Phaser.Scene {
  553. constructor() {
  554. super('preload');
  555. }
  556. preload() {
  557. this.add.sprite(10, height / 2, 'loading').setPosition((width) / 2, height / 2);
  558. this.load.on('progress', (value) => {
  559. console.log("进度", value)
  560. });
  561. // 加载游戏资源
  562. this.load.image('apple', 'static/images/fruit/apple.png');
  563. this.load.image('apple-1', 'static/images/fruit/apple-1.png');
  564. this.load.image('apple-2', 'static/images/fruit/apple-2.png');
  565. this.load.image('background', 'static/images/fruit/background.jpg');
  566. this.load.image('banana', 'static/images/fruit/banana.png');
  567. this.load.image('banana-1', 'static/images/fruit/banana-1.png');
  568. this.load.image('banana-2', 'static/images/fruit/banana-2.png');
  569. this.load.image('basaha', 'static/images/fruit/basaha.png');
  570. this.load.image('basaha-1', 'static/images/fruit/basaha-1.png');
  571. this.load.image('basaha-2', 'static/images/fruit/basaha-2.png');
  572. this.load.image('best', 'static/images/fruit/best.png');
  573. this.load.image('bomb', 'static/images/fruit/bomb.png');
  574. this.load.image('dojo', 'static/images/fruit/dojo.png');
  575. this.load.image('game-over', 'static/images/fruit/game-over.png');
  576. this.load.image('home-desc', 'static/images/fruit/home-desc.png');
  577. this.load.image('home-mask', 'static/images/fruit/home-mask.png');
  578. this.load.image('logo', 'static/images/fruit/logo.png');
  579. this.load.image('lose', 'static/images/fruit/lose.png');
  580. this.load.image('new-game', 'static/images/fruit/new-game.png');
  581. this.load.image('peach', 'static/images/fruit/peach.png');
  582. this.load.image('peach-1', 'static/images/fruit/peach-1.png');
  583. this.load.image('peach-2', 'static/images/fruit/peach-2.png');
  584. this.load.image('quit', 'static/images/fruit/quit.png');
  585. this.load.image('sandia', 'static/images/fruit/sandia.png');
  586. this.load.image('sandia-1', 'static/images/fruit/sandia-1.png');
  587. this.load.image('sandia-2', 'static/images/fruit/sandia-2.png');
  588. this.load.image('score', 'static/images/fruit/score.png');
  589. this.load.image('shadow', 'static/images/fruit/shadow.png');
  590. this.load.image('smoke', 'static/images/fruit/smoke.png');
  591. this.load.image('x', 'static/images/fruit/x.png');
  592. this.load.image('xf', 'static/images/fruit/xf.png');
  593. this.load.image('xx', 'static/images/fruit/xx.png');
  594. this.load.image('xxf', 'static/images/fruit/xxf.png');
  595. this.load.image('xxx', 'static/images/fruit/xxx.png');
  596. this.load.image('xxxf', 'static/images/fruit/xxxf.png');
  597. this.load.bitmapFont('number', 'static/images/fruit/bitmapFont.png', 'static/images/fruit/bitmapFont.xml');
  598. }
  599. create() {
  600. this.scene.start('main');
  601. }
  602. }
  603. // 主场景
  604. class MainScene extends Phaser.Scene {
  605. constructor() {
  606. super('main');
  607. this.bg = null;
  608. this.blade = null;
  609. this.homeGroup = null;
  610. this.home_mask = null;
  611. this.logo = null;
  612. this.home_desc = null;
  613. this.sandiaGroup = null;
  614. this.new_game = null;
  615. this.sandia = null;
  616. this.lose = null;
  617. this.start = false;
  618. this.sandiaRotateSpeed = 0.9;
  619. this.newGameRotateSpeed = -0.3;
  620. }
  621. create() {
  622. // 初始化物理系统
  623. this.physics.world.gravity.y = 0;
  624. // 背景
  625. this.bg = this.add.image(0, 0, "background");
  626. this.bg.setScale(wRatio, hRatio);
  627. this.bg.setPosition(width / 2, height / 2);
  628. this.bg.setOrigin(0.5, 0.5);
  629. // 刀光
  630. this.blade = new Blade({
  631. scene: this
  632. });
  633. // 开始动画
  634. this.homeGroupAnim();
  635. }
  636. update() {
  637. this.updateRotate();
  638. // 检查是否该跳转到游戏场景
  639. // if (this.start) {
  640. // this.gotoNextScene();
  641. // }
  642. // 更新刀光
  643. this.blade.update();
  644. // 检查水果碰撞
  645. if (this.sandia && this.sandia.getSprite() && this.sandia.getSprite().active && !this.start) {
  646. this.blade.checkCollide(
  647. this.sandia.getSprite(),
  648. () => {
  649. this.startGame();
  650. }
  651. );
  652. }
  653. }
  654. homeGroupAnim() {
  655. //创建组合默认先隐藏
  656. this.homeGroup = this.add.container(0, -height);
  657. //背景蒙版
  658. this.home_mask = this.add.image(0, 0, "home-mask");
  659. this.home_mask.setOrigin(0, 0);
  660. this.home_mask.setScale(wRatio);
  661. this.home_mask.y = -200;
  662. //logo
  663. this.logo = this.add.image(20, 50, "logo");
  664. this.logo.setOrigin(0, 0);
  665. //提示语
  666. this.home_desc = this.add.image(0, 0, "home-desc");
  667. this.home_desc.setPosition((width - this.home_desc.width / 2) - 20, 70);
  668. //合并图层
  669. this.homeGroup.add([this.home_mask, this.logo, this.home_desc]);
  670. // 退出按钮
  671. this.lose = this.add.image(0, 0, "lose");
  672. this.lose.setPosition(width - this.lose.width - 30, height - this.lose.height - 30);
  673. this.lose.setInteractive();
  674. // 绑定点击事件
  675. this.lose.on('pointerdown', () => this.getExit());
  676. //动画效果,接着展示西瓜
  677. this.tweens.add({
  678. targets: this.homeGroup,
  679. y: 0,
  680. duration: 400,
  681. ease: 'Sine.InOut',
  682. onComplete: () => this.fruitAnim()
  683. });
  684. }
  685. fruitAnim() {
  686. // 每次创建全新的容器,避免复用旧实例
  687. this.sandiaGroup = this.add.container(0, 0); // 先置0,后续重新计算
  688. // 西瓜组初始位置:基于当前窗口尺寸动态计算(核心修复)
  689. const initX = 323 * wRatio;
  690. const initY = 373 * hRatio;
  691. this.sandiaGroup.setPosition(initX, initY); // 强制设置位置
  692. //圆圈
  693. this.new_game = this.add.sprite(0, 0, "new-game");
  694. this.new_game.setOrigin(0.5, 0.5);
  695. //西瓜
  696. this.sandia = new Fruit({ scene: this, key: "sandia" });
  697. this.sandiaGroup.add([this.new_game, this.sandia.getSprite()]);
  698. //动画效果,接着开放鼠标事件
  699. this.tweens.add({
  700. targets: this.sandiaGroup,
  701. scale: 1,
  702. duration: 500,
  703. ease: 'Linear.None',
  704. onComplete: () => this.allowBlade()
  705. });
  706. }
  707. updateRotate() {
  708. //西瓜外框圆圈图片旋转
  709. if (this.new_game) {
  710. this.new_game.rotation += this.newGameRotateSpeed * 0.016;
  711. }
  712. if (this.sandia && this.sandia.getSprite()) {
  713. this.sandia.getSprite().rotation += this.sandiaRotateSpeed * 0.016;
  714. }
  715. }
  716. allowBlade() {
  717. this.blade.enable();
  718. }
  719. startGame() {
  720. // this.start = true;
  721. // // 隐藏主界面元素
  722. // this.tweens.add({
  723. // targets: this.homeGroup,
  724. // y: -height,
  725. // duration: 200,
  726. // ease: 'Sine.InOut'
  727. // });
  728. // // 隐藏按钮
  729. // this.new_game.destroy();
  730. // this.lose.destroy();
  731. // // 切开西瓜
  732. // const deg = this.blade.collideDeg();
  733. // this.sandia.half(deg);
  734. this.start = true; // 先禁用立即切换
  735. const deg = this.blade.collideDeg();
  736. this.sandia.half(deg); // 切开西瓜,生成两半
  737. // 延迟1秒(1000ms)后再切换场景,等待动画展示
  738. setTimeout(() => {
  739. this.gotoNextScene();
  740. }, 1000);
  741. }
  742. gotoNextScene() {
  743. this.resetScene();
  744. this.scene.start("play");
  745. }
  746. getExit() {
  747. console.log("退出");
  748. router.push({ path: '/game' });
  749. }
  750. resetScene() {
  751. this.sandia = null;
  752. this.start = false;
  753. // 新增:销毁西瓜容器,避免残留
  754. if (this.sandiaGroup) {
  755. this.sandiaGroup.destroy(); // 销毁容器及其子元素
  756. this.sandiaGroup = null; // 置空引用
  757. }
  758. }
  759. }
  760. // 游戏场景
  761. class PlayScene extends Phaser.Scene {
  762. constructor() {
  763. super('play');
  764. this.bg = null;
  765. this.blade = null;
  766. this.fruits = [];
  767. this.score = 0;
  768. this.playing = false; // 改为false,在初始化方法中设置为true
  769. this.bombExplode = false;
  770. this.lostCount = 0;
  771. this.scoreImage = null;
  772. this.best = null;
  773. this.scoreText = null;
  774. this.xxxGroup = null;
  775. this.x = null;
  776. this.xx = null;
  777. this.xxx = null;
  778. this.gravity = 1000;
  779. }
  780. create() {
  781. // 物理系统
  782. this.physics.world.gravity.y = this.gravity;
  783. // 背景
  784. // this.bg = this.add.image(0, 0, 'background');
  785. // this.bg.setScale(wRatio, hRatio);
  786. // this.bg.setPosition(width / 2, height / 2);
  787. // this.bg.setOrigin(0, 0);
  788. this.bg = this.add.image(0, 0, "background");
  789. // 设置背景图铺满整个游戏区域
  790. this.bg.displayWidth = width;
  791. this.bg.displayHeight = height;
  792. // 设置背景图原点为左上角
  793. this.bg.setOrigin(0, 0);
  794. // 刀光
  795. this.blade = new Blade({
  796. scene: this
  797. });
  798. this.blade.enable();
  799. // 初始化UI
  800. this.scoreAnim();
  801. this.scoreTextAnim();
  802. this.bestAnim();
  803. this.xxxAnim();
  804. // 添加调试信息
  805. console.log("PlayScene created");
  806. // 调用初始化方法,而不是直接开始生成水果
  807. this.initGame();
  808. }
  809. initGame() {
  810. // 重置游戏状态
  811. this.fruits = [];
  812. this.score = 0;
  813. this.lostCount = 0;
  814. this.bombExplode = false;
  815. this.scoreText.setText(this.score.toString());
  816. // 重置失去计数UI
  817. if (this.xxxGroup) {
  818. this.xxxGroup.removeAll(true);
  819. this.x = this.add.image(0, 0, 'x');
  820. this.xx = this.add.image(22, 0, 'xx');
  821. this.xxx = this.add.image(49, 0, 'xxx');
  822. this.x.setOrigin(0, 0);
  823. this.xx.setOrigin(0, 0);
  824. this.xxx.setOrigin(0, 0);
  825. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  826. }
  827. // 开始游戏
  828. this.playing = true;
  829. console.log("Game initialized and started");
  830. // 延迟一点时间再生成第一个水果,让玩家有准备
  831. this.time.delayedCall(1000, () => {
  832. this.startFruit();
  833. console.log("First fruit spawned");
  834. });
  835. }
  836. update() {
  837. // 如果游戏未开始,不执行任何操作
  838. if (!this.playing) return;
  839. // 检查是否有水果出界
  840. if (!this.bombExplode) {
  841. for (let i = this.fruits.length - 1; i >= 0; i--) {
  842. const fruit = this.fruits[i];
  843. const sprite = fruit.getSprite();
  844. if (sprite && !sprite.active) continue;
  845. if (sprite && (
  846. sprite.y > height + 100 ||
  847. sprite.x < -100 ||
  848. sprite.x > width + 100
  849. )) {
  850. if (fruit.isFruit) {
  851. this.onOut(fruit);
  852. }
  853. sprite.destroy();
  854. this.fruits.splice(i, 1);
  855. }
  856. }
  857. }
  858. // 如果没有水果且游戏进行中,生成新水果
  859. if (this.playing && this.fruits.length === 0 && !this.bombExplode) {
  860. this.startFruit();
  861. }
  862. // 更新刀光
  863. this.blade.update();
  864. // 检查碰撞
  865. if (!this.bombExplode) {
  866. this.fruits.forEach((fruit, i) => {
  867. if (fruit.getSprite() && fruit.getSprite().active) {
  868. this.blade.checkCollide(
  869. fruit.getSprite(),
  870. () => {
  871. if (fruit.isFruit) {
  872. this.onKill(fruit);
  873. this.fruits.splice(i, 1);
  874. } else {
  875. this.onBomb(fruit);
  876. }
  877. }
  878. );
  879. }
  880. });
  881. }
  882. }
  883. scoreAnim() {
  884. this.scoreImage = this.add.image(-100, 8, 'score');
  885. this.scoreImage.setOrigin(0, 0);
  886. this.tweens.add({
  887. targets: this.scoreImage,
  888. x: 8,
  889. duration: 300,
  890. ease: 'Sine.InOut'
  891. });
  892. }
  893. bestAnim() {
  894. this.best = this.add.image(-100, 52, 'best');
  895. this.best.setOrigin(0, 0);
  896. this.tweens.add({
  897. targets: this.best,
  898. x: 5,
  899. duration: 300,
  900. ease: 'Sine.InOut'
  901. });
  902. }
  903. scoreTextAnim() {
  904. this.scoreText = this.add.bitmapText(-100, 40, 'number', this.score.toString(), 32);
  905. this.scoreText.setOrigin(0, 0);
  906. this.tweens.add({
  907. targets: this.scoreText,
  908. x: 75,
  909. duration: 300,
  910. ease: 'Sine.InOut'
  911. });
  912. }
  913. xxxAnim() {
  914. this.xxxGroup = this.add.container(width + 100, 5);
  915. this.x = this.add.image(0, 0, 'x');
  916. this.xx = this.add.image(22, 0, 'xx');
  917. this.xxx = this.add.image(49, 0, 'xxx');
  918. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  919. this.tweens.add({
  920. targets: this.xxxGroup,
  921. x: width - 86,
  922. duration: 300,
  923. ease: 'Sine.InOut'
  924. });
  925. }
  926. startFruit() {
  927. const number = Math.floor(mathTool.randomMinMax(1, 5));
  928. const hasBomb = Math.random() > 0.3; // 30%概率有炸弹
  929. const bombIndex = hasBomb ? Math.floor(Math.random() * number) : -1;
  930. for (let i = 0; i < number; i++) {
  931. if (i === bombIndex) {
  932. this.fruits.push(this.randomFruit(false));
  933. } else {
  934. this.fruits.push(this.randomFruit(true));
  935. }
  936. }
  937. }
  938. randomFruit(isFruit) {
  939. const fruitArray = ["apple", "banana", "basaha", "peach", "sandia"];
  940. const index = Math.floor(Math.random() * fruitArray.length);
  941. const x = mathTool.randomPosX();
  942. const y = mathTool.randomPosY();
  943. const vx = mathTool.randomVelocityX(x);
  944. const vy = mathTool.randomVelocityY();
  945. let fruit;
  946. if (isFruit) {
  947. fruit = new Fruit({
  948. scene: this,
  949. key: fruitArray[index],
  950. x: x,
  951. y: y
  952. });
  953. } else {
  954. fruit = new Bomb({
  955. scene: this,
  956. x: x,
  957. y: y
  958. });
  959. }
  960. console.log("isFruitisFruitisFruit",isFruit)
  961. fruit.isFruit = isFruit;
  962. const sprite = fruit.getSprite();
  963. if (sprite.body) {
  964. sprite.body.velocity.x = vx;
  965. sprite.body.velocity.y = vy;
  966. sprite.body.gravity.y = this.gravity;
  967. }
  968. return fruit;
  969. }
  970. onOut(fruit) {
  971. const sprite = fruit.getSprite();
  972. let x, y;
  973. // 确定失去标记的位置
  974. if (sprite.y > height) {
  975. x = sprite.x;
  976. y = height - 30;
  977. } else if (sprite.x < 0) {
  978. x = 30;
  979. y = sprite.y;
  980. } else {
  981. x = width - 30;
  982. y = sprite.y;
  983. }
  984. // 创建失去标记动画
  985. const lose = this.add.sprite(x, y, 'lose');
  986. lose.setOrigin(0.5, 0.5);
  987. lose.setScale(0);
  988. const tweenShow = this.tweens.add({
  989. targets: lose,
  990. scale: 1,
  991. duration: 300,
  992. ease: 'Sine.InOut',
  993. paused: true
  994. });
  995. const tweenHide = this.tweens.add({
  996. targets: lose,
  997. scale: 0,
  998. duration: 300,
  999. ease: 'Sine.InOut',
  1000. paused: true,
  1001. delay: 1000
  1002. });
  1003. this.tweens.chain({
  1004. targets: lose,
  1005. tweens: [
  1006. {
  1007. scale: 1,
  1008. duration: 300,
  1009. ease: 'Sine.InOut'
  1010. },
  1011. {
  1012. scale: 0,
  1013. duration: 300,
  1014. ease: 'Sine.InOut',
  1015. delay: 1000,
  1016. onComplete: () => {
  1017. lose.destroy();
  1018. }
  1019. }
  1020. ]
  1021. });
  1022. tweenShow.play();
  1023. tweenHide.on('complete', () => {
  1024. lose.destroy();
  1025. });
  1026. this.lostCount++;
  1027. this.loseCount();
  1028. }
  1029. onKill(fruit) {
  1030. const deg = this.blade.collideDeg();
  1031. fruit.half(deg, true);
  1032. this.score++;
  1033. this.scoreText.setText(this.score.toString());
  1034. }
  1035. onBomb(bomb) {
  1036. this.bombExplode = true;
  1037. // 停止所有水果的物理运动
  1038. this.fruits.forEach(fruit => {
  1039. if (fruit.getSprite() && fruit.getSprite().body) {
  1040. fruit.getSprite().body.setVelocity(0);
  1041. fruit.getSprite().body.setGravity(0);
  1042. }
  1043. });
  1044. // 炸弹爆炸
  1045. bomb.explode(
  1046. () => {
  1047. // 白屏显示时的回调:销毁所有水果
  1048. this.fruits.forEach(fruit => {
  1049. if (fruit.getSprite()) {
  1050. fruit.getSprite().destroy();
  1051. }
  1052. });
  1053. this.fruits = [];
  1054. },
  1055. () => {
  1056. // 爆炸完成后的回调
  1057. this.gameOver();
  1058. }
  1059. );
  1060. }
  1061. loseCount() {
  1062. if (this.lostCount === 1) {
  1063. this.lostAnim(this.x, 'xf');
  1064. } else if (this.lostCount === 2) {
  1065. this.lostAnim(this.xx, 'xxf');
  1066. } else if (this.lostCount >= 3) {
  1067. this.lostAnim(this.xxx, 'xxxf');
  1068. this.gameOver();
  1069. }
  1070. }
  1071. lostAnim(removeObj, addKey) {
  1072. removeObj.destroy();
  1073. const newObj = this.add.sprite(removeObj.x, removeObj.y, addKey);
  1074. newObj.setOrigin(0, 0);
  1075. newObj.setScale(0);
  1076. this.xxxGroup.add(newObj);
  1077. this.tweens.add({
  1078. targets: newObj,
  1079. scale: 1,
  1080. duration: 300,
  1081. ease: 'Sine.InOut'
  1082. });
  1083. }
  1084. gameOver() {
  1085. this.playing = false;
  1086. // 1. 确保所有其他元素停止更新,避免干扰
  1087. this.blade.allowBlade = false; // 禁用刀光
  1088. // 2. 创建game-over图片,并设置最高层级
  1089. const gameOverSprite = this.add.sprite(width / 2, height / 2, 'game-over');
  1090. gameOverSprite.setOrigin(0.5, 0.5);
  1091. gameOverSprite.setScale(0);
  1092. gameOverSprite.setDepth(1000); // 设置最高层级,确保不被覆盖
  1093. // 3. 优化入场动画,确保平滑显示
  1094. this.tweens.add({
  1095. targets: gameOverSprite,
  1096. scale: 1,
  1097. duration: 500, // 延长动画时间,确保可见
  1098. ease: 'Elastic.Out', // 更明显的弹性动画,增强视觉效果
  1099. onComplete: () => {
  1100. // 动画完成后再设置自动返回,确保用户有足够时间看到画面
  1101. setTimeout(() => {
  1102. console.log('游戏结束,返回首页');
  1103. this.scene.start('main');
  1104. }, 3000); // 延长至3秒,给用户足够时间观察
  1105. }
  1106. });
  1107. // 4. 支持点击立即返回,提升交互体验
  1108. gameOverSprite.setInteractive();
  1109. gameOverSprite.on('pointerdown', () => {
  1110. console.log('点击返回首页');
  1111. this.scene.start('main');
  1112. });
  1113. }
  1114. }
  1115. // 初始化游戏
  1116. onMounted(() => {
  1117. // 获取容器尺寸
  1118. const container = document.getElementById('game');
  1119. // 初始化工具类
  1120. mathTool.init();
  1121. // 创建游戏实例
  1122. game = new Phaser.Game({
  1123. type: Phaser.CANVAS,
  1124. width: width,
  1125. height: height,
  1126. parent: 'game',
  1127. scene: [BootScene, PreloadScene, MainScene, PlayScene],
  1128. physics: {
  1129. default: 'arcade',
  1130. arcade: {
  1131. debug: false
  1132. }
  1133. }
  1134. });
  1135. });
  1136. </script>
  1137. <style lang="scss" scoped></style>