ZumaCore.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. /** @format */
  2. import {Ball} from './Ball'
  3. import {ConstValue} from '../data/ConstValue'
  4. import {ZumaRole} from './ZumaRole'
  5. import {IZumaConfig, ZumaConfig} from '../config/ZumaConfig'
  6. import {ZumaUI} from '../ui/ZumaUI'
  7. import {Data} from '../GameControl'
  8. import {ccUtils} from '../utils/ccUtils'
  9. import {Bezier} from './Bezier'
  10. import {ZumamapConfig} from '../config/ZumamapConfig'
  11. import {GAME_PREFAB_TYPE, GAME_ROLE_TIP, GOODS} from '../enums/Enum'
  12. import {node, observer} from '../mobx/observer'
  13. import {ZumabuffConfig} from '../config/ZumabuffConfig'
  14. const {ccclass, property} = cc._decorator
  15. enum GAME_STATE {
  16. none,
  17. rollIn,
  18. run,
  19. rollOut,
  20. gamePass,
  21. }
  22. enum BORN_TYPE {
  23. random = 1,
  24. normal,
  25. }
  26. @ccclass
  27. @observer
  28. export default class ZumaCore extends cc.Component {
  29. @node('startImg')
  30. private startImgNode: cc.Node
  31. @node('endImg')
  32. private endImgNode: cc.Node
  33. @node('buff')
  34. private buffNode: cc.Node
  35. private ballPrefab: cc.Prefab = null
  36. //小球的节点池
  37. private ballPool: cc.NodePool
  38. //小球的父节点
  39. private ballParent: cc.Node
  40. //发射出来的小球
  41. private ballShoot: any[] = []
  42. //做小球的挤入效果时存储需要被检测碰撞的小球
  43. private ballCrushed: Ball[][] = []
  44. //被吸引回退的第一个球的数组
  45. private ballAttracted: Ball[] = []
  46. //最大连击数
  47. private maxCombo: number = 0
  48. //当前连击数
  49. private curCombo: number = 0
  50. //游戏是否结束
  51. private canShoot: boolean = true
  52. //计算额外加分的时候要用到
  53. private lastId: number
  54. /** 存储球链中所有的小球 */
  55. private rollBalls: Ball[] = []
  56. //开始滚入的小球的个数
  57. private rollInCount: number = 35
  58. //游戏中球的总数
  59. private rollInTotalCount: number = 100
  60. //球的大小 必须是 stepLength 的倍数
  61. private ballSize: number = 64
  62. //步长
  63. private stepLength: number = 2
  64. //一个球的坐标间距
  65. private ballStep: number = 0
  66. //起始下标
  67. private startIndex: number = 0
  68. //结束下标 越大实现快速缩进
  69. private endIndex: number = 8
  70. /** 角色 */
  71. private role: ZumaRole
  72. //射速
  73. private shootSpeed: number = 20
  74. /** 关卡数据 */
  75. public levelCfg: IZumaConfig
  76. //UI
  77. public zumaUI: ZumaUI
  78. //游戏状态
  79. public gameSate: GAME_STATE
  80. //当前颜色数组
  81. public curBallIDs: number[]
  82. buffPool: cc.NodePool
  83. protected onLoad() {
  84. this.node.on(cc.Node.EventType.TOUCH_START, this.onBattleAttack, this)
  85. this.ballParent = cc.find('balls', this.node)
  86. this.buffPool = new cc.NodePool()
  87. }
  88. protected update(dt: number) {
  89. switch (this.gameSate) {
  90. case GAME_STATE.rollIn:
  91. this.ballRollIn()
  92. break
  93. case GAME_STATE.run:
  94. //运行发射球
  95. this.onShootBallMove()
  96. //检测发射球的碰撞
  97. let isCrush = false
  98. isCrush = this.checkPushCollision()
  99. //吸附小球
  100. let isAttract = false
  101. if (!isCrush) isAttract = this.attract()
  102. //小球向前滚动
  103. if (!isCrush || !isAttract) this.ballRollComplete()
  104. break
  105. case GAME_STATE.rollOut:
  106. this.ballRollOut()
  107. break
  108. }
  109. }
  110. public init(role: ZumaRole, zumaUI: ZumaUI): void {
  111. this.ballStep = this.ballSize / this.stepLength
  112. this.role = role
  113. this.zumaUI = zumaUI
  114. this.ballPrefab = Data.game.gameAssetMap.get('prefab/zuma/ball') as cc.Prefab
  115. this.setBuffNum()
  116. }
  117. public startGame() {
  118. this.unscheduleAllCallbacks()
  119. if (this.levelCfg.bornType == BORN_TYPE.random) {
  120. this.curBallIDs = [...this.levelCfg.colorBall]
  121. this.curBallIDs.outOrder()
  122. } else if (this.levelCfg.bornType == BORN_TYPE.normal) {
  123. this.curBallIDs = [...this.levelCfg.colorBall]
  124. }
  125. this.gameSate = GAME_STATE.none
  126. this.ballShoot.length = 0
  127. this.ballCrushed.length = 0
  128. this.ballAttracted.length = 0
  129. this.initBall()
  130. this.role.init(this)
  131. }
  132. public initData(): void {
  133. this.levelCfg = ZumaConfig[Data.game.stageRound]
  134. this.rollInCount = this.levelCfg.rollInCount
  135. this.rollInTotalCount = this.levelCfg.rollInTotal.length
  136. Data.zuma.mapData.length = 0
  137. let mapCfg = ZumamapConfig[this.levelCfg.map]
  138. let mapData = mapCfg.mapData
  139. for (let i = 0; i < mapData.length; i++) {
  140. let tmp = mapData[i]
  141. let startX = i == 0 ? tmp[0] : i == 1 ? mapData[i - 1][4] : mapData[i - 1][2]
  142. let startY = i == 0 ? tmp[1] : i == 1 ? mapData[i - 1][5] : mapData[i - 1][3]
  143. let controlX = tmp[i == 0 ? 2 : 0]
  144. let controlY = tmp[i == 0 ? 3 : 1]
  145. let endX = tmp[i == 0 ? 4 : 2]
  146. let endY = tmp[i == 0 ? 5 : 3]
  147. let steps = Bezier.init(
  148. cc.v2(startX, startY),
  149. cc.v2(controlX, controlY),
  150. cc.v2(endX, endY),
  151. this.stepLength,
  152. )
  153. for (let m = 1; m <= steps; ++m) {
  154. let data = Bezier.getAnchorPoint(m)
  155. Data.zuma.mapData.push(data)
  156. }
  157. }
  158. this.startImgNode.x = Data.zuma.mapData[0][0]
  159. this.startImgNode.y = Data.zuma.mapData[0][1]
  160. this.startImgNode.angle = Data.zuma.mapData[0][2] + 90
  161. this.endImgNode.x = Data.zuma.mapData[Data.zuma.mapData.length - 1][0]
  162. this.endImgNode.y = Data.zuma.mapData[Data.zuma.mapData.length - 1][1]
  163. let nodes = ccUtils.instantChildren(
  164. cc.find('mapPoints/route_point', this.node),
  165. Math.floor(Data.zuma.mapData.length / this.ballStep) - 1,
  166. )
  167. nodes.forEach((node, i) => {
  168. node.x = Data.zuma.mapData[(i + 1) * this.ballStep][0]
  169. node.y = Data.zuma.mapData[(i + 1) * this.ballStep][1]
  170. })
  171. }
  172. /** 发射球 */
  173. private onBattleAttack(e): void {
  174. if (!this.role.canShoot || !this.canShoot || this.gameSate != GAME_STATE.run || this.zumaUI.curBalls < 1) return
  175. //更新角色旋转角度
  176. this.updateRoleAngle(e)
  177. this.shootBall()
  178. }
  179. /** 更新角色角度 */
  180. private updateRoleAngle(e): void {
  181. let pos = ccUtils.convertWorldPosToNode(this.node, ccUtils.convertTouchPosToWorld(e))
  182. let dir = pos.sub(cc.v2(this.role.node.position)).normalize()
  183. let angle = Math.atan2(dir.y, dir.x)
  184. this.role.node.angle = cc.misc.radiansToDegrees(angle) - 90
  185. }
  186. /** 初始化第一个小球,并开始滚入小球 */
  187. private initBall() {
  188. if (!this.ballPool) this.ballPool = new cc.NodePool()
  189. this.rollBalls.length = 0
  190. let ball: Ball = this.getBall()
  191. ball.node.parent = this.ballParent
  192. this.rollBalls.unshift(ball)
  193. //将数组的第this.startIndex个位置设为起点
  194. ball.posIndex = this.startIndex
  195. this.gameSate = GAME_STATE.rollIn
  196. }
  197. /** 开始滚入小球 */
  198. private ballRollIn() {
  199. if (this.rollBalls.length < this.rollInCount) {
  200. for (let i: number = 0; i < this.rollBalls.length; ++i) {
  201. //所有小球移动rollSpeed个位置
  202. this.rollBalls[i].posIndex += this.levelCfg.rollSpeed * 2
  203. }
  204. //第一个球距起点this.startIndex个位置则补充小球进去
  205. if (this.rollBalls[0].posIndex == this.ballStep + this.startIndex) {
  206. let tmpBall: Ball = this.getBall()
  207. this.rollBalls.unshift(tmpBall)
  208. tmpBall.node.parent = this.ballParent
  209. tmpBall.posIndex = this.startIndex
  210. }
  211. } else {
  212. //滚入小球完毕
  213. //发射器初始化
  214. this.role.initBall()
  215. //TODO 停止滚动声音
  216. // soundMgr.stopRollingSound();
  217. //开始正常游戏阶段
  218. this.gameSate = GAME_STATE.run
  219. }
  220. }
  221. /** 滚入小球结束后,由该函数驱动小球向前滚动 */
  222. private ballRollComplete() {
  223. //与第一个小球相连的小球都向前滚动一步
  224. if (this.rollBalls.length != 0) {
  225. this.pushBallFrom(0, this.levelCfg.rollSpeed)
  226. //如果总球数不为0并且第一个球距第一个点位置相差startIndex则补充小球
  227. if (
  228. this.rollBalls[0] &&
  229. this.rollBalls[0].posIndex == this.ballStep + this.startIndex &&
  230. this.rollInTotalCount != 0
  231. ) {
  232. let _ball: Ball = this.getBall()
  233. this.rollBalls.unshift(_ball)
  234. _ball.node.parent = this.ballParent
  235. _ball.posIndex = this.startIndex
  236. }
  237. }
  238. }
  239. /** 获取一个小球 */
  240. public getBall(): Ball {
  241. if (this.rollInTotalCount <= 0) {
  242. return null
  243. console.error('rollInTotalCount is zero')
  244. }
  245. let ball = this.getBallFromPool()
  246. ball.init(
  247. this.curBallIDs[this.levelCfg.rollInTotal[this.levelCfg.rollInTotal.length - this.rollInTotalCount] - 1],
  248. this,
  249. )
  250. --this.rollInTotalCount
  251. return ball
  252. }
  253. public getBallFromPool(): Ball {
  254. //node的parent赋值是异步的,要给对象池中的对象加标记,否则会出问题
  255. let ballNode: cc.Node = this.ballPool.get()
  256. let ball = ballNode?.getComponent(Ball)
  257. if (!ballNode) {
  258. ballNode = cc.instantiate(this.ballPrefab)
  259. ball = ballNode.getComponent(Ball)
  260. }
  261. ballNode.stopAllActions()
  262. ballNode.scale = 1
  263. return ball
  264. }
  265. public putBall(ball: Ball): void {
  266. if (this.ballPool.size() < 50) this.ballPool?.put(ball.node)
  267. }
  268. /**
  269. * 推进小球的函数,传入参数推进的起始位置,推进的步数(为正即为向前推进,为负即为向后回退)
  270. * 在使用回退道具时要传入负参数
  271. */
  272. public pushBallFrom(index: number, step: number): void {
  273. let temp: Ball[] = []
  274. temp.push(this.rollBalls[index])
  275. //算出所有与ballArray中索引为index的小球"相连"的小球
  276. for (let i: number = index; i < this.rollBalls.length - 1; ++i) {
  277. if (this.rollBalls[i + 1].posIndex - this.rollBalls[i].posIndex <= this.ballStep) {
  278. //如小球位置有误差则纠正
  279. if (this.rollBalls[i + 1].posIndex - this.rollBalls[i].posIndex < this.ballStep) {
  280. this.rollBalls[i + 1].posIndex = Math.min(
  281. this.rollBalls[i].posIndex + this.ballStep,
  282. Data.zuma.mapData.length - 1,
  283. )
  284. }
  285. temp.push(this.rollBalls[i + 1])
  286. } else {
  287. break
  288. }
  289. }
  290. //然后将其移动指定的步数
  291. for (let j: number = temp.length - 1; j >= 0; j--) {
  292. if (temp[j].posIndex + step > Data.zuma.mapData.length - 1) {
  293. this.putBall(temp[j])
  294. this.rollBalls.splice(this.rollBalls.indexOf(temp[j]), 1)
  295. //暂时不回收球
  296. // if (this.rollBalls.indexOf(temp[j]) == this.rollBalls.length - 1) {
  297. // this.gameSate = GAME_STATE.rollOut
  298. // }
  299. } else {
  300. temp[j].posIndex += step
  301. }
  302. }
  303. //每次推动小球都需要判断是否结束游戏
  304. if (this.gameSate == GAME_STATE.run) {
  305. if (this.rollBalls.length <= 0) {
  306. this.gamePass()
  307. }
  308. }
  309. }
  310. /** 游戏结束,小球进终点后消除 */
  311. public ballRollOut(): void {
  312. //放入正在发射的小球
  313. this.ballShoot.forEach(v => this.putBall(v[0]))
  314. for (let i: number = this.rollBalls.length - 1; i >= 0; --i) {
  315. if (this.rollBalls[i].posIndex > Data.zuma.mapData.length - this.endIndex - 1) {
  316. this.putBall(this.rollBalls[i])
  317. this.rollBalls.splice(i, 1)
  318. if (this.rollBalls.length == 0) {
  319. //TODO 播放游戏胜利音效
  320. // soundMgr.stopGameOverSound();
  321. }
  322. } else {
  323. this.rollBalls[i].posIndex += this.endIndex
  324. }
  325. }
  326. }
  327. /** 开始射击 */
  328. public shootBall(): void {
  329. //发射球时的弧度
  330. let roleNode = this.role.node
  331. let radian: number = ccUtils.angle2radian(roleNode.angle + 90)
  332. let ball: Ball = this.role.getCurrentBall()
  333. ball.node.parent = this.ballParent
  334. ball.node.position = this.role.node.position
  335. /*因发射出来的小球可能不只一个球同时存在,所以建立数组来保存
  336. 小球以及相应发射该小球时的角度(经测试最多同时存在3个)*/
  337. this.ballShoot.push([ball, radian])
  338. if (this.zumaUI.curBalls > 1) this.zumaUI.curBalls -= 1
  339. }
  340. public hasSpecialBall(): boolean {
  341. return this.rollBalls.every(v => v.ballCfg.association[0])
  342. }
  343. private onShootBallMove(): void {
  344. if (this.ballShoot.length != 0) {
  345. //这个循环对发射出来的每个球都进行检测
  346. for (let i: number = 0; i < this.ballShoot.length; ++i) {
  347. let ballNode: cc.Node = this.ballShoot[i][0].node
  348. let ball: Ball = this.ballShoot[i][0]
  349. let radian: number = this.ballShoot[i][1]
  350. //这里检测是否越界
  351. if (
  352. ballNode.x > -ConstValue.REAL_WIDTH / 2 &&
  353. ballNode.x < ConstValue.REAL_WIDTH / 2 &&
  354. ballNode.y > -ConstValue.REAL_HEIGHT / 2 &&
  355. ballNode.y < ConstValue.REAL_HEIGHT / 2
  356. ) {
  357. //检测碰撞
  358. let flag: number = this.checkCollision(ballNode)
  359. if (flag == -1) {
  360. ballNode.x += Math.cos(radian) * this.shootSpeed
  361. ballNode.y += Math.sin(radian) * this.shootSpeed
  362. } else {
  363. // soundMgr.CollisionSound.play(100);
  364. let dis: number = Math.sqrt(
  365. (this.rollBalls[flag].node.x - ballNode.x) * (this.rollBalls[flag].node.x - ballNode.x) +
  366. (this.rollBalls[flag].node.y - ballNode.y) * (this.rollBalls[flag].node.y - ballNode.y),
  367. )
  368. //调整下小球的位置使之与碰撞的小球刚好接触
  369. ballNode.x -= (this.ballSize - dis) * Math.cos(radian)
  370. ballNode.y -= (this.ballSize - dis) * Math.sin(radian)
  371. /*碰到了则算出球应该在哪两个小球中挤入进去,采用检测相邻两个放球的位置与要
  372. 插入的球的距离的方法,哪个距离近则向哪边插入小球,这也是为什么要将起点跟终
  373. 点设为与实际路径的起点终点相差this.startIndex个位置的原因,防止+this.startIndex或者-this.startIndex之后造成数组的越界
  374. */
  375. //算出距前一个小球的位置的距离
  376. let PrevX: number = Data.zuma.mapData[this.rollBalls[flag].posIndex - this.startIndex][0]
  377. let PrevY: number = Data.zuma.mapData[this.rollBalls[flag].posIndex - this.startIndex][1]
  378. let prevDis: number = Math.sqrt(
  379. (ballNode.x - PrevX) * (ballNode.x - PrevX) + (ballNode.y - PrevY) * (ballNode.y - PrevY),
  380. )
  381. //算出距下一个小球的位置的距离
  382. let NextX: number = Data.zuma.mapData[this.rollBalls[flag].posIndex + this.startIndex][0]
  383. let NextY: number = Data.zuma.mapData[this.rollBalls[flag].posIndex + this.startIndex][1]
  384. let nextDis: number = Math.sqrt(
  385. (ballNode.x - NextX) * (ballNode.x - NextX) + (ballNode.y - NextY) * (ballNode.y - NextY),
  386. )
  387. //比较距离远近,从而得出应在被碰撞的球的前一个位置插入还是后一个位置插入
  388. this.insertBall(ball, flag, prevDis > nextDis)
  389. this.ballShoot.splice(i, 1)
  390. }
  391. } else {
  392. //越界则从显示列表中remove掉,并从ballShoot中移除该小球
  393. this.putBall(ball)
  394. this.ballShoot.splice(i, 1)
  395. }
  396. }
  397. }
  398. }
  399. //-----------检测发射出来的小球与球链中球的碰撞,碰撞了返回被碰撞球的索引,否则返回-1
  400. private checkCollision(ballNode: cc.Node): number {
  401. //这里检测是否与球链self._rollBalls中的球碰撞,碰撞采用距离计算
  402. for (let j: number = 0; j < this.rollBalls.length; ++j) {
  403. let targetNode = this.rollBalls[j].node
  404. let dis: number = Math.sqrt(
  405. (targetNode.x - ballNode.x) * (targetNode.x - ballNode.x) +
  406. (targetNode.y - ballNode.y) * (targetNode.y - ballNode.y),
  407. )
  408. if (dis <= this.ballSize) {
  409. return j
  410. }
  411. }
  412. return -1
  413. }
  414. //--------插入小球,第一个参数是要插入的小球,第二个是被碰撞的小球的索引,第三个是标志向前插入还是向后插入
  415. private insertBall(ball: Ball, index: number, isNext: boolean): void {
  416. let posX: number
  417. let posY: number
  418. let insertPos: number
  419. //确定obj要运动到的位置
  420. if (isNext) {
  421. insertPos = this.rollBalls[index].posIndex + this.ballStep
  422. //插入的位置后面还有小球并且在挤入的时候会发生碰撞就需要检查与这个小球的碰撞情况
  423. if (
  424. this.rollBalls[index + 1] &&
  425. this.rollBalls[index + 1].posIndex - this.rollBalls[index].posIndex < this.ballStep * 2
  426. ) {
  427. this.ballCrushed.push([ball, this.rollBalls[index + 1]])
  428. }
  429. } else {
  430. //向前插入分两种情况,第一种就是向前插入会挤到前面的球,此时插入的球的位置应是被碰小球的
  431. //前一个球的posIndex+this.ballStep位置,并推动后面的球空出位置出来
  432. if (
  433. this.rollBalls[index - 1] &&
  434. this.rollBalls[index].posIndex - this.rollBalls[index - 1].posIndex < this.ballStep * 2
  435. ) {
  436. insertPos = this.rollBalls[index - 1].posIndex + this.ballStep
  437. this.ballCrushed.push([ball, this.rollBalls[index]])
  438. }
  439. //第二种情况就是向前插入不会挤到前面的球,此时插入的球的位置应是被碰小球的posIndex-this.ballStep个位置
  440. else {
  441. insertPos = this.rollBalls[index].posIndex - this.ballStep
  442. }
  443. }
  444. if (insertPos < 0) {
  445. this.putBall(ball)
  446. return
  447. }
  448. posX = Data.zuma.mapData[insertPos][0]
  449. posY = Data.zuma.mapData[insertPos][1]
  450. //做一个简单的动画效果
  451. cc.tween(ball.node)
  452. .to(this.ballStep / this.stepLength / 60 - 0.1, {x: posX, y: posY}, {easing: 'sineOut'})
  453. .call(() => {
  454. if (this.node.activeInHierarchy) this.motionFinished(ball, insertPos)
  455. })
  456. .start()
  457. }
  458. //------逐帧检测挤入的小球与后一个小球的碰撞情况,如果距离小于this.ballSize即为碰撞,
  459. //------此时要将球后移,直到没碰撞,形成一个挤入的效果-------------------------
  460. private checkPushCollision(): boolean {
  461. if (this.ballCrushed.length != 0) {
  462. for (let i: number = 0; i < this.ballCrushed.length; ++i) {
  463. let firstBall = this.ballCrushed[i][0]
  464. let secondBall = this.ballCrushed[i][1]
  465. let firstBallNode = this.ballCrushed[i][0].node
  466. let secondBallNode = this.ballCrushed[i][1].node
  467. let dis: number = Math.sqrt(
  468. (firstBallNode.x - secondBallNode.x) * (firstBallNode.x - secondBallNode.x) +
  469. (firstBallNode.y - secondBallNode.y) * (firstBallNode.y - secondBallNode.y),
  470. )
  471. let isCollision: boolean = dis < this.ballSize ? true : false
  472. let moveStep: number = 0
  473. while (isCollision) {
  474. //如果是碰撞的则假设小球向前移动一步在检测是否碰撞,如此下去直到不碰撞了
  475. ++moveStep
  476. if (secondBall.posIndex + moveStep > Data.zuma.mapData.length - 1) {
  477. isCollision = false
  478. } else {
  479. dis = Math.sqrt(
  480. (firstBallNode.x - Data.zuma.mapData[secondBall.posIndex + moveStep][0]) *
  481. (firstBallNode.x - Data.zuma.mapData[secondBall.posIndex + moveStep][0]) +
  482. (firstBallNode.y - Data.zuma.mapData[secondBall.posIndex + moveStep][1]) *
  483. (firstBallNode.y - Data.zuma.mapData[secondBall.posIndex + moveStep][1]),
  484. )
  485. isCollision = dis < this.ballSize ? true : false
  486. }
  487. }
  488. //执行推进小球的操作,指定moveStep为应推进的步数,执行前需检查被检测碰撞的球
  489. //是否由于执行消除操作删除了,没有被删除才能执行推动小球,否则将引起数组操作越界
  490. let index: number
  491. index = this.rollBalls.indexOf(secondBall)
  492. if (index != -1) {
  493. this.pushBallFrom(index, moveStep)
  494. }
  495. }
  496. }
  497. return this.ballCrushed.length != 0
  498. }
  499. //-------动画效果完成后要将小球插入到数组中,并调整好位置,第一个参数是要插入的小球,
  500. //-------第二个是球插入到地图数组中的位置,第三个是标志向前插入还是向后插入----------------------------
  501. private motionFinished(ball: Ball, insertPos: number): void {
  502. let index: number
  503. //算出应插入到self._rollBalls中位置的索引,这里采用传入insertPos参数的方式
  504. //而不是传入被碰撞小球的引用的原因是为了防止消除操作删除掉了小球而算不出
  505. //数组的插入位置
  506. for (let i: number = 0; i < this.rollBalls.length; ++i) {
  507. if (this.rollBalls[i].posIndex > insertPos) {
  508. index = i
  509. break
  510. }
  511. if (i == this.rollBalls.length - 1) {
  512. index = i + 1
  513. }
  514. }
  515. //删除对应ballCrushed中的元素
  516. this.ballCrushed.splice(
  517. this.ballCrushed.findIndex(v => v[0] == ball),
  518. 1,
  519. )
  520. //根据插入的位置在self._rollBalls中插入该小球
  521. ball.posIndex = insertPos
  522. this.rollBalls.splice(index, 0, ball)
  523. //检查插入后是否有被吸引的小球
  524. if (
  525. this.rollBalls[index - 1] &&
  526. this.rollBalls[index - 1].ballID == this.rollBalls[index].ballID &&
  527. this.rollBalls[index].posIndex - this.rollBalls[index - 1].posIndex > this.ballStep + 1
  528. ) {
  529. this.addToBallAttracted(this.rollBalls[index])
  530. }
  531. if (
  532. this.rollBalls[index + 1] &&
  533. this.rollBalls[index + 1].ballID == this.rollBalls[index].ballID &&
  534. this.rollBalls[index + 1].posIndex - this.rollBalls[index].posIndex > this.ballStep + 1
  535. ) {
  536. this.addToBallAttracted(this.rollBalls[index + 1])
  537. }
  538. //检查是有要清除的小球
  539. this.clearCheck(index, true)
  540. }
  541. /**
  542. * 传入检查的起点向两端开始搜索,检查是否应消除小球传入一个参数clear
  543. * 表示是否执行消去,若为false则只返回搜索到颜色相同的球有几个
  544. */
  545. private clearCheck(index: number, clear: boolean): number {
  546. if (!this.rollBalls[index]) return
  547. let temp: any[] = []
  548. temp.push(this.rollBalls[index])
  549. let ballID: number = this.rollBalls[index].ballID
  550. //此循环向下搜索
  551. //是否要清除的参数为false则将不相连的也算起(为判断是否应取消连击计数提供依据),否则不算,下同
  552. for (let i: number = index + 1; i < this.rollBalls.length; i++) {
  553. if (
  554. this.rollBalls[i] &&
  555. (this.rollBalls[i].ballID == ballID || this.rollBalls[i].ballCfg.association.includes(ballID))
  556. ) {
  557. //有一定的间隙也算作是连接,由于挤入的操作会使球插入后不一定绝对
  558. //只相差this.ballStep个位置,所以这里判断是否连接的条件应当放宽一点,this.ballStep的一点5倍而不是this.ballStep
  559. if (this.rollBalls[i].posIndex - this.rollBalls[i - 1].posIndex <= this.ballStep * 1.5) {
  560. temp.push(this.rollBalls[i])
  561. } else if (!clear) {
  562. temp.push(this.rollBalls[i])
  563. } else {
  564. break
  565. }
  566. } else {
  567. break
  568. }
  569. }
  570. //此循环向上搜索
  571. let j: number = index - 1
  572. while (this.rollBalls[j]) {
  573. if (this.rollBalls[j].ballID == ballID || this.rollBalls[j].ballCfg.association.includes(ballID)) {
  574. if (this.rollBalls[j + 1].posIndex - this.rollBalls[j].posIndex <= this.ballStep + 1) {
  575. temp.push(this.rollBalls[j])
  576. --j
  577. } else if (!clear) {
  578. temp.push(this.rollBalls[j])
  579. --j
  580. } else break
  581. } else {
  582. break
  583. }
  584. }
  585. //将j加1后传给清除小球函数,作为删除小球的起点
  586. ++j
  587. //temp长度大于三则执行消除小球,传入该消除的小球数组以及起始球索引
  588. if (temp.length > 2 && clear) {
  589. this.clearBall(j, temp)
  590. }
  591. return temp.length
  592. }
  593. //------------------消除小球,并在消除后检测是否两端小球颜色相同,相同则应该吸引过去-----------
  594. private clearBall(f: number, arr: Ball[]): void {
  595. //当前连击数加1
  596. ++this.curCombo
  597. //播放的音调
  598. let id: number = this.curCombo > 5 ? 5 : this.curCombo
  599. if (this.curCombo > 1) {
  600. // soundMgr.BallExplosionSound.play();
  601. }
  602. for (let i: number = 0; i < arr.length; ++i) {
  603. //小球爆炸
  604. arr[i].explode()
  605. // soundMgr.playCollisionSound(id);
  606. }
  607. //从球链数组中删除这些小球前先判断删除后是否游戏过关,如删除后游戏过关就得先获取self._rollBalls中
  608. //最后一个球的posIndex位置传给extraScore()函数,计算出额外的加分
  609. if (this.rollBalls.length == arr.length) {
  610. this.gamePass()
  611. this.lastId = this.rollBalls[this.rollBalls.length - 1].posIndex
  612. }
  613. this.rollBalls.splice(f, arr.length)
  614. let clearID = arr.find(v => !v.ballCfg.association[0]).ballID
  615. //如果消除数量大于x,生产特殊球
  616. let sameBalls = arr.filter(v => v.ballID == clearID)
  617. //S球产生的buff推入队列
  618. let specials = arr.filter(v => v.ballCfg.association[0] > 0)
  619. specials.forEach(v => {
  620. v.ballCfg.activation.forEach((buff, index) => {
  621. if (v.ballCfg.association.indexOf(clearID) == index) {
  622. this.zumaUI.addZumaBuff(buff)
  623. this.flyZumaBuff(buff, v.node.position)
  624. }
  625. })
  626. })
  627. //特殊球生成
  628. let ballCfg = sameBalls[0].ballCfg
  629. if (ballCfg.specific <= sameBalls.length) {
  630. let specialBall = this.getBallFromPool()
  631. specialBall.init(ballCfg.generate, this)
  632. specialBall.posIndex = arr[0].posIndex
  633. this.rollBalls.splice(f, -1, specialBall)
  634. specialBall.node.parent = this.ballParent
  635. }
  636. //大于一定数量buff生成
  637. let selfBuff = ballCfg.trigger[ballCfg.eliminate.indexOf(sameBalls.length)]
  638. if (selfBuff) {
  639. this.zumaUI.addZumaBuff(selfBuff)
  640. this.flyZumaBuff(selfBuff, sameBalls[0].node.position)
  641. }
  642. //若全部球滚出来了,就需要检查发射器中球的颜色了
  643. if (this.rollInTotalCount == 0) {
  644. this.checkColor(clearID)
  645. }
  646. //检测断开的球链两端颜色是否相同应该吸引过去,注意此处删除了小球,应对比颜色的小球索引有变化
  647. if (
  648. this.rollBalls[f - 1] &&
  649. this.rollBalls[f] &&
  650. (this.rollBalls[f - 1].ballID == this.rollBalls[f].ballID ||
  651. this.rollBalls[f - 1].ballCfg.association.includes(this.rollBalls[f].ballID))
  652. ) {
  653. //利用clearCheck的返回值来确定是否为连击,吸引结束不会引发消除则更新最大连击数
  654. //并将当前连击数置为0
  655. if (this.clearCheck(f, false) < 3) {
  656. if (this.curCombo > this.maxCombo) {
  657. this.maxCombo = this.curCombo
  658. }
  659. this.curCombo = 0
  660. }
  661. //因在吸引过程中球链可能会有变动,比如爆炸、插入后,导致不能吸引,所以逐帧执行检测是否要吸引,延迟400毫秒执行
  662. //将被吸引的小球加入检测数组
  663. this.addToBallAttracted(this.rollBalls[f])
  664. } else {
  665. //这里需要更新连击数
  666. if (this.curCombo > this.maxCombo) {
  667. this.maxCombo = this.curCombo
  668. }
  669. this.curCombo = 0
  670. }
  671. }
  672. /** 检查发射器的颜色,使发射器中球不会出现球链中没有的颜色 */
  673. private checkColor(ballID: number): void {
  674. //检查球链中的球
  675. for (let i: number = 0; i < this.rollBalls.length; ++i) {
  676. if (this.rollBalls[i].ballID == ballID) return
  677. }
  678. //检查发射出来的球但还未插入的
  679. for (let j: number = 0; j < this.ballShoot.length; ++j) {
  680. if (this.ballShoot[j][0].ballID == ballID) return
  681. }
  682. this.role.colorCleared(ballID)
  683. }
  684. /** 将需要被检测吸引的小球加入ballAttracted数组 */
  685. private addToBallAttracted(ball: Ball): void {
  686. this.ballAttracted.push(ball)
  687. }
  688. /** 传入球断开的球的两端的球的索引点,将球吸引闭合起来 */
  689. private attract(): boolean {
  690. if (this.ballAttracted.length != 0) {
  691. for (let i: number = 0; i < this.ballAttracted.length; ++i) {
  692. let index: number = this.rollBalls.indexOf(this.ballAttracted[i])
  693. if (index != -1 && this.rollBalls[index - 1]) {
  694. if (
  695. this.ballAttracted[i].ballID == this.rollBalls[index - 1].ballID ||
  696. this.rollBalls[index - 1].ballCfg.association.includes(this.ballAttracted[i].ballID)
  697. ) {
  698. //算出应移动多少步
  699. let steps: number =
  700. this.ballAttracted[i].posIndex - this.rollBalls[index - 1].posIndex >
  701. this.ballStep + this.levelCfg.stableSpeed
  702. ? this.levelCfg.stableSpeed
  703. : this.ballAttracted[i].posIndex - this.rollBalls[index - 1].posIndex - this.ballStep
  704. this.pushBallFrom(index, -steps)
  705. //判断吸引是否结束了
  706. if (this.ballAttracted[i].posIndex - this.rollBalls[index - 1].posIndex <= this.ballStep) {
  707. //TODO
  708. // soundMgr.CollisionSound.play();
  709. //吸引结束从数组中删除
  710. this.ballAttracted.splice(i, 1)
  711. this.clearCheck(index - 1, true)
  712. }
  713. } else {
  714. //此种情况针对由于消除、插入操作造成的不能继续吸引,并且连击条件被破坏
  715. this.ballAttracted.splice(i, 1)
  716. if (this.curCombo > this.maxCombo) {
  717. this.maxCombo = this.curCombo
  718. }
  719. this.curCombo = 0
  720. }
  721. }
  722. }
  723. }
  724. return this.ballAttracted.length == 0
  725. }
  726. /** 游戏过关 */
  727. private gamePass(): void {
  728. this.gameSate = GAME_STATE.gamePass
  729. this.extraScore()
  730. this.role.clearBalls()
  731. this.ballParent.children.forEach(v => this.putBall(v.getComponent(Ball)))
  732. this.zumaUI.endZuma()
  733. }
  734. /** 计算加分 */
  735. private extraScore(): void {
  736. if (this.lastId + this.startIndex < Data.zuma.mapData.length - this.startIndex - 1) {
  737. this.lastId += this.startIndex
  738. //TODO 播放结算音效
  739. // soundMgr.EndExplosionSound.play();
  740. }
  741. }
  742. setBallNum(num: number) {
  743. ccUtils.setLabel(num.toString(), this.node, 'goods/lb')
  744. }
  745. setBuffNum() {
  746. ccUtils.setLabel(this.zumaUI.activeBuff.length.toString(), this.buffNode, 'lb')
  747. }
  748. /** 飞入祖玛buff */
  749. flyZumaBuff(buffID: number, pos: cc.Vec3) {
  750. let origin = Data.game.gameAssetMap.get('prefab/zuma/zumabuff') as cc.Prefab
  751. let node: cc.Node = this.buffPool.get()
  752. if (!node) node = cc.instantiate(origin)
  753. this.zumaUI.initZumaBuffItem(buffID, node)
  754. node.stopAllActions()
  755. node.parent = cc.find('buffFlyParent', this.node)
  756. node.setPosition(pos)
  757. cc.tween(node)
  758. .then(cc.tween().to(1, {x: this.buffNode.x, y: this.buffNode.y}, {easing: 'quartOut'}))
  759. .call(() => {
  760. if (cc.isValid(node)) {
  761. this.buffPool.put(node)
  762. }
  763. })
  764. .start()
  765. }
  766. }