/** 清洗状态由 Block 立即更新;这里只有逐格泥巴渐隐及气泡尾效。 */ export default class WashingEffect { private layer = new cc.Node("washingEffect"); private bubbles = new cc.Node("washingBubbles"); private pending = 0; private callbacks: (() => void)[] = []; private disposed = false; constructor(private parent: cc.Node) { parent.addChild(this.layer); this.bubbles.zIndex = 1000; const icon = parent.getChildByName("icon"); if (icon) { this.layer.setSiblingIndex(icon.getSiblingIndex() + 1); this.layer.zIndex = icon.zIndex; } // 单色地块以 simple 格图替代 icon。 parent.children.filter(child => child.name === "simple").forEach(child => { this.layer.zIndex = Math.max(this.layer.zIndex, child.zIndex + 1); }); } get isCleaning(): boolean { return this.pending > 0; } play(cells: { x: number; y: number; frame: number }[], dirtFrames: cc.SpriteFrame[], bubbleFrame: cc.SpriteFrame): void { if (this.disposed || !cc.isValid(this.layer, true)) return; for (const cell of cells) { const x = -65 + cell.x * 120, y = 60 + cell.y * 120; const frame = dirtFrames[cell.frame]; if (frame) { const dirt = this.sprite("washingDirt", frame, x, y, 120); this.pending++; cc.tween(dirt).to(0.2, { opacity: 0 }).call(() => { if (this.disposed) return; this.remove(dirt); this.pending--; this.flush(); }).start(); } if (!bubbleFrame || !this.parent.parent) continue; if (!this.bubbles.parent) this.parent.parent.addChild(this.bubbles); // 气泡留在洗澡地形上,方块移走时不跟随。 const center = this.bubbles.convertToNodeSpaceAR(this.parent.convertToWorldSpaceAR(cc.v2(x, y))); for (let i = 0; i < 12; i++) { const bubble = this.sprite("washingBubble", bubbleFrame, center.x + (i % 3 - 1) * 28 + Math.random() * 12 - 6, center.y - 42 + Math.floor(i / 3) * 24 + Math.random() * 12, 18 + Math.random() * 18, this.bubbles); bubble.opacity = 210; bubble.color = i % 3 === 1 ? cc.color(220, 249, 255) : cc.Color.WHITE; cc.tween(bubble).to(0.8 + Math.random() * 0.2, { x: bubble.x + Math.random() * 28 - 14, y: bubble.y + 126 + Math.random() * 72, opacity: 0, }, { easing: "sineOut" }).call(() => { this.remove(bubble); if (!this.bubbles.children.length) this.bubbles.removeFromParent(); }).start(); } } } whenClean(callback: () => void): void { if (this.disposed) return; this.callbacks.push(callback); this.flush(); } private flush(): void { while (!this.disposed && this.pending === 0 && this.callbacks.length) this.callbacks.shift()(); } private sprite(name: string, frame: cc.SpriteFrame, x: number, y: number, size: number, layer = this.layer): cc.Node { const node = new cc.Node(name); const sprite = node.addComponent(cc.Sprite); sprite.spriteFrame = frame; sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM; sprite.trim = false; node.setContentSize(size, size); node.setPosition(x, y); layer.addChild(node); return node; } private remove(node: cc.Node): void { if (!cc.isValid(node, true)) return; node.removeFromParent(); node.destroy(); } dispose(): void { if (this.disposed) return; this.disposed = true; this.callbacks = []; this.pending = 0; [this.layer, this.bubbles].forEach(layer => { layer.children.slice().forEach(node => cc.Tween.stopAllByTarget(node)); this.remove(layer); }); } }