fruit.vue 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722
  1. <template>
  2. <div class="game-container">
  3. <div id="game"></div>
  4. <canvas ref="canvasRef" :width="clientObj.width" :height="clientObj.height"
  5. style="position:fixed;left: 0; top: 0;"></canvas>
  6. </div>
  7. </template>
  8. <script setup name="Fruit" lang="ts">
  9. import { onMounted, ref } from 'vue';
  10. import Phaser from 'phaser';
  11. import { initSpeech, speckText, playMusic, controlMusic, speckCancel, chineseNumber } from '@/utils/speech';
  12. const { proxy } = getCurrentInstance() as any;
  13. const router = useRouter();
  14. const emit = defineEmits(['confirmExit']);
  15. const gameRef = ref(null); // 用于保存游戏实例的ref
  16. const canvasRef = ref(null);
  17. const data = reactive<any>({
  18. bodyposeData: {},//姿态信息
  19. clientObj: {},//浏览器对象
  20. boxes: [],//四个点坐标
  21. proportion: null,//人框和屏幕比例
  22. scaleRatio: 2.2,//素材比例
  23. });
  24. const { bodyposeData, clientObj, boxes, proportion, scaleRatio } = toRefs(data);
  25. /**
  26. * 初始化
  27. */
  28. const getInit = (e: any) => {
  29. let arr = e.data.result.keypoints;
  30. let result = [];
  31. for (let i = 0; i < arr.length; i += 3) {
  32. result.push(arr.slice(i, i + 2));
  33. }
  34. // console.log("result", result)
  35. bodyposeData.value = result;
  36. if (boxes.value.length == 0) {
  37. speckText("识别成功");
  38. proxy?.$modal.msgWarning(`识别成功`);
  39. let arr = e.data.result.boxes;
  40. boxes.value = [{ x: arr[0], y: arr[3] }, { x: arr[0], y: arr[1] }, { x: arr[2], y: arr[1] }, { x: arr[2], y: arr[3] }]
  41. proportion.value = (clientObj.value.height / (arr[3] - arr[1])).toFixed(2);
  42. }
  43. getCanvas();
  44. };
  45. /**
  46. * 绘图
  47. */
  48. const getCanvas = () => {
  49. const canvas: any = canvasRef.value;
  50. const ctx = canvas.getContext('2d');
  51. // 清空整个画布
  52. ctx.clearRect(0, 0, canvas.width, canvas.height);
  53. // 保存当前状态
  54. ctx.save();
  55. function calculateOffset(a: any, b: any) {
  56. return {
  57. x: b.x - a.x,
  58. y: b.y - a.y
  59. };
  60. }
  61. const pointA = { x: clientObj.value.width / 2, y: clientObj.value.height / 2 };
  62. const pointB = { x: (boxes.value[2].x + boxes.value[0].x) / 2, y: (boxes.value[3].y + boxes.value[1].y) / 2 };
  63. const offset = calculateOffset(pointA, pointB);
  64. ctx.translate(-offset.x, -offset.y);
  65. // console.log("Canvas分辨率", clientObj.value);
  66. // console.log("人体图片四点坐标", boxes.value)
  67. // console.log("Canvas中心", pointA);
  68. // console.log("人体中心", pointB);
  69. // console.log("offset", offset)
  70. // console.log("proportion.value",proportion.value)
  71. const originalPoints = bodyposeData.value;
  72. // 计算缩放后坐标
  73. const postData = originalPoints.map((point: any) => {
  74. const newX = (point[0] - pointB.x) * proportion.value + pointB.x;
  75. const newY = (point[1] - pointB.y) * proportion.value + pointB.y;
  76. return [newX, newY];
  77. });
  78. // console.log("原始坐标:", originalPoints);
  79. // console.log("缩放后坐标:", postData);
  80. //externalMethod(postData[10][0] - offset.x, postData[10][1] - offset.y)
  81. externalRightHandMethod(postData[10][0] - offset.x, postData[10][1] - offset.y)
  82. externalLeftHandMethod(postData[9][0] - offset.x, postData[9][1] - offset.y)
  83. //绘制头部
  84. const point1 = { x: postData[4][0], y: postData[4][1] };
  85. const point2 = { x: postData[3][0], y: postData[3][1] };
  86. // 计算椭圆参数
  87. const centerX = (point1.x + point2.x) / 2; // 椭圆中心X
  88. const centerY = (point1.y + point2.y) / 2; // 椭圆中心Y
  89. const distance = Math.sqrt(
  90. Math.pow(point2.x - point1.x, 2) +
  91. Math.pow(point2.y - point1.y, 2)
  92. ); // 两个焦点之间的距离
  93. const radiusX = distance * 0.5; // 水平半径(可调整)
  94. const radiusY = distance * 0.6; // 垂直半径(可调整)
  95. // 1. 绘制填充椭圆
  96. ctx.beginPath();
  97. ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2);
  98. ctx.fillStyle = 'red'; // 填充颜色
  99. ctx.fill(); // 填充
  100. // 2. 绘制边框
  101. ctx.strokeStyle = 'red';
  102. ctx.lineWidth = 5;
  103. ctx.stroke();
  104. // 绘制每个点
  105. postData.forEach((point: any, index: number) => {
  106. //眼睛鼻子不显示
  107. if (![0, 1, 2].includes(index)) {
  108. const [x, y] = point;
  109. ctx.beginPath();
  110. ctx.arc(x, y, 5, 0, Math.PI * 2); // 绘制半径为5的圆点
  111. ctx.fillStyle = 'red';
  112. ctx.fill();
  113. ctx.lineWidth = 1;
  114. ctx.stroke();
  115. }
  116. });
  117. // 根据点关系连线
  118. const arr = [[10, 8], [8, 6], [6, 5], [5, 7], [7, 9], [6, 12], [5, 11], [12, 11], [12, 14], [14, 16], [11, 13], [13, 15]]
  119. arr.forEach((point: any) => {
  120. let index1 = point[0];
  121. let index2 = point[1];
  122. //连线
  123. const dian1 = { x: postData[index1][0], y: postData[index1][1] };
  124. const dian2 = { x: postData[index2][0], y: postData[index2][1] };
  125. // 绘制连线
  126. ctx.beginPath();
  127. ctx.moveTo(dian1.x, dian1.y); // 起点
  128. ctx.lineTo(dian2.x, dian2.y); // 终点
  129. ctx.strokeStyle = 'red'; // 线条颜色
  130. ctx.lineWidth = 3; // 线条宽度
  131. ctx.stroke(); // 描边
  132. });
  133. ctx.restore(); // 恢复状态
  134. };
  135. // 游戏容器和尺寸相关
  136. const width = document.documentElement.clientWidth;
  137. const height = document.documentElement.clientHeight;
  138. const wRatio = document.documentElement.clientWidth / 640;
  139. const hRatio = document.documentElement.clientHeight / 480;
  140. const gameContainer = ref(null);
  141. let game = null;
  142. // 工具类
  143. const mathTool = {
  144. init() {
  145. },
  146. // 计算延长线,p2往p1延长
  147. calcParallel(p1, p2, L) {
  148. const p = {};
  149. if (p1.x === p2.x) {
  150. if (p1.y - p2.y > 0) {
  151. p.x = p1.x;
  152. p.y = p1.y + L;
  153. } else {
  154. p.x = p1.x;
  155. p.y = p1.y - L;
  156. }
  157. } else {
  158. const k = (p2.y - p1.y) / (p2.x - p1.x);
  159. if (p2.x - p1.x < 0) {
  160. p.x = p1.x + L / Math.sqrt(1 + k * k);
  161. p.y = p1.y + L * k / Math.sqrt(1 + k * k);
  162. } else {
  163. p.x = p1.x - L / Math.sqrt(1 + k * k);
  164. p.y = p1.y - L * k / Math.sqrt(1 + k * k);
  165. }
  166. }
  167. p.x = Math.round(p.x);
  168. p.y = Math.round(p.y);
  169. return new Phaser.Math.Vector2(p.x, p.y);
  170. },
  171. // 计算垂直线,p2点开始垂直
  172. calcVertical(p1, p2, L, isLeft) {
  173. const p = {};
  174. if (p1.y === p2.y) {
  175. p.x = p2.x;
  176. if (isLeft) {
  177. if (p2.x - p1.x > 0) {
  178. p.y = p2.y - L;
  179. } else {
  180. p.y = p2.y + L;
  181. }
  182. } else {
  183. if (p2.x - p1.x > 0) {
  184. p.y = p2.y + L;
  185. } else {
  186. p.y = p2.y - L;
  187. }
  188. }
  189. } else {
  190. const k = -(p2.x - p1.x) / (p2.y - p1.y);
  191. if (isLeft) {
  192. if (p2.y - p1.y > 0) {
  193. p.x = p2.x + L / Math.sqrt(1 + k * k);
  194. p.y = p2.y + L * k / Math.sqrt(1 + k * k);
  195. } else {
  196. p.x = p2.x - L / Math.sqrt(1 + k * k);
  197. p.y = p2.y - L * k / Math.sqrt(1 + k * k);
  198. }
  199. } else {
  200. if (p2.y - p1.y > 0) {
  201. p.x = p2.x - L / Math.sqrt(1 + k * k);
  202. p.y = p2.y - L * k / Math.sqrt(1 + k * k);
  203. } else {
  204. p.x = p2.x + L / Math.sqrt(1 + k * k);
  205. p.y = p2.y + L * k / Math.sqrt(1 + k * k);
  206. }
  207. }
  208. }
  209. p.x = Math.round(p.x);
  210. p.y = Math.round(p.y);
  211. return new Phaser.Math.Vector2(p.x, p.y);
  212. },
  213. // 形成刀光点
  214. generateBlade(points) {
  215. const res = [];
  216. if (points.length <= 0) {
  217. return res;
  218. } else if (points.length === 1) {
  219. const oneLength = 6;
  220. res.push(new Phaser.Math.Vector2(points[0].x - oneLength, points[0].y));
  221. res.push(new Phaser.Math.Vector2(points[0].x, points[0].y - oneLength));
  222. res.push(new Phaser.Math.Vector2(points[0].x + oneLength, points[0].y));
  223. res.push(new Phaser.Math.Vector2(points[0].x, points[0].y + oneLength));
  224. } else {
  225. const tailLength = 10;
  226. const headLength = 20;
  227. const tailWidth = 1;
  228. const headWidth = 6;
  229. res.push(this.calcParallel(points[0], points[1], tailLength));
  230. for (let i = 0; i < points.length - 1; i++) {
  231. res.push(this.calcVertical(
  232. points[i + 1],
  233. points[i],
  234. Math.round((headWidth - tailWidth) * i / (points.length - 1) + tailWidth),
  235. true
  236. ));
  237. }
  238. res.push(this.calcVertical(
  239. points[points.length - 2],
  240. points[points.length - 1],
  241. headWidth,
  242. false
  243. ));
  244. res.push(this.calcParallel(
  245. points[points.length - 1],
  246. points[points.length - 2],
  247. headLength
  248. ));
  249. res.push(this.calcVertical(
  250. points[points.length - 2],
  251. points[points.length - 1],
  252. headWidth,
  253. true
  254. ));
  255. for (let i = points.length - 1; i > 0; i--) {
  256. res.push(this.calcVertical(
  257. points[i],
  258. points[i - 1],
  259. Math.round((headWidth - tailWidth) * (i - 1) / (points.length - 1) + tailWidth),
  260. false
  261. ));
  262. }
  263. }
  264. return res;
  265. },
  266. randomMinMax(min, max) {
  267. return Math.random() * (max - min) + min;
  268. },
  269. randomPosX() {
  270. return this.randomMinMax(-100, width + 100);
  271. },
  272. randomPosY() {
  273. //return this.randomMinMax(100, 200) + height;
  274. return this.randomMinMax(height - 100, height);
  275. },
  276. randomVelocityX(posX) {
  277. if (posX < 0) {
  278. return this.randomMinMax(100, 400);
  279. } else if (posX >= 0 && posX < width / 2) {
  280. return this.randomMinMax(0, 400);
  281. } else if (posX >= width / 2 && posX < width) {
  282. return this.randomMinMax(-400, 0);
  283. } else {
  284. return this.randomMinMax(-400, -100);
  285. }
  286. },
  287. randomVelocityY() {
  288. const myH = height - 600;
  289. // 调整范围为原速度的70%左右(根据需要微调)
  290. return this.randomMinMax(-630 - myH * 0.5, -595 - myH * 0.5);
  291. },
  292. degCos(deg) {
  293. return Math.cos(deg * Math.PI / 180);
  294. },
  295. degSin(deg) {
  296. return Math.sin(deg * Math.PI / 180);
  297. },
  298. shuffle(o) {
  299. for (let j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
  300. return o;
  301. }
  302. };
  303. // 炸弹类
  304. class Bomb {
  305. constructor(envConfig) {
  306. this.env = envConfig;
  307. this.game = envConfig.scene;
  308. this.sprite = null;
  309. this.bombImage = null;
  310. this.bombSmoke = null;
  311. this.bombEmit = null;
  312. this.init();
  313. }
  314. init() {
  315. // 创建容器
  316. this.sprite = this.game.add.container(
  317. this.env.x || 0,
  318. this.env.y || 0
  319. );
  320. // 炸弹图像
  321. this.bombImage = this.game.add.sprite(0, 0, 'bomb');
  322. this.bombImage.setOrigin(0.5, 0.5);
  323. // 烟雾
  324. this.bombSmoke = this.game.add.sprite(-55, -55, 'smoke');
  325. // 创建粒子纹理
  326. const bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
  327. this.generateFlame(bitmap);
  328. const texture = bitmap.generateTexture('flameParticle', 50, 50);
  329. // 粒子发射器
  330. this.bombEmit = this.game.add.particles(0, 0, texture.key, {
  331. x: -30,
  332. y: -30,
  333. speed: { min: -100, max: 100 },
  334. scale: { start: 1, end: 0.8 },
  335. alpha: { start: 1, end: 0.1 },
  336. lifespan: 1500,
  337. frequency: 50,
  338. maxParticles: 20
  339. });
  340. // 添加到容器
  341. this.sprite.add([this.bombImage, this.bombEmit, this.bombSmoke]);
  342. // 物理属性
  343. this.game.physics.add.existing(this.sprite);
  344. this.sprite.body.setCollideWorldBounds(false);
  345. }
  346. generateFlame(bitmap) {
  347. const len = 5;
  348. bitmap.fillStyle(0xffffff);
  349. bitmap.beginPath();
  350. bitmap.moveTo(25 + len, 25 - len);
  351. bitmap.lineTo(25 + len, 25 + len);
  352. bitmap.lineTo(25 - len, 25 + len);
  353. bitmap.lineTo(25 - len, 25 - len);
  354. bitmap.closePath();
  355. bitmap.fill();
  356. }
  357. explode(onWhite, onComplete) {
  358. // 播放炸弹爆炸音效
  359. const boomSound = this.game.sound.add('boom');
  360. boomSound.play({
  361. volume: 0.7 // 炸弹音效可稍大,增强冲击力
  362. });
  363. const lights = [];
  364. const startDeg = Math.floor(Math.random() * 360);
  365. // 保存外部this上下文
  366. const self = this;
  367. // 创建直射光芒效果
  368. const maxRays = 25; // 光芒数量
  369. const rayLength = 400; // 光芒长度
  370. const rayWidth = 200; // 光芒宽度
  371. for (let i = 0; i < maxRays; i++) {
  372. const angle = (i / maxRays) * Math.PI * 2; // 计算每个光芒的角度
  373. const light = self.game.add.graphics({
  374. x: self.sprite.x,
  375. y: self.sprite.y
  376. });
  377. // 设置初始透明度和颜色
  378. const alpha = 0.8 - (Math.random() * 0.3); // 随机透明度,增加自然感
  379. const hue = 45 - (Math.random() * 20); // 随机色调,从黄色到白色
  380. const color = Phaser.Display.Color.HSVToRGB(hue / 360, 0.8, 1).color;
  381. light.alpha = alpha;
  382. light.fillStyle(color, 1);
  383. // 创建细长三角形
  384. light.beginPath();
  385. light.moveTo(0, 0); // 起点在中心
  386. light.lineTo(
  387. Math.cos(angle) * rayLength,
  388. Math.sin(angle) * rayLength
  389. ); // 终点在外围
  390. light.lineTo(
  391. Math.cos(angle + Math.PI + 0.1) * (rayWidth / 2),
  392. Math.sin(angle + Math.PI + 0.1) * (rayWidth / 2)
  393. ); // 底部左点
  394. light.closePath();
  395. light.fill();
  396. light.setDepth(2000);
  397. lights.push(light);
  398. // 添加脉动动画效果
  399. self.game.tweens.add({
  400. targets: light,
  401. alpha: alpha * 0.5,
  402. duration: 800 + Math.random() * 400,
  403. yoyo: true,
  404. repeat: -1,
  405. ease: 'Sine.easeInOut'
  406. });
  407. }
  408. // 2. 打乱灯光顺序
  409. mathTool.shuffle(lights);
  410. // 3. 创建白屏元素
  411. const whiteScreen = self.game.add.graphics({
  412. x: 0,
  413. y: 0
  414. });
  415. whiteScreen.fillStyle(0xffffff, 0);
  416. whiteScreen.fillRect(0, 0, width, height);
  417. whiteScreen.setDepth(3000);
  418. whiteScreen.alpha = 0;
  419. // 4. 按顺序执行灯光动画
  420. function playChainAnimations(index) {
  421. if (index >= lights.length) {
  422. playWhiteScreenAnimation();
  423. return;
  424. }
  425. const light = lights[index];
  426. self.game.tweens.add({
  427. targets: light,
  428. alpha: { from: 0, to: 1, from: 0 },
  429. scale: { from: 1, to: 2 },
  430. duration: 100,
  431. onComplete: () => {
  432. light.destroy();
  433. playChainAnimations(index + 1);
  434. }
  435. });
  436. }
  437. function playWhiteScreenAnimation() {
  438. self.game.tweens.add({
  439. targets: whiteScreen,
  440. alpha: { from: 0, to: 1, to: 0 },
  441. duration: 50,
  442. onUpdate: (tween) => {
  443. if (tween.progress >= 0.25 && tween.progress <= 0.3) {
  444. onWhite();
  445. }
  446. },
  447. onComplete: () => {
  448. whiteScreen.destroy();
  449. if (typeof onComplete === 'function') {
  450. onComplete();
  451. }
  452. }
  453. });
  454. }
  455. // 开始执行第一个灯光动画
  456. playChainAnimations(0);
  457. // 新增:在爆炸动画开始后立即隐藏炸弹,动画结束后销毁
  458. this.sprite.visible = false; // 先隐藏视觉元素
  459. // 在爆炸完成回调中销毁炸弹
  460. const originalOnComplete = onComplete;
  461. onComplete = () => {
  462. // 销毁炸弹容器及其内部所有元素
  463. if (this.sprite) {
  464. this.sprite.destroy();
  465. }
  466. if (this.bombEmit) {
  467. this.bombEmit.destroy();
  468. }
  469. if (this.bombSmoke) {
  470. this.bombSmoke.destroy();
  471. }
  472. // 执行原有的完成回调
  473. if (typeof originalOnComplete === 'function') {
  474. originalOnComplete();
  475. }
  476. };
  477. }
  478. getSprite() {
  479. return this.sprite;
  480. }
  481. }
  482. // 水果类
  483. class Fruit {
  484. constructor(envConfig) {
  485. this.env = envConfig;
  486. this.game = envConfig.scene;
  487. this.sprite = null;
  488. this.emitterMap = {
  489. "apple": 0xFFC3E925,
  490. "banana": 0xFFFFE337,
  491. "basaha": 0xFFEB2D13,
  492. "peach": 0xFFF8C928,
  493. "sandia": 0xFF739E0F
  494. };
  495. this.bitmap = null;
  496. this.emitter = null;
  497. this.halfOne = null;
  498. this.halfTwo = null;
  499. this.init();
  500. }
  501. init() {
  502. this.sprite = this.game.add.sprite(
  503. this.env.x || 0,
  504. this.env.y || 0,
  505. this.env.key
  506. );
  507. this.sprite.setOrigin(0.5, 0.5);
  508. this.sprite.setScale(scaleRatio.value);
  509. // 物理属性
  510. this.game.physics.add.existing(this.sprite);
  511. this.sprite.body.setCollideWorldBounds(false);
  512. this.sprite.body.onWorldBounds = true;
  513. // 创建粒子纹理
  514. this.bitmap = this.game.make.graphics({ x: 0, y: 0, add: false });
  515. // 粒子发射器
  516. this.emitter = this.game.add.particles(0, 0, 'flameParticle', {
  517. visible: false // 初始隐藏
  518. });
  519. }
  520. // 水果类中的half方法
  521. half(deg, shouldEmit = false) {
  522. // 计算世界坐标
  523. const transform = this.sprite.getWorldTransformMatrix();
  524. const worldPos = new Phaser.Math.Vector2(
  525. transform.getX(0, 0),
  526. transform.getY(0, 0)
  527. );
  528. // 播放切割音效
  529. if (shouldEmit) { // 仅在需要发射粒子时播放(即有效切割时)
  530. const splatterSound = this.game.sound.add('splatter');
  531. splatterSound.play({
  532. volume: 0.5 // 可调节音量,范围0-1
  533. });
  534. }
  535. // 1. 计算切割方向的垂直向量(用于分离力)
  536. // 刀刃角度转弧度
  537. const rad = Phaser.Math.DegToRad(deg);
  538. // 垂直于刀刃的方向向量(单位向量)
  539. const sepX = Math.sin(rad); // 垂直方向X分量
  540. const sepY = -Math.cos(rad); // 垂直方向Y分量
  541. // 分离力度(可根据水果大小调整)
  542. const sepForce = 300;
  543. // 2. 创建第一半水果
  544. this.halfOne = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-1');
  545. this.halfOne.setOrigin(0.5, 0.5);
  546. this.halfOne.setScale(scaleRatio.value);
  547. this.halfOne.rotation = Phaser.Math.DegToRad(deg + 45); // 初始角度与刀刃匹配
  548. this.game.physics.add.existing(this.halfOne);
  549. // 速度 = 原速度 + 分离速度(向一侧)
  550. this.halfOne.body.velocity.x = this.sprite.body.velocity.x + sepX * sepForce;
  551. this.halfOne.body.velocity.y = this.sprite.body.velocity.y + sepY * sepForce;
  552. this.halfOne.body.gravity.y = 2000;
  553. // 增加旋转(顺时针)
  554. this.halfOne.body.angularVelocity = 500; // 旋转速度(度/秒)
  555. this.halfOne.body.setCollideWorldBounds(false);
  556. this.halfOne.checkWorldBounds = true;
  557. this.halfOne.outOfBoundsKill = true;
  558. // 3. 创建第二半水果(分离方向相反)
  559. this.halfTwo = this.game.add.sprite(worldPos.x, worldPos.y, this.sprite.texture.key + '-2');
  560. this.halfTwo.setOrigin(0.5, 0.5);
  561. this.halfTwo.setScale(scaleRatio.value);
  562. this.halfTwo.rotation = Phaser.Math.DegToRad(deg + 45);
  563. this.game.physics.add.existing(this.halfTwo);
  564. // 速度 = 原速度 - 分离速度(向另一侧)
  565. this.halfTwo.body.velocity.x = this.sprite.body.velocity.x - sepX * sepForce;
  566. this.halfTwo.body.velocity.y = this.sprite.body.velocity.y - sepY * sepForce;
  567. this.halfTwo.body.gravity.y = 2000;
  568. // 增加旋转(逆时针,与第一半相反)
  569. this.halfTwo.body.angularVelocity = -500;
  570. this.halfTwo.body.setCollideWorldBounds(false);
  571. this.halfTwo.checkWorldBounds = true;
  572. this.halfTwo.outOfBoundsKill = true;
  573. // 4. 原水果透明度渐变消失(替代直接销毁)
  574. this.game.tweens.add({
  575. targets: this.sprite,
  576. alpha: 0, // 透明度从1→0
  577. duration: 100, // 100ms内消失
  578. onComplete: () => {
  579. this.sprite.destroy();
  580. }
  581. });
  582. // 5. 优化粒子效果(沿分离方向飞溅)
  583. if (shouldEmit) {
  584. const emitColor = this.emitterMap[this.sprite.texture.key];
  585. this.generateFlame(this.bitmap, emitColor);
  586. const texture = this.bitmap.generateTexture('fruitParticle', 60, 60);
  587. // 粒子发射器:沿分离方向扩散
  588. this.emitter = this.game.add.particles(0, 0, texture.key, {
  589. x: worldPos.x,
  590. y: worldPos.y,
  591. // 速度方向:以分离方向为中心,±30度范围
  592. angle: {
  593. min: deg - 30,
  594. max: deg + 30
  595. },
  596. speed: { min: 100, max: 300 }, // 速度与分离力度匹配
  597. scale: { start: 0.8, end: 0.1 },
  598. alpha: { start: 1, end: 0.1 },
  599. lifespan: 600, // 延长粒子生命周期,增强视觉效果
  600. maxParticles: 15 // 增加粒子数量
  601. });
  602. }
  603. }
  604. generateFlame(bitmap, color) {
  605. // const len = 30;
  606. // bitmap.clear();
  607. // const rgb = Phaser.Display.Color.IntegerToRGB(color);
  608. // const radgrad = bitmap.context.createRadialGradient(len, len, 4, len, len, len);
  609. // radgrad.addColorStop(0, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1)`);
  610. // radgrad.addColorStop(1, `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0)`);
  611. // bitmap.fillStyle(radgrad);
  612. // bitmap.fillRect(0, 0, 2 * len, 2 * len);
  613. const len = 30;
  614. bitmap.clear();
  615. // 将 16 进制颜色转换为 RGB 值(0-255 范围)
  616. const rgb = Phaser.Display.Color.IntegerToRGB(color);
  617. const r = rgb.r / 255; // 转换为 0-1 范围
  618. const g = rgb.g / 255;
  619. const b = rgb.b / 255;
  620. // 使用 Phaser 的径向渐变 API:fillGradientStyle
  621. // 参数说明:
  622. // 1. 渐变类型:1 表示径向渐变
  623. // 2-5. 内圆中心坐标 (x1, y1) 和半径 (r1)
  624. // 6-9. 外圆中心坐标 (x2, y2) 和半径 (r2)
  625. // 10-13. 内圆颜色(r, g, b, a)
  626. // 14-17. 外圆颜色(r, g, b, a)
  627. bitmap.fillGradientStyle(
  628. 1, // 径向渐变
  629. len, len, 4, // 内圆:中心 (len, len),半径 4
  630. len, len, len, // 外圆:中心 (len, len),半径 len
  631. r, g, b, 1, // 内圆颜色(不透明)
  632. r, g, b, 0 // 外圆颜色(透明)
  633. );
  634. // 绘制矩形作为粒子纹理
  635. bitmap.fillRect(0, 0, 2 * len, 2 * len);
  636. }
  637. getSprite() {
  638. return this.sprite;
  639. }
  640. }
  641. // 刀身类
  642. class Blade {
  643. constructor(envConfig, handType) {// 新增handType区分左右手
  644. this.env = envConfig;
  645. this.game = envConfig.scene;
  646. this.points = []; // 记录鼠标轨迹点
  647. this.graphics = null;
  648. this.POINTLIFETIME = 200; // 轨迹点的生命周期(毫秒)
  649. this.allowBlade = false;
  650. this.lastPoint = null; // 上一个记录的点
  651. this.moveThreshold = 5; // 鼠标移动超过这个距离才记录新点(避免密集冗余)
  652. this.handType = handType; // 存储"left"或"right"
  653. this.color = handType === 'left' ? 0x00FFFF : 0xFFFF00; // 左手蓝色,右手黄色(示例)
  654. this.init();
  655. }
  656. init() {
  657. this.graphics = this.game.add.graphics({
  658. x: 0,
  659. y: 0
  660. });
  661. // 根据手型设置绘图层级(可选)
  662. this.graphics.setDepth(this.handType === 'left' ? 1001 : 1002);
  663. // 监听鼠标移动事件(可选,也可在update中处理)
  664. this.game.input.on('pointermove', (pointer) => {
  665. //console.log("11111", pointer)
  666. if (this.allowBlade) {
  667. this.handleMouseMove(pointer);
  668. }
  669. });
  670. }
  671. // 处理鼠标移动:记录轨迹点
  672. handleMouseMove(pointer) {
  673. if (!this.allowBlade) return;
  674. const point = {
  675. x: pointer.x,
  676. y: pointer.y,
  677. time: Date.now()
  678. };
  679. if (!this.lastPoint) {
  680. // 首次记录点
  681. this.lastPoint = point;
  682. this.points.push(point);
  683. } else {
  684. // 计算与上一个点的距离,超过阈值才记录新点
  685. const dis = Math.hypot(
  686. point.x - this.lastPoint.x,
  687. point.y - this.lastPoint.y
  688. );
  689. if (dis > this.moveThreshold) {
  690. this.lastPoint = point;
  691. this.points.push(point);
  692. }
  693. }
  694. }
  695. update() {
  696. if (!this.allowBlade) return;
  697. this.graphics.clear();
  698. const now = Date.now();
  699. this.points = this.points.filter(point => now - point.time < this.POINTLIFETIME);
  700. if (this.points.length > 0) {
  701. const bladePoints = mathTool.generateBlade(this.points);
  702. if (bladePoints.length > 0) {
  703. this.graphics.fillStyle(this.color, 0.8); // 使用当前刀光的颜色
  704. this.graphics.beginPath();
  705. this.graphics.moveTo(bladePoints[0].x, bladePoints[0].y);
  706. bladePoints.forEach((point, i) => i > 0 && this.graphics.lineTo(point.x, point.y));
  707. this.graphics.closePath();
  708. this.graphics.fill();
  709. }
  710. }
  711. }
  712. // 碰撞检测:去掉“鼠标按下”的限制,只要有轨迹就检测
  713. checkCollide(sprite, onCollide) {
  714. if (this.allowBlade && this.points.length > 2) { // 仅保留轨迹点数量的判断
  715. const bounds = sprite.getBounds();
  716. for (const point of this.points) {
  717. if (Phaser.Geom.Rectangle.Contains(bounds, point.x, point.y)) {
  718. onCollide();
  719. break;
  720. }
  721. }
  722. }
  723. }
  724. collideDeg() {
  725. let deg = 0;
  726. const len = this.points.length;
  727. if (len >= 2) {
  728. const p0 = this.points[0];
  729. const p1 = this.points[len - 1];
  730. if (p0.x === p1.x) {
  731. deg = 90;
  732. } else {
  733. const val = (p0.y - p1.y) / (p0.x - p1.x);
  734. deg = Math.round(Math.atan(val) * 180 / Math.PI);
  735. }
  736. if (deg < 0) {
  737. deg += 180;
  738. }
  739. }
  740. return deg;
  741. }
  742. enable() {
  743. this.allowBlade = true;
  744. }
  745. }
  746. // 启动场景
  747. class BootScene extends Phaser.Scene {
  748. constructor() {
  749. super('boot');
  750. }
  751. preload() {
  752. this.load.image('loading', 'static/images/fruit/preloader.gif');
  753. }
  754. create() {
  755. this.scene.start('preload');
  756. }
  757. }
  758. // 预加载场景
  759. class PreloadScene extends Phaser.Scene {
  760. constructor() {
  761. super('preload');
  762. }
  763. preload() {
  764. this.add.sprite(10, height / 2, 'loading').setPosition((width) / 2, height / 2);
  765. this.load.on('progress', (value) => {
  766. // console.log("进度", value)
  767. });
  768. // 加载游戏资源
  769. this.load.image('apple', 'static/images/fruit/apple.png');
  770. this.load.image('apple-1', 'static/images/fruit/apple-1.png');
  771. this.load.image('apple-2', 'static/images/fruit/apple-2.png');
  772. this.load.image('background', 'static/images/fruit/background.jpg');
  773. this.load.image('banana', 'static/images/fruit/banana.png');
  774. this.load.image('banana-1', 'static/images/fruit/banana-1.png');
  775. this.load.image('banana-2', 'static/images/fruit/banana-2.png');
  776. this.load.image('basaha', 'static/images/fruit/basaha.png');
  777. this.load.image('basaha-1', 'static/images/fruit/basaha-1.png');
  778. this.load.image('basaha-2', 'static/images/fruit/basaha-2.png');
  779. this.load.image('best', 'static/images/fruit/best.png');
  780. this.load.image('bomb', 'static/images/fruit/bomb.png');
  781. this.load.image('game-over', 'static/images/fruit/game-over.png');
  782. this.load.image('home-desc', 'static/images/fruit/home-desc.png');
  783. this.load.image('home-mask', 'static/images/fruit/home-mask.png');
  784. this.load.image('logo', 'static/images/fruit/logo.png');
  785. this.load.image('lose', 'static/images/fruit/lose.png');
  786. this.load.image('new-game', 'static/images/fruit/new-game.png');
  787. this.load.image('peach', 'static/images/fruit/peach.png');
  788. this.load.image('peach-1', 'static/images/fruit/peach-1.png');
  789. this.load.image('peach-2', 'static/images/fruit/peach-2.png');
  790. this.load.image('quit', 'static/images/fruit/quit.png');
  791. this.load.image('sandia', 'static/images/fruit/sandia.png');
  792. this.load.image('sandia-1', 'static/images/fruit/sandia-1.png');
  793. this.load.image('sandia-2', 'static/images/fruit/sandia-2.png');
  794. this.load.image('score', 'static/images/fruit/score.png');
  795. this.load.image('shadow', 'static/images/fruit/shadow.png');
  796. this.load.image('smoke', 'static/images/fruit/smoke.png');
  797. this.load.image('x', 'static/images/fruit/x.png');
  798. this.load.image('xf', 'static/images/fruit/xf.png');
  799. this.load.image('xx', 'static/images/fruit/xx.png');
  800. this.load.image('xxf', 'static/images/fruit/xxf.png');
  801. this.load.image('xxx', 'static/images/fruit/xxx.png');
  802. this.load.image('xxxf', 'static/images/fruit/xxxf.png');
  803. this.load.bitmapFont('number', 'static/images/fruit/bitmapFont.png', 'static/images/fruit/bitmapFont.xml');
  804. this.load.audio('splatter', 'static/images/fruit/splatter.mp3');
  805. this.load.audio('boom', 'static/images/fruit/boom.mp3');
  806. this.load.audio('throw', 'static/images/fruit/throw.mp3');
  807. }
  808. create() {
  809. this.scene.start('main');
  810. }
  811. }
  812. // 主场景
  813. class MainScene extends Phaser.Scene {
  814. constructor() {
  815. super('main');
  816. this.bg = null;
  817. this.blade = null;
  818. this.homeGroup = null;
  819. this.home_mask = null;
  820. this.logo = null;
  821. this.home_desc = null;
  822. this.sandiaGroup = null;
  823. this.new_game = null;
  824. this.sandia = null;
  825. this.lose = null;
  826. this.start = false;
  827. this.sandiaRotateSpeed = 0.9;
  828. this.newGameRotateSpeed = -0.3;
  829. this.leftBlade = null; // 左手刀光
  830. this.rightBlade = null; // 右手刀光
  831. }
  832. create() {
  833. // 初始化物理系统
  834. this.physics.world.gravity.y = 0;
  835. // 背景
  836. this.bg = this.add.image(0, 0, "background");
  837. this.bg.setScale(wRatio, hRatio);
  838. this.bg.setPosition(width / 2, height / 2);
  839. this.bg.setOrigin(0.5, 0.5);
  840. // 刀光
  841. this.blade = new Blade({
  842. scene: this
  843. });
  844. // 开始动画
  845. this.homeGroupAnim();
  846. // 初始化左右手刀光
  847. this.leftBlade = new Blade({ scene: this }, 'left');
  848. this.rightBlade = new Blade({ scene: this }, 'right');
  849. }
  850. update() {
  851. this.updateRotate();
  852. // 检查是否该跳转到游戏场景
  853. // if (this.start) {
  854. // this.gotoNextScene();
  855. // }
  856. // 更新刀光
  857. this.blade.update();
  858. // 检查水果碰撞
  859. if (this.sandia && this.sandia.getSprite() && this.sandia.getSprite().active && !this.start) {
  860. this.blade.checkCollide(
  861. this.sandia.getSprite(),
  862. () => {
  863. this.startGame();
  864. }
  865. );
  866. }
  867. // 分别更新左右手刀光
  868. this.leftBlade.update();
  869. this.rightBlade.update();
  870. // 分别检测左右手刀光与水果的碰撞
  871. if (this.sandia && this.sandia.getSprite() && !this.start) {
  872. // 右手碰撞检测
  873. this.rightBlade.checkCollide(
  874. this.sandia.getSprite(),
  875. () => this.startGame()
  876. );
  877. // 左手碰撞检测(可选:允许左手也能启动游戏)
  878. this.leftBlade.checkCollide(
  879. this.sandia.getSprite(),
  880. () => this.startGame()
  881. );
  882. }
  883. }
  884. homeGroupAnim() {
  885. //创建组合默认先隐藏
  886. this.homeGroup = this.add.container(0, -height);
  887. //背景蒙版
  888. // this.home_mask = this.add.image(0, 0, "home-mask");
  889. // this.home_mask.setOrigin(0, 0);
  890. // this.home_mask.setScale(wRatio);
  891. // this.home_mask.y = -200;
  892. //logo
  893. this.logo = this.add.image(20, 50, "logo");
  894. this.logo.setOrigin(0, 0);
  895. //提示语
  896. this.home_desc = this.add.image(0, 0, "home-desc");
  897. this.home_desc.setPosition((width - this.home_desc.width / 2) - 20, 70);
  898. //合并图层
  899. //this.homeGroup.add([this.home_mask, this.logo, this.home_desc]);
  900. this.homeGroup.add([this.logo, this.home_desc]);
  901. // 退出按钮
  902. this.lose = this.add.image(0, 0, "lose");
  903. this.lose.setPosition(width - this.lose.width - 30, height - this.lose.height - 30);
  904. this.lose.setInteractive();
  905. // 绑定点击事件
  906. this.lose.on('pointerdown', () => this.getExit());
  907. //动画效果,接着展示西瓜
  908. this.tweens.add({
  909. targets: this.homeGroup,
  910. y: 0,
  911. duration: 400,
  912. ease: 'Sine.InOut',
  913. onComplete: () => this.fruitAnim()
  914. });
  915. }
  916. fruitAnim() {
  917. // 每次创建全新的容器,避免复用旧实例
  918. this.sandiaGroup = this.add.container(0, 0); // 先置0,后续重新计算
  919. // 西瓜组初始位置:基于当前窗口尺寸动态计算(核心修复)
  920. this.sandiaGroup.setPosition(width / 2, height / 2); // 强制设置位置
  921. //圆圈
  922. this.new_game = this.add.sprite(0, 0, "new-game");
  923. this.new_game.setOrigin(0.5, 0.5);
  924. this.new_game.setScale(scaleRatio.value);
  925. //西瓜
  926. this.sandia = new Fruit({ scene: this, key: "sandia" });
  927. this.sandiaGroup.add([this.new_game, this.sandia.getSprite()]);
  928. //动画效果,接着开放鼠标事件
  929. this.tweens.add({
  930. targets: this.sandiaGroup,
  931. scale: 1,
  932. duration: 500,
  933. ease: 'Linear.None',
  934. onComplete: () => this.allowBlade()
  935. });
  936. }
  937. updateRotate() {
  938. //西瓜外框圆圈图片旋转
  939. if (this.new_game) {
  940. this.new_game.rotation += this.newGameRotateSpeed * 0.016;
  941. }
  942. if (this.sandia && this.sandia.getSprite()) {
  943. this.sandia.getSprite().rotation += this.sandiaRotateSpeed * 0.016;
  944. }
  945. }
  946. allowBlade() {
  947. this.blade.enable();
  948. // 同时启用左右手刀光
  949. this.leftBlade.enable();
  950. this.rightBlade.enable();
  951. }
  952. startGame() {
  953. // this.start = true;
  954. // // 隐藏主界面元素
  955. // this.tweens.add({
  956. // targets: this.homeGroup,
  957. // y: -height,
  958. // duration: 200,
  959. // ease: 'Sine.InOut'
  960. // });
  961. // // 隐藏按钮
  962. // this.new_game.destroy();
  963. // this.lose.destroy();
  964. // // 切开西瓜
  965. // const deg = this.blade.collideDeg();
  966. // this.sandia.half(deg);
  967. this.start = true; // 先禁用立即切换
  968. const deg = this.blade.collideDeg();
  969. // 播放初始西瓜切割音效
  970. const splatterSound = this.sound.add('splatter');
  971. splatterSound.play({
  972. volume: 0.5 // 与普通水果切割音量保持一致
  973. });
  974. this.sandia.half(deg); // 切开西瓜,生成两半
  975. // 延迟1秒(1000ms)后再切换场景,等待动画展示
  976. setTimeout(() => {
  977. this.gotoNextScene();
  978. }, 1000);
  979. }
  980. gotoNextScene() {
  981. this.resetScene();
  982. this.scene.start("play");
  983. }
  984. getExit() {
  985. console.log("退出");
  986. // router.push({ path: '/game' });
  987. emit('confirmExit', {});
  988. }
  989. resetScene() {
  990. this.sandia = null;
  991. this.start = false;
  992. // 新增:销毁西瓜容器,避免残留
  993. if (this.sandiaGroup) {
  994. this.sandiaGroup.destroy(); // 销毁容器及其子元素
  995. this.sandiaGroup = null; // 置空引用
  996. }
  997. }
  998. }
  999. // 游戏场景
  1000. class PlayScene extends Phaser.Scene {
  1001. constructor() {
  1002. super('play');
  1003. this.bg = null;
  1004. this.blade = null;
  1005. this.fruits = [];
  1006. this.score = 0;
  1007. this.playing = false; // 改为false,在初始化方法中设置为true
  1008. this.bombExplode = false;
  1009. this.lostCount = 0;
  1010. this.scoreImage = null;
  1011. this.best = null;
  1012. this.scoreText = null;
  1013. this.xxxGroup = null;
  1014. this.x = null;
  1015. this.xx = null;
  1016. this.xxx = null;
  1017. this.gravity = 200;
  1018. this.leftBlade = null;
  1019. this.rightBlade = null;
  1020. }
  1021. create() {
  1022. // 物理系统
  1023. this.physics.world.gravity.y = this.gravity;
  1024. // 背景
  1025. // this.bg = this.add.image(0, 0, 'background');
  1026. // this.bg.setScale(wRatio, hRatio);
  1027. // this.bg.setPosition(width / 2, height / 2);
  1028. // this.bg.setOrigin(0, 0);
  1029. this.bg = this.add.image(0, 0, "background");
  1030. // 设置背景图铺满整个游戏区域
  1031. this.bg.displayWidth = width;
  1032. this.bg.displayHeight = height;
  1033. // 设置背景图原点为左上角
  1034. this.bg.setOrigin(0, 0);
  1035. // 刀光
  1036. this.blade = new Blade({
  1037. scene: this
  1038. });
  1039. this.blade.enable();
  1040. // 初始化UI
  1041. this.scoreAnim();
  1042. this.scoreTextAnim();
  1043. this.bestAnim();
  1044. this.xxxAnim();
  1045. // 添加调试信息
  1046. console.log("PlayScene created");
  1047. // 调用初始化方法,而不是直接开始生成水果
  1048. this.initGame();
  1049. // 初始化左右手刀光
  1050. this.leftBlade = new Blade({ scene: this }, 'left');
  1051. this.rightBlade = new Blade({ scene: this }, 'right');
  1052. this.leftBlade.enable();
  1053. this.rightBlade.enable();
  1054. }
  1055. initGame() {
  1056. // 重置游戏状态
  1057. this.fruits = [];
  1058. this.score = 0;
  1059. this.lostCount = 0;
  1060. this.bombExplode = false;
  1061. this.scoreText.setText(this.score.toString());
  1062. // 重置失去计数UI
  1063. if (this.xxxGroup) {
  1064. this.xxxGroup.removeAll(true);
  1065. this.x = this.add.image(0, 0, 'x');
  1066. this.xx = this.add.image(22, 0, 'xx');
  1067. this.xxx = this.add.image(49, 0, 'xxx');
  1068. this.x.setOrigin(0, 0);
  1069. this.xx.setOrigin(0, 0);
  1070. this.xxx.setOrigin(0, 0);
  1071. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  1072. }
  1073. // 开始游戏
  1074. this.playing = true;
  1075. console.log("Game initialized and started");
  1076. // 延迟一点时间再生成第一个水果,让玩家有准备
  1077. this.time.delayedCall(1000, () => {
  1078. this.startFruit();
  1079. console.log("First fruit spawned");
  1080. });
  1081. }
  1082. update() {
  1083. // 如果游戏未开始,不执行任何操作
  1084. if (!this.playing) return;
  1085. // 检查是否有水果出界
  1086. if (!this.bombExplode) {
  1087. for (let i = this.fruits.length - 1; i >= 0; i--) {
  1088. const fruit = this.fruits[i];
  1089. const sprite = fruit.getSprite();
  1090. if (sprite && !sprite.active) continue;
  1091. if (sprite && (
  1092. sprite.y > height + 100 ||
  1093. sprite.x < -100 ||
  1094. sprite.x > width + 100
  1095. )) {
  1096. if (fruit.isFruit) {
  1097. this.onOut(fruit);
  1098. }
  1099. sprite.destroy();
  1100. this.fruits.splice(i, 1);
  1101. }
  1102. }
  1103. }
  1104. // 如果没有水果且游戏进行中,生成新水果
  1105. if (this.playing && this.fruits.length === 0 && !this.bombExplode) {
  1106. this.startFruit();
  1107. }
  1108. // 更新刀光
  1109. this.blade.update();
  1110. // 分别更新左右手刀光
  1111. this.leftBlade.update();
  1112. this.rightBlade.update();
  1113. // 检查碰撞
  1114. if (!this.bombExplode) {
  1115. this.fruits.forEach((fruit, i) => {
  1116. if (fruit.getSprite() && fruit.getSprite().active) {
  1117. this.blade.checkCollide(
  1118. fruit.getSprite(),
  1119. () => {
  1120. if (fruit.isFruit) {
  1121. this.onKill(fruit);
  1122. this.fruits.splice(i, 1);
  1123. } else {
  1124. this.onBomb(fruit);
  1125. }
  1126. }
  1127. );
  1128. // 右手碰撞
  1129. this.rightBlade.checkCollide(
  1130. fruit.getSprite(),
  1131. () => this.handleCollision(fruit, i)
  1132. );
  1133. // 左手碰撞
  1134. this.leftBlade.checkCollide(
  1135. fruit.getSprite(),
  1136. () => this.handleCollision(fruit, i)
  1137. );
  1138. }
  1139. });
  1140. }
  1141. }
  1142. // 统一处理碰撞逻辑(提取重复代码)
  1143. handleCollision(fruit, index) {
  1144. if (fruit.isFruit) {
  1145. this.onKill(fruit);
  1146. this.fruits.splice(index, 1);
  1147. } else {
  1148. this.onBomb(fruit);
  1149. }
  1150. }
  1151. scoreAnim() {
  1152. this.scoreImage = this.add.image(-100, 8, 'score');
  1153. this.scoreImage.setOrigin(0, 0);
  1154. this.tweens.add({
  1155. targets: this.scoreImage,
  1156. x: 8,
  1157. duration: 300,
  1158. ease: 'Sine.InOut'
  1159. });
  1160. }
  1161. bestAnim() {
  1162. this.best = this.add.image(-100, 52, 'best');
  1163. this.best.setOrigin(0, 0);
  1164. this.tweens.add({
  1165. targets: this.best,
  1166. x: 5,
  1167. duration: 300,
  1168. ease: 'Sine.InOut'
  1169. });
  1170. }
  1171. scoreTextAnim() {
  1172. this.scoreText = this.add.bitmapText(-100, 40, 'number', this.score.toString(), 32);
  1173. this.scoreText.setOrigin(0, 0);
  1174. this.tweens.add({
  1175. targets: this.scoreText,
  1176. x: 75,
  1177. duration: 300,
  1178. ease: 'Sine.InOut'
  1179. });
  1180. }
  1181. xxxAnim() {
  1182. this.xxxGroup = this.add.container(width + 100, 5);
  1183. this.x = this.add.image(0, 0, 'x');
  1184. this.xx = this.add.image(22, 0, 'xx');
  1185. this.xxx = this.add.image(49, 0, 'xxx');
  1186. this.xxxGroup.add([this.x, this.xx, this.xxx]);
  1187. this.tweens.add({
  1188. targets: this.xxxGroup,
  1189. x: width - 86,
  1190. duration: 300,
  1191. ease: 'Sine.InOut'
  1192. });
  1193. }
  1194. startFruit() {
  1195. // 根据分数动态调整数量:分数越高,生成越多水果
  1196. let min = 1;
  1197. let max = 1;
  1198. if (this.score >= 0 && this.score < 30) {
  1199. min = 1;
  1200. max = 1;
  1201. } else if (this.score >= 30 && this.score < 60) {
  1202. min = 1;
  1203. max = 2;
  1204. } else {
  1205. min = 2;
  1206. max = 3;
  1207. }
  1208. const number = Math.floor(mathTool.randomMinMax(min, max + 1)); // +1是因为randomMinMax的max是 exclusive
  1209. const hasBomb = Math.random() > 0.9;
  1210. const bombIndex = hasBomb ? Math.floor(Math.random() * number) : -1;
  1211. for (let i = 0; i < number; i++) {
  1212. if (i === bombIndex) {
  1213. this.fruits.push(this.randomFruit(false));
  1214. } else {
  1215. this.fruits.push(this.randomFruit(true));
  1216. }
  1217. }
  1218. }
  1219. randomFruit(isFruit) {
  1220. const fruitArray = ["apple", "banana", "basaha", "peach", "sandia"];
  1221. const index = Math.floor(Math.random() * fruitArray.length);
  1222. const x = mathTool.randomPosX();
  1223. const y = mathTool.randomPosY();
  1224. const vx = mathTool.randomVelocityX(x);
  1225. const vy = mathTool.randomVelocityY();
  1226. let fruit;
  1227. if (isFruit) {
  1228. fruit = new Fruit({
  1229. scene: this,
  1230. key: fruitArray[index],
  1231. x: x,
  1232. y: y
  1233. });
  1234. } else {
  1235. fruit = new Bomb({
  1236. scene: this,
  1237. x: x,
  1238. y: y
  1239. });
  1240. }
  1241. console.log("isFruitisFruitisFruit", isFruit)
  1242. fruit.isFruit = isFruit;
  1243. const sprite = fruit.getSprite();
  1244. if (sprite.body) {
  1245. sprite.body.velocity.x = vx;
  1246. sprite.body.velocity.y = vy;
  1247. sprite.body.gravity.y = this.gravity;
  1248. }
  1249. return fruit;
  1250. }
  1251. onOut(fruit) {
  1252. const sprite = fruit.getSprite();
  1253. let x, y;
  1254. // 确定失去标记的位置
  1255. if (sprite.y > height) {
  1256. x = sprite.x;
  1257. y = height - 30;
  1258. } else if (sprite.x < 0) {
  1259. x = 30;
  1260. y = sprite.y;
  1261. } else {
  1262. x = width - 30;
  1263. y = sprite.y;
  1264. }
  1265. // 创建失去标记动画
  1266. const lose = this.add.sprite(x, y, 'lose');
  1267. lose.setOrigin(0.5, 0.5);
  1268. lose.setScale(0);
  1269. const tweenShow = this.tweens.add({
  1270. targets: lose,
  1271. scale: 1,
  1272. duration: 300,
  1273. ease: 'Sine.InOut',
  1274. paused: true
  1275. });
  1276. const tweenHide = this.tweens.add({
  1277. targets: lose,
  1278. scale: 0,
  1279. duration: 300,
  1280. ease: 'Sine.InOut',
  1281. paused: true,
  1282. delay: 1000
  1283. });
  1284. this.tweens.chain({
  1285. targets: lose,
  1286. tweens: [
  1287. {
  1288. scale: 1,
  1289. duration: 300,
  1290. ease: 'Sine.InOut'
  1291. },
  1292. {
  1293. scale: 0,
  1294. duration: 300,
  1295. ease: 'Sine.InOut',
  1296. delay: 1000,
  1297. onComplete: () => {
  1298. lose.destroy();
  1299. }
  1300. }
  1301. ]
  1302. });
  1303. tweenShow.play();
  1304. tweenHide.on('complete', () => {
  1305. lose.destroy();
  1306. });
  1307. this.lostCount++;
  1308. this.loseCount();
  1309. }
  1310. onKill(fruit) {
  1311. const deg = this.blade.collideDeg();
  1312. fruit.half(deg, true);
  1313. this.score++;
  1314. this.scoreText.setText(this.score.toString());
  1315. }
  1316. onBomb(bomb) {
  1317. this.bombExplode = true;
  1318. // 屏幕震动效果
  1319. this.shakeScreen();
  1320. // 停止所有水果的物理运动
  1321. this.fruits.forEach(fruit => {
  1322. if (fruit.getSprite() && fruit.getSprite().body) {
  1323. fruit.getSprite().body.setVelocity(0);
  1324. fruit.getSprite().body.setGravity(0);
  1325. }
  1326. });
  1327. // 炸弹爆炸
  1328. bomb.explode(
  1329. () => {
  1330. // 白屏显示时的回调:销毁所有水果
  1331. this.fruits.forEach(fruit => {
  1332. if (fruit.getSprite()) {
  1333. fruit.getSprite().destroy();
  1334. }
  1335. });
  1336. this.fruits = [];
  1337. },
  1338. () => {
  1339. // 爆炸完成后的回调
  1340. this.gameOver();
  1341. }
  1342. );
  1343. }
  1344. // 添加屏幕震动方法
  1345. shakeScreen() {
  1346. // 获取主相机
  1347. const camera = this.cameras.main;
  1348. // 保存相机初始位置
  1349. const startX = camera.x;
  1350. const startY = camera.y;
  1351. // 震动持续时间(ms)
  1352. const duration = 2000;
  1353. // 震动强度
  1354. const intensity = 8;
  1355. // 震动频率控制
  1356. const frequency = 20;
  1357. // 计算震动次数
  1358. const shakes = duration / frequency;
  1359. let shakeCount = 0;
  1360. // 创建震动定时器
  1361. const shakeInterval = setInterval(() => {
  1362. if (shakeCount < shakes) {
  1363. // 随机生成震动偏移量
  1364. const offsetX = Phaser.Math.Between(-intensity, intensity);
  1365. const offsetY = Phaser.Math.Between(-intensity, intensity);
  1366. // 应用震动
  1367. camera.setPosition(startX + offsetX, startY + offsetY);
  1368. shakeCount++;
  1369. } else {
  1370. // 震动结束,恢复相机位置
  1371. clearInterval(shakeInterval);
  1372. camera.setPosition(startX, startY);
  1373. }
  1374. }, frequency);
  1375. }
  1376. loseCount() {
  1377. if (this.lostCount === 1) {
  1378. this.lostAnim(this.x, 'xf');
  1379. } else if (this.lostCount === 2) {
  1380. this.lostAnim(this.xx, 'xxf');
  1381. } else if (this.lostCount >= 3) {
  1382. this.lostAnim(this.xxx, 'xxxf');
  1383. this.gameOver();
  1384. }
  1385. }
  1386. lostAnim(removeObj, addKey) {
  1387. removeObj.destroy();
  1388. const newObj = this.add.sprite(removeObj.x, removeObj.y, addKey);
  1389. newObj.setOrigin(0, 0);
  1390. newObj.setScale(0);
  1391. this.xxxGroup.add(newObj);
  1392. this.tweens.add({
  1393. targets: newObj,
  1394. scale: 1,
  1395. duration: 300,
  1396. ease: 'Sine.InOut'
  1397. });
  1398. }
  1399. gameOver() {
  1400. this.playing = false;
  1401. // 1. 确保所有其他元素停止更新,避免干扰
  1402. this.blade.allowBlade = false; // 禁用刀光
  1403. // 2. 创建game-over图片,并设置最高层级
  1404. const gameOverSprite = this.add.sprite(width / 2, height / 2, 'game-over');
  1405. gameOverSprite.setOrigin(0.5, 0.5);
  1406. gameOverSprite.setScale(0);
  1407. gameOverSprite.setDepth(1000); // 设置最高层级,确保不被覆盖
  1408. // 3. 优化入场动画,确保平滑显示
  1409. this.tweens.add({
  1410. targets: gameOverSprite,
  1411. scale: 1,
  1412. duration: 500, // 延长动画时间,确保可见
  1413. ease: 'Elastic.Out', // 更明显的弹性动画,增强视觉效果
  1414. onComplete: () => {
  1415. // 动画完成后再设置自动返回,确保用户有足够时间看到画面
  1416. setTimeout(() => {
  1417. console.log('游戏结束,返回首页');
  1418. this.scene.start('main');
  1419. }, 1000); // 延长至3秒,给用户足够时间观察
  1420. }
  1421. });
  1422. // 4. 支持点击立即返回,提升交互体验
  1423. gameOverSprite.setInteractive();
  1424. gameOverSprite.on('pointerdown', () => {
  1425. console.log('点击返回首页');
  1426. this.scene.start('main');
  1427. });
  1428. }
  1429. }
  1430. // 外部方法(如Vue组件中的某个按钮点击事件)
  1431. const externalMethod = (autoX, autoY) => {
  1432. // 1. 获取游戏实例(从之前保存的gameRef中)
  1433. const game = gameRef.value;
  1434. if (!game) {
  1435. console.error("游戏未初始化");
  1436. return;
  1437. }
  1438. // 2. 获取目标场景(根据当前活跃场景选择'main'或'play')
  1439. // 例如:获取PlayScene(场景key为'play')
  1440. const currentScene = getActiveScene();
  1441. const targetScene = game.scene.getScene(currentScene);
  1442. // 若当前在主场景,可改为 game.scene.getScene('main')
  1443. // 3. 检查场景中的Blade实例是否存在且已启用
  1444. if (!targetScene || !targetScene.blade || !targetScene.blade.allowBlade) {
  1445. console.error("Blade实例未初始化或未启用");
  1446. return;
  1447. }
  1448. // 4. 构造模拟的pointer参数(包含x和y)
  1449. const mockPointer = {
  1450. x: autoX, // 外部传入的X坐标
  1451. y: autoY // 外部传入的Y坐标
  1452. };
  1453. // 5. 调用Blade的handleMouseMove方法
  1454. targetScene.blade.handleMouseMove(mockPointer);
  1455. };
  1456. // 新增:区分左右手的输入方法
  1457. const externalLeftHandMethod = (x, y) => {
  1458. const game = gameRef.value;
  1459. if (!game) return;
  1460. const currentScene = getActiveScene();
  1461. const targetScene = game.scene.getScene(currentScene);
  1462. if (targetScene && targetScene.leftBlade && targetScene.leftBlade.allowBlade) {
  1463. targetScene.leftBlade.handleMouseMove({ x, y });
  1464. }
  1465. };
  1466. const externalRightHandMethod = (x, y) => {
  1467. const game = gameRef.value;
  1468. if (!game) return;
  1469. const currentScene = getActiveScene();
  1470. const targetScene = game.scene.getScene(currentScene);
  1471. if (targetScene && targetScene.rightBlade && targetScene.rightBlade.allowBlade) {
  1472. targetScene.rightBlade.handleMouseMove({ x, y });
  1473. }
  1474. };
  1475. const getActiveScene = () => {
  1476. const game = gameRef.value; // 获取游戏实例
  1477. if (!game) return null;
  1478. // 获取所有活跃场景(通常只有一个)
  1479. const activeScenes = game.scene.getScenes(true);
  1480. // 返回第一个活跃场景(如果有)
  1481. return activeScenes.length > 0 ? activeScenes[0].scene.key : null;
  1482. };
  1483. onBeforeMount(() => {
  1484. clientObj.value = {
  1485. width: document.documentElement.clientWidth,
  1486. height: document.documentElement.clientHeight,
  1487. }
  1488. scaleRatio.value = clientObj.value.height / 480;
  1489. });
  1490. // 初始化游戏
  1491. onMounted(() => {
  1492. // 获取容器尺寸
  1493. const container = document.getElementById('game');
  1494. // 初始化工具类
  1495. mathTool.init();
  1496. // 创建游戏实例
  1497. game = new Phaser.Game({
  1498. type: Phaser.CANVAS,
  1499. width: width,
  1500. height: height,
  1501. parent: 'game',
  1502. scene: [BootScene, PreloadScene, MainScene, PlayScene],
  1503. physics: {
  1504. default: 'arcade',
  1505. arcade: {
  1506. debug: false
  1507. }
  1508. }
  1509. });
  1510. gameRef.value = game;
  1511. });
  1512. onBeforeUnmount(() => {
  1513. });
  1514. //暴露给父组件用
  1515. defineExpose({
  1516. getInit
  1517. })
  1518. </script>
  1519. <style lang="scss" scoped></style>