import { applyRainbowCut } from "./RainbowCut"; import RainbowTrail from "./RainbowTrail"; import RainbowBottleWater from "./RainbowBottleWater"; 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 flight: RainbowTrail = null; private fadingFlights: RainbowTrail[] = []; private elapsed = 0; private selection: any = null; private from: cc.Vec2; private to: cc.Vec2; 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; if (!bottle.getComponent(RainbowBottleWater)) bottle.addComponent(RainbowBottleWater); } 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 && !this.isButtonColor(block)) .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 isButtonColor(block: any): boolean { const upper = block.type === 1 && block.block_Info.node && block.block_Info.node.getComponent("Block"); return (this.map.buttonBlock || []).some(node => { const button = cc.isValid(node, true) && node.getComponent("MapBlock"); return button && button.color > 0 && (button.color === block.color || (upper && button.color === upper.color)); }); } 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.allBlocks.length > 1 && (block.type === 10 || this.isButtonColor(block)))); }); 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); }); } private linkedNodes(block: any): cc.Node[] { const nodes = (block.teamBlocks || []).slice(); if ((block.type === 9 || block.type === 1 || block.type === 10) && block.block_Info.node) { nodes.push(block.block_Info.node); } return nodes.filter(node => cc.isValid(node, true) && node.getComponent("Block")); } private linkedGroup(block: any): any[] { if (!cc.isValid(block.node, true)) return []; const members = this.map.node.children.filter(node => cc.isValid(node, true)) .map(node => node.getComponent("Block")).filter(Boolean) .map(member => ({ block: member, links: this.linkedNodes(member) })); const group = [block]; for (let i = 0; i < group.length; i++) { const links = this.linkedNodes(group[i]); members.forEach(member => { // 地板先解除的成员可能只剩队友对它的反向引用,也属于同一联动组。 if (group.indexOf(member.block) < 0 && (links.indexOf(member.block.node) >= 0 || member.links.indexOf(group[i].node) >= 0)) group.push(member.block); }); } return group; } private groupKey(group: any[]): string { return group.map(block => block.node.uuid + ":" + this.linkedNodes(block).map(node => node.uuid).sort().join(",") + ":" + (block.block_Info.floorMove === true)).sort().join("|"); } private groupBusy(group: any[]): boolean { return group.some(block => block.isTouch || block.over || block.rainbowEliminating || block.spawnLocked || block.node.getNumberOfRunningActions() > 0); } private checkSelection(): void { if (!this.selection || this.selection.missed) return; const group = this.linkedGroup(this.selection.block); if (this.groupKey(group) !== this.selection.groupKey || this.groupBusy(group)) this.selection.missed = true; } onPress(block: any): void { // 先记录本轮操作,不能被“非首次点击”的提前返回跳过;松手后也不恢复本轮。 if (this.selection && (this.selection.group.indexOf(block) >= 0 || this.linkedGroup(this.selection.block).indexOf(block) >= 0)) this.selection.missed = true; if (!this.first || !this.counting()) return; this.first = false; // 首次点击立即施放,避开当前按住的整个联动组;后续仍在 update 中等松手。 const excluded = this.linkedGroup(block); const candidates = this.refreshAvailability().filter(candidate => excluded.indexOf(candidate.block) < 0); if (this.energy.consume()) this.begin(candidates); this.render(); } eliminated(node: cc.Node): void { if (this.credited.has(node)) return; this.credited.add(node); const block = node.getComponent("Block"); if (block && !this.map.gameOver && !this.map.gameWin) 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); this.clearTrails(); this.render(); return; } this.checkSelection(); const paused = !this.counting(); if (this.flight) this.flight.update(paused); this.fadingFlights = this.fadingFlights.filter(trail => trail.update(paused)); 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) { // 先抛到目标上方,再竖直落入目标格;弧顶比两端高 180。 const top = Math.max(this.from.y, this.to.y) + 180; const controlY = top + Math.sqrt((top - this.from.y) * (top - this.to.y)); const u = 1 - t; this.flight.node.setPosition(this.to.x + (this.from.x - this.to.x) * u * u, u * u * this.from.y + 2 * u * t * controlY + t * t * this.to.y); this.flight.setProgress(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.node.children.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; const group = this.linkedGroup(block); this.selection = { block, cut: choice.cut, shape: block.block_Info.block, x: block.posX, y: block.posY, nodeX: block.node.x, nodeY: block.node.y, group, groupKey: this.groupKey(group), missed: this.groupBusy(group) }; this.casting = true; this.elapsed = 0; // 只记录目标格子的原位置;飞行期间目标和联动成员都照常接受操作。 if (this.map.magics && this.bottle) { const parent = this.map.magics.parent; this.from = 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 = parent.convertToNodeSpaceAR(cell.parent.convertToWorldSpaceAR(cc.v2(cell.x, cell.y))); this.flight = new RainbowTrail(parent, RAINBOW_CAST_SECONDS); this.flight.node.setPosition(this.from); } } private finish(apply: boolean): void { this.checkSelection(); const target = this.selection; try { if (apply && target && !target.missed && cc.isValid(target.block.node, true) && this.map.blocks.indexOf(target.block.node) >= 0 && !target.block.over && !target.block.rainbowEliminating && !this.isButtonColor(target.block) && target.block.block_Info.block === target.shape && target.block.allBlocks.some(cell => cell.x === target.cut.cell.x && cell.y === target.cut.cell.y) && target.block.posX === target.x && target.block.posY === target.y // 原节点、原形状、原局部格和实际原点必须一致,不能用移到落点上的另一格代替。 && Math.abs(target.block.node.x - target.nodeX) < 0.01 && Math.abs(target.block.node.y - target.nodeY) < 0.01) { applyRainbowCut(this.map, target.block, target.cut); } } finally { this.casting = false; this.selection = null; if (this.flight) { if (apply) { this.flight.stop(); this.fadingFlights.push(this.flight); } else this.flight.destroy(); this.flight = null; } } } private render(): void { if (!this.bottle || !cc.isValid(this.bottle, true)) return; const disabled = this.map.gameOver || this.map.gameWin || 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); this.clearTrails(); cc.game.off(cc.game.EVENT_HIDE, this.hide); cc.game.off(cc.game.EVENT_SHOW, this.show); } private clearTrails(): void { this.fadingFlights.forEach(trail => trail.destroy()); this.fadingFlights = []; } }