ZumaCore.ts 32 KB

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