fruit.vue 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  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. // 手动计算世界坐标 (替代getWorldPosition)
  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. // 创建两半水果,位置严格等于原水果的世界坐标
  342. this.halfOne = this.game.add.sprite(
  343. worldPos.x, // 使用计算出的世界坐标x
  344. worldPos.y, // 使用计算出的世界坐标y
  345. this.sprite.texture.key + '-1'
  346. );
  347. this.halfOne.setOrigin(0.5, 0.5);
  348. this.halfOne.rotation = Phaser.Math.DegToRad(deg + 45);
  349. // 物理属性:初始速度归零,避免瞬间偏移
  350. this.game.physics.add.existing(this.halfOne);
  351. this.halfOne.body.velocity.x = this.sprite.body.velocity.x; // 仅继承原速度
  352. this.halfOne.body.velocity.y = this.sprite.body.velocity.y;
  353. this.halfOne.body.gravity.y = 2000;
  354. this.halfOne.body.setCollideWorldBounds(false);
  355. this.halfOne.checkWorldBounds = true;
  356. this.halfOne.outOfBoundsKill = true;
  357. // 第二半水果同理
  358. this.halfTwo = this.game.add.sprite(
  359. worldPos.x, // 使用计算出的世界坐标x
  360. worldPos.y, // 使用计算出的世界坐标y
  361. this.sprite.texture.key + '-2'
  362. );
  363. this.halfTwo.setOrigin(0.5, 0.5);
  364. this.halfTwo.rotation = Phaser.Math.DegToRad(deg + 45);
  365. this.game.physics.add.existing(this.halfTwo);
  366. this.halfTwo.body.velocity.x = this.sprite.body.velocity.x; // 仅继承原速度
  367. this.halfTwo.body.velocity.y = this.sprite.body.velocity.y;
  368. this.halfTwo.body.gravity.y = 2000;
  369. this.halfTwo.body.setCollideWorldBounds(false);
  370. this.halfTwo.checkWorldBounds = true;
  371. this.halfTwo.outOfBoundsKill = true;
  372. // 延迟销毁原水果,确保视觉上"替换"而非"突然消失"
  373. setTimeout(() => {
  374. this.sprite.destroy();
  375. }, 50);
  376. // 粒子效果
  377. if (shouldEmit) {
  378. const emitColor = this.emitterMap[this.sprite.texture.key];
  379. this.generateFlame(this.bitmap, emitColor);
  380. const texture = this.bitmap.generateTexture('fruitParticle', 60, 60);
  381. this.emitter = this.game.add.particles(0, 0, texture.key, {
  382. x: worldPos.x,
  383. y: worldPos.y,
  384. speed: { min: -400, max: 400 },
  385. scale: { start: 1, end: 0.1 },
  386. alpha: { start: 1, end: 0.1 },
  387. lifespan: 4000,
  388. maxParticles: 10
  389. });
  390. }
  391. }
  392. generateFlame(bitmap, color) {
  393. // const len = 30;
  394. // bitmap.clear();
  395. // const rgb = Phaser.Display.Color.IntegerToRGB(color);
  396. // const radgrad = bitmap.context.createRadialGradient(len, len, 4, len, len, len);
  397. // radgrad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`);
  398. // radgrad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
  399. // bitmap.fillStyle(radgrad);
  400. // bitmap.fillRect(0, 0, 2 * len, 2 * len);
  401. const len = 30;
  402. bitmap.clear();
  403. // 将 16 进制颜色转换为 RGB 值(0-255 范围)
  404. const rgb = Phaser.Display.Color.IntegerToRGB(color);
  405. const r = rgb.r / 255; // 转换为 0-1 范围
  406. const g = rgb.g / 255;
  407. const b = rgb.b / 255;
  408. // 使用 Phaser 的径向渐变 API:fillGradientStyle
  409. // 参数说明:
  410. // 1. 渐变类型:1 表示径向渐变
  411. // 2-5. 内圆中心坐标 (x1, y1) 和半径 (r1)
  412. // 6-9. 外圆中心坐标 (x2, y2) 和半径 (r2)
  413. // 10-13. 内圆颜色(r, g, b, a)
  414. // 14-17. 外圆颜色(r, g, b, a)
  415. bitmap.fillGradientStyle(
  416. 1, // 径向渐变
  417. len, len, 4, // 内圆:中心 (len, len),半径 4
  418. len, len, len, // 外圆:中心 (len, len),半径 len
  419. r, g, b, 1, // 内圆颜色(不透明)
  420. r, g, b, 0 // 外圆颜色(透明)
  421. );
  422. // 绘制矩形作为粒子纹理
  423. bitmap.fillRect(0, 0, 2 * len, 2 * len);
  424. }
  425. getSprite() {
  426. return this.sprite;
  427. }
  428. }
  429. // 刀身类
  430. class Blade {
  431. constructor(envConfig) {
  432. this.env = envConfig;
  433. this.game = envConfig.scene;
  434. this.points = [];
  435. this.graphics = null;
  436. this.POINTLIFETIME = 200;
  437. this.allowBlade = false;
  438. this.lastPoint = null;
  439. this.init();
  440. }
  441. init() {
  442. this.graphics = this.game.add.graphics({
  443. x: 0,
  444. y: 0
  445. });
  446. }
  447. update() {
  448. if (this.allowBlade) {
  449. this.graphics.clear();
  450. // 清理过期点
  451. const now = Date.now();
  452. this.points = this.points.filter(point => now - point.time < this.POINTLIFETIME);
  453. if (this.game.input.activePointer.isDown) {
  454. const point = {
  455. x: this.game.input.activePointer.x,
  456. y: this.game.input.activePointer.y,
  457. time: Date.now()
  458. };
  459. if (!this.lastPoint) {
  460. this.lastPoint = point;
  461. this.points.push(point);
  462. } else {
  463. const dis = Math.hypot(
  464. point.x - this.lastPoint.x,
  465. point.y - this.lastPoint.y
  466. );
  467. if (dis > Math.sqrt(300)) { // 相当于距离平方>300
  468. this.lastPoint = point;
  469. this.points.push(point);
  470. }
  471. }
  472. }
  473. if (this.points.length > 0) {
  474. const bladePoints = mathTool.generateBlade(this.points);
  475. if (bladePoints.length > 0) {
  476. this.graphics.fillStyle(0xffffff, 0.8);
  477. this.graphics.beginPath();
  478. this.graphics.moveTo(bladePoints[0].x, bladePoints[0].y);
  479. bladePoints.forEach((point, i) => {
  480. if (i > 0) {
  481. this.graphics.lineTo(point.x, point.y);
  482. }
  483. });
  484. this.graphics.closePath();
  485. this.graphics.fill();
  486. }
  487. }
  488. }
  489. }
  490. checkCollide(sprite, onCollide) {
  491. if (this.allowBlade && this.game.input.activePointer.isDown && this.points.length > 2) {
  492. const bounds = sprite.getBounds();
  493. for (const point of this.points) {
  494. if (Phaser.Geom.Rectangle.Contains(bounds, point.x, point.y)) {
  495. onCollide();
  496. break;
  497. }
  498. }
  499. }
  500. }
  501. collideDeg() {
  502. let deg = 0;
  503. const len = this.points.length;
  504. if (len >= 2) {
  505. const p0 = this.points[0];
  506. const p1 = this.points[len - 1];
  507. if (p0.x === p1.x) {
  508. deg = 90;
  509. } else {
  510. const val = (p0.y - p1.y) / (p0.x - p1.x);
  511. deg = Math.round(Math.atan(val) * 180 / Math.PI);
  512. }
  513. if (deg < 0) {
  514. deg += 180;
  515. }
  516. }
  517. return deg;
  518. }
  519. enable() {
  520. this.allowBlade = true;
  521. }
  522. }
  523. // 启动场景
  524. class BootScene extends Phaser.Scene {
  525. constructor() {
  526. super('boot');
  527. }
  528. preload() {
  529. this.load.image('loading', 'static/images/fruit/preloader.gif');
  530. }
  531. create() {
  532. this.scene.start('preload');
  533. }
  534. }
  535. // 预加载场景
  536. class PreloadScene extends Phaser.Scene {
  537. constructor() {
  538. super('preload');
  539. }
  540. preload() {
  541. this.add.sprite(10, height / 2, 'loading').setPosition((width) / 2, height / 2);
  542. this.load.on('progress', (value) => {
  543. console.log("进度", value)
  544. });
  545. // 加载游戏资源
  546. this.load.image('apple', 'static/images/fruit/apple.png');
  547. this.load.image('apple-1', 'static/images/fruit/apple-1.png');
  548. this.load.image('apple-2', 'static/images/fruit/apple-2.png');
  549. this.load.image('background', 'static/images/fruit/background.jpg');
  550. this.load.image('banana', 'static/images/fruit/banana.png');
  551. this.load.image('banana-1', 'static/images/fruit/banana-1.png');
  552. this.load.image('banana-2', 'static/images/fruit/banana-2.png');
  553. this.load.image('basaha', 'static/images/fruit/basaha.png');
  554. this.load.image('basaha-1', 'static/images/fruit/basaha-1.png');
  555. this.load.image('basaha-2', 'static/images/fruit/basaha-2.png');
  556. this.load.image('best', 'static/images/fruit/best.png');
  557. this.load.image('bomb', 'static/images/fruit/bomb.png');
  558. this.load.image('dojo', 'static/images/fruit/dojo.png');
  559. this.load.image('game-over', 'static/images/fruit/game-over.png');
  560. this.load.image('home-desc', 'static/images/fruit/home-desc.png');
  561. this.load.image('home-mask', 'static/images/fruit/home-mask.png');
  562. this.load.image('logo', 'static/images/fruit/logo.png');
  563. this.load.image('lose', 'static/images/fruit/lose.png');
  564. this.load.image('new-game', 'static/images/fruit/new-game.png');
  565. this.load.image('peach', 'static/images/fruit/peach.png');
  566. this.load.image('peach-1', 'static/images/fruit/peach-1.png');
  567. this.load.image('peach-2', 'static/images/fruit/peach-2.png');
  568. this.load.image('quit', 'static/images/fruit/quit.png');
  569. this.load.image('sandia', 'static/images/fruit/sandia.png');
  570. this.load.image('sandia-1', 'static/images/fruit/sandia-1.png');
  571. this.load.image('sandia-2', 'static/images/fruit/sandia-2.png');
  572. this.load.image('score', 'static/images/fruit/score.png');
  573. this.load.image('shadow', 'static/images/fruit/shadow.png');
  574. this.load.image('smoke', 'static/images/fruit/smoke.png');
  575. this.load.image('x', 'static/images/fruit/x.png');
  576. this.load.image('xf', 'static/images/fruit/xf.png');
  577. this.load.image('xx', 'static/images/fruit/xx.png');
  578. this.load.image('xxf', 'static/images/fruit/xxf.png');
  579. this.load.image('xxx', 'static/images/fruit/xxx.png');
  580. this.load.image('xxxf', 'static/images/fruit/xxxf.png');
  581. this.load.bitmapFont('number', 'static/images/fruit/bitmapFont.png', 'static/images/fruit/bitmapFont.xml');
  582. }
  583. create() {
  584. this.scene.start('main');
  585. }
  586. }
  587. // 主场景
  588. class MainScene extends Phaser.Scene {
  589. constructor() {
  590. super('main');
  591. this.bg = null;
  592. this.blade = null;
  593. this.homeGroup = null;
  594. this.home_mask = null;
  595. this.logo = null;
  596. this.home_desc = null;
  597. this.sandiaGroup = null;
  598. this.new_game = null;
  599. this.sandia = null;
  600. this.lose = null;
  601. this.start = false;
  602. this.sandiaRotateSpeed = 0.9;
  603. this.newGameRotateSpeed = -0.3;
  604. }
  605. create() {
  606. // 初始化物理系统
  607. this.physics.world.gravity.y = 0;
  608. // 背景
  609. this.bg = this.add.image(0, 0, "background");
  610. this.bg.setScale(wRatio, hRatio);
  611. this.bg.setPosition(width / 2, height / 2);
  612. this.bg.setOrigin(0.5, 0.5);
  613. // 刀光
  614. this.blade = new Blade({
  615. scene: this
  616. });
  617. // 开始动画
  618. this.homeGroupAnim();
  619. }
  620. update() {
  621. this.updateRotate();
  622. // 检查是否该跳转到游戏场景
  623. // if (this.start) {
  624. // this.gotoNextScene();
  625. // }
  626. // 更新刀光
  627. this.blade.update();
  628. // 检查水果碰撞
  629. if (this.sandia && this.sandia.getSprite() && this.sandia.getSprite().active && !this.start) {
  630. this.blade.checkCollide(
  631. this.sandia.getSprite(),
  632. () => {
  633. this.startGame();
  634. }
  635. );
  636. }
  637. }
  638. homeGroupAnim() {
  639. //创建组合默认先隐藏
  640. this.homeGroup = this.add.container(0, -height);
  641. //背景蒙版
  642. this.home_mask = this.add.image(0, 0, "home-mask");
  643. this.home_mask.setOrigin(0, 0);
  644. this.home_mask.setScale(wRatio);
  645. this.home_mask.y = -200;
  646. //logo
  647. this.logo = this.add.image(20, 50, "logo");
  648. this.logo.setOrigin(0, 0);
  649. //提示语
  650. this.home_desc = this.add.image(0, 0, "home-desc");
  651. this.home_desc.setPosition((width - this.home_desc.width / 2) - 20, 70);
  652. //合并图层
  653. this.homeGroup.add([this.home_mask, this.logo, this.home_desc]);
  654. // 退出按钮
  655. this.lose = this.add.image(0, 0, "lose");
  656. this.lose.setPosition(width - this.lose.width - 30, height - this.lose.height - 30);
  657. this.lose.setInteractive();
  658. // 绑定点击事件
  659. this.lose.on('pointerdown', () => this.getExit());
  660. //动画效果,接着展示西瓜
  661. this.tweens.add({
  662. targets: this.homeGroup,
  663. y: 0,
  664. duration: 400,
  665. ease: 'Sine.InOut',
  666. onComplete: () => this.fruitAnim()
  667. });
  668. }
  669. fruitAnim() {
  670. // 西瓜组
  671. this.sandiaGroup = this.add.container(323 * wRatio, 373 * hRatio);
  672. this.sandiaGroup.setScale(0);
  673. //圆圈
  674. this.new_game = this.add.sprite(0, 0, "new-game");
  675. this.new_game.setOrigin(0.5, 0.5);
  676. //西瓜
  677. this.sandia = new Fruit({ scene: this, key: "sandia" });
  678. this.sandiaGroup.add([this.new_game, this.sandia.getSprite()]);
  679. //动画效果,接着开放鼠标事件
  680. this.tweens.add({
  681. targets: this.sandiaGroup,
  682. scale: 1,
  683. duration: 500,
  684. ease: 'Linear.None',
  685. onComplete: () => this.allowBlade()
  686. });
  687. }
  688. updateRotate() {
  689. //西瓜外框圆圈图片旋转
  690. if (this.new_game) {
  691. this.new_game.rotation += this.newGameRotateSpeed * 0.016;
  692. }
  693. if (this.sandia && this.sandia.getSprite()) {
  694. this.sandia.getSprite().rotation += this.sandiaRotateSpeed * 0.016;
  695. }
  696. }
  697. allowBlade() {
  698. this.blade.enable();
  699. }
  700. startGame() {
  701. // this.start = true;
  702. // // 隐藏主界面元素
  703. // this.tweens.add({
  704. // targets: this.homeGroup,
  705. // y: -height,
  706. // duration: 200,
  707. // ease: 'Sine.InOut'
  708. // });
  709. // // 隐藏按钮
  710. // this.new_game.destroy();
  711. // this.lose.destroy();
  712. // // 切开西瓜
  713. // const deg = this.blade.collideDeg();
  714. // this.sandia.half(deg);
  715. this.start = false; // 先禁用立即切换
  716. const deg = this.blade.collideDeg();
  717. this.sandia.half(deg); // 切开西瓜,生成两半
  718. // 延迟1秒(1000ms)后再切换场景,等待动画展示
  719. setTimeout(() => {
  720. this.gotoNextScene();
  721. }, 1000);
  722. }
  723. gotoNextScene() {
  724. this.resetScene();
  725. this.scene.start("play");
  726. }
  727. getExit() {
  728. console.log("退出");
  729. router.push({ path: '/game' });
  730. }
  731. resetScene() {
  732. this.sandia = null;
  733. this.start = false;
  734. }
  735. }
  736. // 游戏场景
  737. class PlayScene extends Phaser.Scene {
  738. constructor() {
  739. super('play');
  740. this.bg = null;
  741. this.blade = null;
  742. this.fruits = [];
  743. this.score = 0;
  744. this.playing = false; // 改为false,在初始化方法中设置为true
  745. this.bombExplode = false;
  746. this.lostCount = 0;
  747. this.scoreImage = null;
  748. this.best = null;
  749. this.scoreText = null;
  750. this.xxxGroup = null;
  751. this.x = null;
  752. this.xx = null;
  753. this.xxx = null;
  754. this.gravity = 1000;
  755. }
  756. create() {
  757. // 物理系统
  758. this.physics.world.gravity.y = this.gravity;
  759. // 背景
  760. // this.bg = this.add.image(0, 0, 'background');
  761. // this.bg.setScale(wRatio, hRatio);
  762. // this.bg.setPosition(width / 2, height / 2);
  763. // this.bg.setOrigin(0, 0);
  764. this.bg = this.add.image(0, 0, "background");
  765. // 设置背景图铺满整个游戏区域
  766. this.bg.displayWidth = width;
  767. this.bg.displayHeight = height;
  768. // 设置背景图原点为左上角
  769. this.bg.setOrigin(0, 0);
  770. // 刀光
  771. this.blade = new Blade({
  772. scene: this
  773. });
  774. this.blade.enable();
  775. // 初始化UI
  776. this.scoreAnim();
  777. this.scoreTextAnim();
  778. this.bestAnim();
  779. this.xxxAnim();
  780. // 添加调试信息
  781. console.log("PlayScene created");
  782. // 调用初始化方法,而不是直接开始生成水果
  783. this.initGame();
  784. }
  785. initGame() {
  786. // 重置游戏状态
  787. this.fruits = [];
  788. this.score = 0;
  789. this.lostCount = 0;
  790. this.bombExplode = false;
  791. this.scoreText.setText(this.score.toString());
  792. // 重置失去计数UI
  793. if (this.xxxGroup) {
  794. this.xxxGroup.removeAll(true);
  795. this.x = this.add.image(0, 0, 'x');
  796. this.xx = this.add.image(22, 0, 'xx');
  797. this.xxx = this.add.image(49, 0, 'xxx');
  798. this.x.setOrigin(0, 0);
  799. this.xx.setOrigin(0, 0);
  800. this.xxx.setOrigin(0, 0);
  801. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  802. }
  803. // 开始游戏
  804. this.playing = true;
  805. console.log("Game initialized and started");
  806. // 延迟一点时间再生成第一个水果,让玩家有准备
  807. this.time.delayedCall(1000, () => {
  808. this.startFruit();
  809. console.log("First fruit spawned");
  810. });
  811. }
  812. update() {
  813. // 如果游戏未开始,不执行任何操作
  814. if (!this.playing) return;
  815. // 检查是否有水果出界
  816. if (!this.bombExplode) {
  817. for (let i = this.fruits.length - 1; i >= 0; i--) {
  818. const fruit = this.fruits[i];
  819. const sprite = fruit.getSprite();
  820. if (sprite && !sprite.active) continue;
  821. if (sprite && (
  822. sprite.y > height + 100 ||
  823. sprite.x < -100 ||
  824. sprite.x > width + 100
  825. )) {
  826. if (fruit.isFruit) {
  827. this.onOut(fruit);
  828. }
  829. sprite.destroy();
  830. this.fruits.splice(i, 1);
  831. }
  832. }
  833. }
  834. // 如果没有水果且游戏进行中,生成新水果
  835. if (this.playing && this.fruits.length === 0 && !this.bombExplode) {
  836. this.startFruit();
  837. }
  838. // 更新刀光
  839. this.blade.update();
  840. // 检查碰撞
  841. if (!this.bombExplode) {
  842. this.fruits.forEach((fruit, i) => {
  843. if (fruit.getSprite() && fruit.getSprite().active) {
  844. this.blade.checkCollide(
  845. fruit.getSprite(),
  846. () => {
  847. if (fruit.isFruit) {
  848. this.onKill(fruit);
  849. this.fruits.splice(i, 1);
  850. } else {
  851. this.onBomb(fruit);
  852. }
  853. }
  854. );
  855. }
  856. });
  857. }
  858. }
  859. scoreAnim() {
  860. this.scoreImage = this.add.image(-100, 8, 'score');
  861. this.scoreImage.setOrigin(0, 0);
  862. this.tweens.add({
  863. targets: this.scoreImage,
  864. x: 8,
  865. duration: 300,
  866. ease: 'Sine.InOut'
  867. });
  868. }
  869. bestAnim() {
  870. this.best = this.add.image(-100, 52, 'best');
  871. this.best.setOrigin(0, 0);
  872. this.tweens.add({
  873. targets: this.best,
  874. x: 5,
  875. duration: 300,
  876. ease: 'Sine.InOut'
  877. });
  878. }
  879. scoreTextAnim() {
  880. this.scoreText = this.add.bitmapText(-100, 40, 'number', this.score.toString(), 32);
  881. this.scoreText.setOrigin(0, 0);
  882. this.tweens.add({
  883. targets: this.scoreText,
  884. x: 75,
  885. duration: 300,
  886. ease: 'Sine.InOut'
  887. });
  888. }
  889. xxxAnim() {
  890. this.xxxGroup = this.add.container(width + 100, 5);
  891. this.x = this.add.image(0, 0, 'x');
  892. this.xx = this.add.image(22, 0, 'xx');
  893. this.xxx = this.add.image(49, 0, 'xxx');
  894. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  895. this.tweens.add({
  896. targets: this.xxxGroup,
  897. x: width - 86,
  898. duration: 300,
  899. ease: 'Sine.InOut'
  900. });
  901. }
  902. startFruit() {
  903. const number = Math.floor(mathTool.randomMinMax(1, 5));
  904. const hasBomb = Math.random() > 0.7; // 30%概率有炸弹
  905. const bombIndex = hasBomb ? Math.floor(Math.random() * number) : -1;
  906. for (let i = 0; i < number; i++) {
  907. if (i === bombIndex) {
  908. this.fruits.push(this.randomFruit(false));
  909. } else {
  910. this.fruits.push(this.randomFruit(true));
  911. }
  912. }
  913. }
  914. randomFruit(isFruit) {
  915. const fruitArray = ["apple", "banana", "basaha", "peach", "sandia"];
  916. const index = Math.floor(Math.random() * fruitArray.length);
  917. const x = mathTool.randomPosX();
  918. const y = mathTool.randomPosY();
  919. const vx = mathTool.randomVelocityX(x);
  920. const vy = mathTool.randomVelocityY();
  921. let fruit;
  922. if (isFruit) {
  923. fruit = new Fruit({
  924. scene: this,
  925. key: fruitArray[index],
  926. x: x,
  927. y: y
  928. });
  929. } else {
  930. fruit = new Bomb({
  931. scene: this,
  932. x: x,
  933. y: y
  934. });
  935. }
  936. fruit.isFruit = isFruit;
  937. const sprite = fruit.getSprite();
  938. if (sprite.body) {
  939. sprite.body.velocity.x = vx;
  940. sprite.body.velocity.y = vy;
  941. sprite.body.gravity.y = this.gravity;
  942. }
  943. return fruit;
  944. }
  945. onOut(fruit) {
  946. const sprite = fruit.getSprite();
  947. let x, y;
  948. // 确定失去标记的位置
  949. if (sprite.y > height) {
  950. x = sprite.x;
  951. y = height - 30;
  952. } else if (sprite.x < 0) {
  953. x = 30;
  954. y = sprite.y;
  955. } else {
  956. x = width - 30;
  957. y = sprite.y;
  958. }
  959. // 创建失去标记动画
  960. const lose = this.add.sprite(x, y, 'lose');
  961. lose.setOrigin(0.5, 0.5);
  962. lose.setScale(0);
  963. const tweenShow = this.tweens.add({
  964. targets: lose,
  965. scale: 1,
  966. duration: 300,
  967. ease: 'Sine.InOut',
  968. paused: true
  969. });
  970. const tweenHide = this.tweens.add({
  971. targets: lose,
  972. scale: 0,
  973. duration: 300,
  974. ease: 'Sine.InOut',
  975. paused: true,
  976. delay: 1000
  977. });
  978. this.tweens.chain({
  979. targets: lose,
  980. tweens: [
  981. {
  982. scale: 1,
  983. duration: 300,
  984. ease: 'Sine.InOut'
  985. },
  986. {
  987. scale: 0,
  988. duration: 300,
  989. ease: 'Sine.InOut',
  990. delay: 1000,
  991. onComplete: () => {
  992. lose.destroy();
  993. }
  994. }
  995. ]
  996. });
  997. tweenShow.play();
  998. tweenHide.on('complete', () => {
  999. lose.destroy();
  1000. });
  1001. this.lostCount++;
  1002. this.loseCount();
  1003. }
  1004. onKill(fruit) {
  1005. const deg = this.blade.collideDeg();
  1006. fruit.half(deg, true);
  1007. this.score++;
  1008. this.scoreText.setText(this.score.toString());
  1009. }
  1010. onBomb(bomb) {
  1011. this.bombExplode = true;
  1012. // 停止所有水果的物理运动
  1013. this.fruits.forEach(fruit => {
  1014. if (fruit.getSprite() && fruit.getSprite().body) {
  1015. fruit.getSprite().body.setVelocity(0);
  1016. fruit.getSprite().body.setGravity(0);
  1017. }
  1018. });
  1019. // 炸弹爆炸
  1020. bomb.explode(
  1021. () => {
  1022. // 白屏显示时的回调:销毁所有水果
  1023. this.fruits.forEach(fruit => {
  1024. if (fruit.getSprite()) {
  1025. fruit.getSprite().destroy();
  1026. }
  1027. });
  1028. this.fruits = [];
  1029. },
  1030. () => {
  1031. // 爆炸完成后的回调
  1032. this.gameOver();
  1033. }
  1034. );
  1035. }
  1036. loseCount() {
  1037. if (this.lostCount === 1) {
  1038. this.lostAnim(this.x, 'xf');
  1039. } else if (this.lostCount === 2) {
  1040. this.lostAnim(this.xx, 'xxf');
  1041. } else if (this.lostCount >= 3) {
  1042. this.lostAnim(this.xxx, 'xxxf');
  1043. this.gameOver();
  1044. }
  1045. }
  1046. lostAnim(removeObj, addKey) {
  1047. removeObj.destroy();
  1048. const newObj = this.add.sprite(removeObj.x, removeObj.y, addKey);
  1049. newObj.setOrigin(0, 0);
  1050. newObj.setScale(0);
  1051. this.xxxGroup.add(newObj);
  1052. this.tweens.add({
  1053. targets: newObj,
  1054. scale: 1,
  1055. duration: 300,
  1056. ease: 'Sine.InOut'
  1057. });
  1058. }
  1059. gameOver() {
  1060. this.playing = false;
  1061. // 1. 确保所有其他元素停止更新,避免干扰
  1062. this.blade.allowBlade = false; // 禁用刀光
  1063. // 2. 创建game-over图片,并设置最高层级
  1064. const gameOverSprite = this.add.sprite(width / 2, height / 2, 'game-over');
  1065. gameOverSprite.setOrigin(0.5, 0.5);
  1066. gameOverSprite.setScale(0);
  1067. gameOverSprite.setDepth(1000); // 设置最高层级,确保不被覆盖
  1068. // 3. 优化入场动画,确保平滑显示
  1069. this.tweens.add({
  1070. targets: gameOverSprite,
  1071. scale: 1,
  1072. duration: 500, // 延长动画时间,确保可见
  1073. ease: 'Elastic.Out', // 更明显的弹性动画,增强视觉效果
  1074. onComplete: () => {
  1075. // 动画完成后再设置自动返回,确保用户有足够时间看到画面
  1076. setTimeout(() => {
  1077. console.log('游戏结束,返回首页');
  1078. this.scene.start('main');
  1079. }, 3000); // 延长至3秒,给用户足够时间观察
  1080. }
  1081. });
  1082. // 4. 支持点击立即返回,提升交互体验
  1083. gameOverSprite.setInteractive();
  1084. gameOverSprite.on('pointerdown', () => {
  1085. console.log('点击返回首页');
  1086. this.scene.start('main');
  1087. });
  1088. }
  1089. }
  1090. // 初始化游戏
  1091. onMounted(() => {
  1092. // 获取容器尺寸
  1093. const container = document.getElementById('game');
  1094. // 初始化工具类
  1095. mathTool.init();
  1096. // 创建游戏实例
  1097. game = new Phaser.Game({
  1098. type: Phaser.CANVAS,
  1099. width: width,
  1100. height: height,
  1101. parent: 'game',
  1102. scene: [BootScene, PreloadScene, MainScene, PlayScene],
  1103. physics: {
  1104. default: 'arcade',
  1105. arcade: {
  1106. debug: false
  1107. }
  1108. }
  1109. });
  1110. });
  1111. </script>
  1112. <style lang="scss" scoped></style>