feat(MissionCard): 添加新手按钮引导功能

1.  新增新手引导配置与相关工具方法,实现英雄、装备、技能、药品四个面板按钮的依次引导
2.  修复预制体文件中冗余的节点与组件配置,清理无效的ebars_node引用
3.  新增引导结束标记逻辑,确保仅首次进入战斗阶段时触发引导
4.  在组件销毁与任务结束时自动清理引导调度与动画,防止回调泄漏
This commit is contained in:
panFD
2026-08-06 22:02:55 +08:00
parent c78502c4ff
commit 748f901e01
3 changed files with 275 additions and 106 deletions

View File

@@ -224,6 +224,28 @@ export class MissionCardComp extends CCComp {
/** 覆盖面板显示/隐藏动画时长 */
private readonly cardPanelAnimDuration: number = 0.25;
// ======================== 新手按钮指引 ========================
/** 新手按钮指引 ID写入 finish_guides已占用 0~4此处用 5 */
private static readonly BUTTON_GUIDE_ID: number = 5;
/** 新手按钮指引抖动最大角度(度) */
private static readonly BUTTON_GUIDE_SHAKE_ANGLE: number = 10;
/** 新手按钮指引每步时长(秒) */
private static readonly BUTTON_GUIDE_STEP_DURATION: number = 4.2;
/** 新手按钮指引气泡停留时长(秒) */
private static readonly BUTTON_GUIDE_TIP_STAY: number = 3.4;
/** 指引步骤配置:按钮节点取值器 + 气泡文案 */
private readonly guideSteps: Array<{ getBtn: () => Node | null, text: string }> = [
{ getBtn: () => this.showHeros, text: "这里召唤英雄" },
{ getBtn: () => this.showEquips, text: "这里购买装备" },
{ getBtn: () => this.showSkills, text: "这里购买技能卷轴" },
{ getBtn: () => this.showShop, text: "这里购买药品" },
];
/** 当前指引下标(-1 = 未在指引中) */
private guideStepIndex: number = -1;
/** 气泡原文缓存(指引结束后还原) */
private guideTipTextCache: Map<Node, string> = new Map();
// ======================== 生命周期 ========================
/**
@@ -248,6 +270,9 @@ export class MissionCardComp extends CCComp {
/** 组件销毁时解绑所有事件并清理英雄信息面板 */
onDestroy() {
super.onDestroy();
// 清理新手按钮指引的调度与动画,防止回调泄漏
this.stopButtonGuide();
this.unscheduleAllCallbacks();
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
@@ -345,6 +370,9 @@ export class MissionCardComp extends CCComp {
comp.applyEmpty();
}
}
// 任务结束清理:停止新手按钮指引的调度与动画
this.stopButtonGuide();
this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) {
this.node.active = false;
}
@@ -465,6 +493,11 @@ export class MissionCardComp extends CCComp {
smc.finish_guides.push(4);
oops.gui.remove(UIID.Guide4);
}
// 首次进入战斗阶段:启动按钮逐个指引(仅提醒 1 次)
if (!smc.finish_guides.includes(MissionCardComp.BUTTON_GUIDE_ID)) {
this.playButtonGuide();
}
}
private onShowSmallTip(event: string, args: any) {
@@ -903,6 +936,141 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_chou);
}
// ======================== 新手按钮指引 ========================
/**
* 启动新手按钮指引:依次指引 英雄→装备→技能→药品 四个面板按钮。
* 每步播放 bg/icon 循环抖动 + smalltip 气泡文案,
* 全部播放完成后写入 smc.finish_guides仅提醒 1 次。
*/
private playButtonGuide() {
// 确保无旧指引残留,再从头开始
this.stopButtonGuide();
this.guideStepIndex = 0;
this.playButtonGuideStep();
}
/** 播放当前步指引,并用 scheduleOnce 驱动进入下一步 */
private playButtonGuideStep() {
const step = this.guideSteps[this.guideStepIndex];
if (!step) return;
const btn = step.getBtn();
if (btn && btn.isValid) {
this.playGuideShake(btn, true);
this.showGuideTip(btn, step.text);
}
this.scheduleOnce(() => {
this.stopGuideShake(btn);
this.guideStepIndex++;
if (this.guideStepIndex < this.guideSteps.length) {
this.playButtonGuideStep();
} else {
this.finishButtonGuide();
}
}, MissionCardComp.BUTTON_GUIDE_STEP_DURATION);
}
/** 指引全部完成:还原气泡文案并记录引导完成标记 */
private finishButtonGuide() {
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
if (!smc.finish_guides.includes(MissionCardComp.BUTTON_GUIDE_ID)) {
smc.finish_guides.push(MissionCardComp.BUTTON_GUIDE_ID);
}
mLogger.log(this.debugMode, "MissionCardComp", "button guide finished");
}
/**
* 中止指引(任务结束 / 组件销毁时调用):
* 停止所有抖动动画、还原气泡文案并隐藏气泡。
*/
private stopButtonGuide() {
if (this.guideStepIndex < 0 && this.guideTipTextCache.size === 0) return;
for (const step of this.guideSteps) {
this.stopGuideShake(step.getBtn());
}
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
}
/** 还原指引覆盖过的气泡文案并隐藏气泡 */
private restoreGuideTipTexts() {
for (const [tipNode, text] of this.guideTipTextCache) {
if (tipNode && tipNode.isValid) {
Tween.stopAllByTarget(tipNode);
const label = tipNode.getComponentInChildren(Label);
if (label) label.string = text;
tipNode.active = false;
}
}
this.guideTipTextCache.clear();
}
/**
* 对按钮 bg/icon 子节点播放循环抖动动画。
* 使用 angle 往复 tween不修改 position避免与 Widget 布局冲突。
* @param btn 面板按钮节点(内部含 bg/icon 子节点)
* @param repeat 是否无限循环true = 循环抖动)
*/
private playGuideShake(btn: Node, repeat: boolean) {
const angle = MissionCardComp.BUTTON_GUIDE_SHAKE_ANGLE;
for (const name of ["bg", "icon"]) {
const child = btn.getChildByName(name);
if (!child || !child.isValid) continue;
Tween.stopAllByTarget(child);
child.angle = 0;
let t = tween(child)
.to(0.07, { angle: angle })
.to(0.14, { angle: -angle })
.to(0.07, { angle: 0 })
.delay(0.12);
if (repeat) {
t = t.repeatForever();
}
t.start();
}
}
/** 停止按钮 bg/icon 抖动并复位角度 */
private stopGuideShake(btn: Node | null) {
if (!btn || !btn.isValid) return;
for (const name of ["bg", "icon"]) {
const child = btn.getChildByName(name);
if (!child || !child.isValid) continue;
Tween.stopAllByTarget(child);
child.angle = 0;
}
}
/**
* 在按钮节点上弹出 smalltip 气泡并覆盖文案。
* 首次覆盖前缓存原文,供指引结束后还原。
* @param btn 面板按钮节点(下挂 smalltip 子节点)
* @param text 指引文案
*/
private showGuideTip(btn: Node, text: string) {
const tipNode = btn.getChildByName("smalltip");
if (!tipNode) return;
if (!this.guideTipTextCache.has(tipNode)) {
const label = tipNode.getComponentInChildren(Label);
this.guideTipTextCache.set(tipNode, label ? label.string : "");
}
const label = tipNode.getComponentInChildren(Label);
if (label) label.string = text;
tipNode.active = true;
Tween.stopAllByTarget(tipNode);
tipNode.setScale(0, 0, 1);
tween(tipNode)
.to(0.15, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadOut' })
.to(0.05, { scale: new Vec3(1, 1, 1) })
.delay(MissionCardComp.BUTTON_GUIDE_TIP_STAY)
.to(0.15, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
.call(() => {
if (tipNode && tipNode.isValid) tipNode.active = false;
})
.start();
}
// ======================== 面板底部划出系统 ========================
/** 面板节点数组(顺序:英雄→装备→技能→药品) */