You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/ui/NewGuideMask/UINewGuideMask.ts

325 lines
10 KiB

3 weeks ago
//*********************
// 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);
}
}
}