//********************* // create by 流云 // time: Thu Nov 13 2025 // desc: //********************* import { _decorator, Button, Color, EventTouch, Graphics, Mask, Node, Rect, Tween, UITransform, tween, } from 'cc'; import { Auto_NewGuideMask } from './Auto_NewGuideMask'; import MTools from 'db://assets/mx/tools/MTools'; const { ccclass } = _decorator; @ccclass('UINewGuideMask') export class UINewGuideMask extends Auto_NewGuideMask { /** 挖洞比目标节点外扩像素(仅视觉,不扩大可点/穿透区域) */ private static readonly HOLE_PADDING = 20; /** 挖洞圆角半径 */ private static readonly HOLE_CORNER_RADIUS = 14; registNode: Node | null = null; /** 实际可点目标(Button 或 registNode 自身),穿透与完成均以此为准 */ private _hitNode: Node | null = null; graphics: Graphics = null; finishCallBack: (() => void) | null = null; nodeArray: Node[] = []; labArray: string[] = []; isPlayAnim = true; blackOpacity = 255; /** 不需要点击拦截 */ hideSpineshouzhi = true; private _targetClickBound = false; onLoad(): void { super.onLoad(); } onEnable(): void { super.onEnable(); } onDisable(): void { this._cancelGuideMotion(); this._unbindTargetClick(); super.onDisable(); } teardownBeforeDestroy(): void { this._cancelGuideMotion(); this._unbindTargetClick(); } /** 取消手指 tween / 挖洞延迟,避免 Tab 切换销毁目标后仍访问节点 */ private _cancelGuideMotion(): void { this.unschedule(this._applyFocusToNode); if (this.shouzhiNode?.isValid) { Tween.stopAllByTarget(this.shouzhiNode); } } /** 引导目标已销毁(如快切副本关掉角色页):关遮罩,进度保留可重进再引导 */ private _closeIfRegistNodeDead(): void { if (this.registNode && !this.registNode.isValid) { this.registNode = null; this._hitNode = null; this.close(); } } /** * 解析真实可点节点: * - registNode 自身有 Button → 用自身 * - 子树仅一个可交互 Button → 用该按钮(避免大容器命中、小按钮未点) * - 否则退回 registNode(自定义 TOUCH_END 的 item 等) */ private _resolveHitNode(registNode: Node | null): Node | null { if (!registNode?.isValid) return null; const selfBtn = registNode.getComponent(Button); if (selfBtn?.interactable && selfBtn.enabledInHierarchy) { return registNode; } const childBtns = registNode.getComponentsInChildren(Button).filter( (b) => b !== selfBtn && b.interactable && b.enabledInHierarchy && b.node.activeInHierarchy, ); if (childBtns.length === 1) { return childBtns[0].node; } return registNode; } private _getHitWorldRect(): Rect | null { const hit = this._hitNode; if (!hit?.isValid) return null; const ui = hit.getComponent(UITransform); return ui ? ui.getBoundingBoxToWorld() : null; } private _isTouchOnHitTarget(event: EventTouch): boolean { const rect = this._getHitWorldRect(); return !!rect && rect.contains(event.getUILocation()); } private _bindTargetClick(hitNode: Node): void { this._unbindTargetClick(); if (!hitNode?.isValid) return; hitNode.on(Node.EventType.TOUCH_END, this._onHitTargetTouchEnd, this); this._targetClickBound = true; } private _unbindTargetClick(): void { if (!this._targetClickBound) return; this._targetClickBound = false; const hit = this._hitNode; if (hit?.isValid) { hit.off(Node.EventType.TOUCH_END, this._onHitTargetTouchEnd, this); } } /** 仅当真实目标收到点击后才推进引导,避免点在挖洞/大框空白处就关掉遮罩 */ private _onHitTargetTouchEnd = (_event: EventTouch): void => { if (!this._hitNode?.isValid) return; this._finishCurrentStep(); }; private _finishCurrentStep(): void { this._unbindTargetClick(); this.registNode = null; this._hitNode = null; const cb = this.finishCallBack; cb?.(); if (!this.node?.isValid) return; if (this.nodeArray.length === 0) { this.close(); } else { const nextNode = this.nodeArray.shift(); this.nextNode(nextNode); } } bindNode() { this.node.on(Node.EventType.TOUCH_START, (event: EventTouch) => { if (!this.registNode || !this.registNode.isValid) { event.preventSwallow = true; event.propagationStopped = true; if (this.registNode && !this.registNode.isValid) { this.close(); } return; } // 仅真实按钮区域穿透;挖洞外扩区仍拦截,避免点空白推进 if (this._isTouchOnHitTarget(event)) { event.preventSwallow = true; event.propagationStopped = true; } else { event.preventSwallow = false; event.propagationStopped = false; } }, this); this.node.on(Node.EventType.TOUCH_END, (event: EventTouch) => { if (!this.registNode || !this.registNode.isValid) { event.preventSwallow = true; event.propagationStopped = true; if (this.registNode && !this.registNode.isValid) { this.close(); } return; } if (this._isTouchOnHitTarget(event)) { event.preventSwallow = true; event.propagationStopped = true; // 不在此推进:等下层真实目标 TOUCH_END(_onHitTargetTouchEnd) } else { event.preventSwallow = false; event.propagationStopped = false; } }, this); } nextNode(registNode) { if (!registNode?.isValid) { this._closeIfRegistNodeDead(); return; } this._unbindTargetClick(); this.registNode = registNode; this._hitNode = this._resolveHitNode(registNode); this._bindTargetClick(this._hitNode ?? registNode); const str = this.labArray.shift(); this._cancelGuideMotion(); const fingerTarget = this._hitNode ?? registNode; const posi = MTools.getNodeToTargetPos(this.shouzhiNode, fingerTarget); if (this.isPlayAnim && this.shouzhiNode.active) { tween(this.shouzhiNode) .to(0.6, { x: posi.x, y: posi.y - 5 }) .call(() => { if (!registNode.isValid) { this._closeIfRegistNodeDead(); return; } this._focusToNode(registNode); if (str && str != '') { this.guideNode.active = true; this.guideDesc.string = str; } else { this.guideNode.active = false; } }) .start(); } else { this.shouzhiNode.setPosition(posi.x + 50, posi.y); this._focusToNode(registNode); if (str && str != '') { this.guideNode.active = true; this.guideDesc.string = str; } else { this.guideNode.active = false; } } } /** 按当前 maskNode 尺寸绘制圆角挖洞(GRAPHICS_STENCIL + roundRect) */ private _drawRoundHole(width: number, height: number): void { const g = this.graphics; const ui = this.maskNode.node.getComponent(UITransform); if (!g || !ui) return; const ax = ui.anchorX; const ay = ui.anchorY; const x = -width * ax; const y = -height * ay; const r = Math.min( UINewGuideMask.HOLE_CORNER_RADIUS, width * 0.5, height * 0.5, ); g.clear(); g.fillColor = Color.WHITE; g.roundRect(x, y, width, height, r); g.fill(); } private _applyFocusToNode = (): void => { const focus = this._hitNode?.isValid ? this._hitNode : this.registNode; if (!focus?.isValid) { this._closeIfRegistNodeDead(); return; } const ui = focus.getComponent(UITransform); if (!ui) return; const rect = ui.getBoundingBoxToWorld(); MTools.setNodeToTargetPos(this.maskNode.node, focus); const holeW = rect.width + UINewGuideMask.HOLE_PADDING; const holeH = rect.height + UINewGuideMask.HOLE_PADDING; this.maskNode.node.getComponent(UITransform).setContentSize(holeW, holeH); this._drawRoundHole(holeW, holeH); }; _focusToNode(node) { if (!node?.isValid) { this._closeIfRegistNodeDead(); return; } this.unschedule(this._applyFocusToNode); this.scheduleOnce(this._applyFocusToNode, 0); } onInit(): void { this.maskNode.type = Mask.Type.GRAPHICS_STENCIL; this.graphics = (this.maskNode.subComp ?? this.maskNode['_graphics']) as Graphics; this.nodeArray = this.prema[0]; this.labArray = this.prema[1]; this.finishCallBack = this.prema[2]; this.isPlayAnim = this.prema[3] || true; this.blackOpacity = 150;// this.prema[4] || 255; if (this.prema[5] === false) { this.hideSpineshouzhi = this.prema[5]; } else { this.hideSpineshouzhi = true; } if (!this.hideSpineshouzhi) { this.node.getChildByName('dianji').active = false; } // this.maskBlackNode.opacity = this.blackOpacity; tween(this.maskBlackNode).to(0.2, { opacity: this.blackOpacity }).start(); this.bindNode(); if (this.nodeArray.length == 0) { this.close(); } else { const nextNode = this.nodeArray.shift(); this.nextNode(nextNode); } } }