import { applyRainbowCut } from "./RainbowCut"; import { chooseRainbowTarget, rainbowPriority, RainbowEnergy, RAINBOW_CUTS, RAINBOW_CAPACITY, RAINBOW_CAST_SECONDS } from "./RainbowRules"; /** 每关独立持有;原地复活不重建,不占用暂停或道具的状态。 */ export default class RainbowStreak { readonly energy = new RainbowEnergy(); casting = false; private first = true; private hidden = false; private credited = new Set(); private bottle: cc.Node; private shield: cc.Node = null; private flight: cc.Node = null; private elapsed = 0; private selection: any = null; private from: cc.Vec2; private to: cc.Vec2; private finger: any = null; private location: cc.Vec2 = null; private released = false; private hide = () => { this.hidden = true; }; private show = () => { this.hidden = false; }; constructor(private map: any, bottle: cc.Node) { this.bottle = bottle; if (bottle) bottle.active = true; cc.game.on(cc.game.EVENT_HIDE, this.hide); cc.game.on(cc.game.EVENT_SHOW, this.show); this.refreshAvailability(); this.render(); } private candidates(): any[] { return this.map.blocks.filter(node => cc.isValid(node, true)).map(node => node.getComponent("Block")) .filter(block => block && !block.spawnLocked && !block.over && !block.rainbowEliminating && block.color !== 11 && block.type !== 11 && block.type !== 22 && block.type !== 10 && block.allBlocks.length > 1) .map(block => { const upper = block.type === 1 && block.block_Info.node && block.block_Info.node.getComponent("Block"); return { block, cuts: RAINBOW_CUTS[block.block_Info.block], priority: Math.max(rainbowPriority(block), upper ? rainbowPriority(upper) : 0) }; }).filter(candidate => candidate.cuts.length > 0); } private refreshAvailability(): any[] { const candidates = this.candidates(); // 正在消除或露出上层的块仍可能改变可选池,等当前过程结束再永久停用。 const pending = this.map.hasPendingSpawnGateWork() || this.map.blocks.some(node => { const block = cc.isValid(node, true) && node.getComponent("Block"); return block && (block.spawnLocked || block.over || block.rainbowEliminating || (block.type === 10 && block.allBlocks.length > 1)); }); this.energy.setAvailability(candidates.length > 0, pending); return candidates; } private counting(): boolean { return !this.hidden && this.map.gameStart && this.map.scheduleCallback != null && !this.map.pause && !this.map.gameOver && !this.map.gameWin && !this.map.shopPause && !this.map.isPauseOpen() && !this.map.iceTrue() && this.map.timeNumber > 0; } private settling(): boolean { return this.map.node.children.some(node => { const block = cc.isValid(node, true) && node.active && node.getComponent("Block"); return block && (block.over || block.rainbowEliminating || node.getNumberOfRunningActions() > 0); }); } onPress(block: any, event: any): void { if (!this.first || !this.counting()) return; this.first = false; const excluded = new Set([block.node]); // 整组都跟随本次手势,不能裁到正在按住的联动成员。 this.addLinked(block.node, excluded); const candidates = this.refreshAvailability().filter(candidate => !excluded.has(candidate.block.node)); if (!this.energy.consume()) return; this.finger = block; this.location = event.getLocation().clone(); this.released = false; this.begin(candidates); if (!this.casting) this.finger = null; this.render(); } private addLinked(node: cc.Node, excluded: Set): void { const block = node.getComponent("Block"); const linked = (block.teamBlocks || []).slice(); if ((block.type === 9 || block.type === 1 || block.type === 10) && block.block_Info.node) linked.push(block.block_Info.node); linked.forEach(member => { if (cc.isValid(member, true) && !excluded.has(member)) { excluded.add(member); this.addLinked(member, excluded); } }); } capture(block: any, event: any, released: boolean): void { if (this.finger !== block) return; this.location = event.getLocation().clone(); this.released = this.released || released; block.touchDelta = cc.v2(0, 0); } eliminated(node: cc.Node): void { if (this.credited.has(node)) return; this.credited.add(node); const block = node.getComponent("Block"); if (block) this.energy.eliminated(block.allBlocks.length); this.render(); } update(dt: number): void { if (this.map.gameOver || this.map.gameWin) { if (this.casting) this.finish(false); return; } if (this.hidden) return; if (this.energy.stopped && !this.casting) return; if (this.casting && this.counting()) { this.elapsed += dt; const t = Math.min(1, this.elapsed / RAINBOW_CAST_SECONDS); if (this.flight) this.flight.setPosition(this.from.x + (this.to.x - this.from.x) * t, this.from.y + (this.to.y - this.from.y) * t + 100 * 4 * t * (1 - t)); if (t >= 1) this.finish(true); } const candidates = this.refreshAvailability(); this.energy.tick(dt, !this.first && this.counting()); if (!this.first && !this.casting && this.counting() && !this.map.touchIng && !this.map.ishammer && !this.map.ismagic && !this.map.magicMask.active && !this.map.blocks.some(node => cc.isValid(node, true) && node.getComponent("Block").isTouch) && !this.settling() && this.energy.consume()) this.begin(candidates); this.render(); } private begin(candidates: any[]): void { const choice = chooseRainbowTarget(candidates); if (!choice) return; const block = choice.target.block; this.selection = { block, cut: choice.cut, shape: block.block_Info.block, x: block.posX, y: block.posY }; this.casting = true; this.elapsed = 0; const canvas = cc.find("Canvas"); this.shield = new cc.Node("rainbowIntercept"); this.shield.setContentSize(canvas.getContentSize()); this.shield.addComponent(cc.BlockInputEvents); canvas.addChild(this.shield); this.shield.zIndex = cc.macro.MAX_ZINDEX; if (this.map.magics && this.bottle) { this.flight = cc.instantiate(this.map.magics); this.flight.parent = this.map.magics.parent; this.flight.active = true; this.flight.zIndex = cc.macro.MAX_ZINDEX; this.from = this.flight.parent.convertToNodeSpaceAR(this.bottle.convertToWorldSpaceAR(cc.v2(0, 0))); const cell = this.map.mapBlocksWall[block.posX + choice.cut.cell.x][block.posY + choice.cut.cell.y]; this.to = this.flight.parent.convertToNodeSpaceAR(cell.parent.convertToWorldSpaceAR(cc.v2(cell.x, cell.y))); this.flight.setPosition(this.from); this.flight.getComponentsInChildren(cc.ParticleSystem).forEach(particle => particle.resetSystem()); } } private finish(apply: boolean): void { const target = this.selection; try { if (apply && target && cc.isValid(target.block.node, true) && this.map.blocks.indexOf(target.block.node) >= 0 && !target.block.over && !target.block.rainbowEliminating && target.block.block_Info.block === target.shape && target.block.posX === target.x && target.block.posY === target.y) { applyRainbowCut(this.map, target.block, target.cut); } } finally { this.casting = false; this.selection = null; if (this.shield) { this.shield.active = false; this.shield.destroy(); this.shield = null; } if (this.flight) { this.flight.destroy(); this.flight = null; } const block = this.finger; this.finger = null; if (apply && block && cc.isValid(block.node, true) && block.isTouch) { const point = this.location.clone(); const local = block.node.parent.convertToNodeSpaceAR(point); block.relative_Position = cc.v2(block.node.x - local.x, block.node.y - local.y); block.touchDelta = cc.v2(0, 0); if (this.released) block.touchEnd({ getLocation: () => point }); } } } private render(): void { if (!this.bottle || !cc.isValid(this.bottle, true)) return; const disabled = this.energy.stopped || this.energy.unavailable; this.bottle.color = disabled ? cc.color(110, 110, 110) : cc.Color.WHITE; this.bottle.getComponent(cc.Sprite).fillRange = disabled ? 1 : this.energy.value / RAINBOW_CAPACITY; } dispose(): void { this.finish(false); cc.game.off(cc.game.EVENT_HIDE, this.hide); cc.game.off(cc.game.EVENT_SHOW, this.show); } }