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.
325 lines
10 KiB
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|