fruit.vue 48 KB

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