refactor(MissionCard): 替换旧版按钮指引为卡牌引导动画,新增选次提示

1. 移除旧的新手按钮指引逻辑,替换为开局三选一卡牌抖动引导动画
2. 新增pickCountLabel节点绑定与选次计数逻辑,显示当前是第几次选择英雄
3. 重构新手引导相关常量与方法,统一卡牌引导的命名与实现
4. 调整任务初始化与结束时的引导清理逻辑
This commit is contained in:
panFD
2026-08-19 23:28:11 +08:00
parent 6c82c632b6
commit 79746409ef
2 changed files with 97 additions and 163 deletions

View File

@@ -15957,7 +15957,7 @@
}, },
{ {
"__type__": "cc.Node", "__type__": "cc.Node",
"_name": "Label-001", "_name": "num",
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": { "_parent": {
@@ -21740,6 +21740,9 @@
"refresh_stone_num_node": { "refresh_stone_num_node": {
"__id__": 83 "__id__": 83
}, },
"pickCountLabel": {
"__id__": 873
},
"_id": "" "_id": ""
}, },
{ {

View File

@@ -176,6 +176,10 @@ export class MissionCardComp extends CCComp {
@property(Node) @property(Node)
refresh_stone_num_node: Node = null! refresh_stone_num_node: Node = null!
/** 选择英雄次数提示 Label显示"第X次" */
@property(Label)
pickCountLabel: Label = null!
// ======================== 运行时状态 ======================== // ======================== 运行时状态 ========================
@@ -198,6 +202,8 @@ export class MissionCardComp extends CCComp {
private static readonly OPENING_FREE_PICK_COUNT = 3; private static readonly OPENING_FREE_PICK_COUNT = 3;
/** 开局免费三选一剩余次数(>0 时招募不扣费,选完 1 张自动弹出下一组) */ /** 开局免费三选一剩余次数(>0 时招募不扣费,选完 1 张自动弹出下一组) */
private openingFreePicks: number = 0; private openingFreePicks: number = 0;
/** 当前是第几次选择英雄(从 1 开始,每次成功选用英雄卡后 +1 */
private currentPickIndex: number = 1;
// ======================== 面板划出状态 ======================== // ======================== 面板划出状态 ========================
@@ -230,25 +236,21 @@ export class MissionCardComp extends CCComp {
/** 覆盖面板显示/隐藏动画时长 */ /** 覆盖面板显示/隐藏动画时长 */
private readonly cardPanelAnimDuration: number = 0.25; private readonly cardPanelAnimDuration: number = 0.25;
// ======================== 新手按钮指引 ======================== // ======================== 新手三选一卡牌引导 ========================
/** 新手按钮指引脉动放大倍率 */ /** 引导动画抖动次数 */
private static readonly BUTTON_GUIDE_PULSE_SCALE: number = 1.15; private static readonly CARDS_GUIDE_SHAKE_TIMES: number = 3;
/** 新手按钮指引每步时长(秒 */ /** 引导动画抖动横向偏移(像素 */
private static readonly BUTTON_GUIDE_STEP_DURATION: number = 4.2; private static readonly CARDS_GUIDE_SHAKE_OFFSET: number = 10;
/** 新手按钮指引气泡停留时长(秒 */ /** 引导动画放大倍率(相对卡牌基准缩放 */
private static readonly BUTTON_GUIDE_TIP_STAY: number = 3.4; private static readonly CARDS_GUIDE_SCALE_PEAK: number = 1.15;
/** 指引步骤配置完成标记键smc.data.tip_done+ 按钮节点取值器 + 气泡文案 */ /** 引导动画单卡总时长(抖 3 下 + 放大回弹 ≈ 0.9s),作为轮流播放间隔 */
private readonly guideSteps: Array<{ key: "hero" | "equip" | "skill" | "shop", getBtn: () => Node | null, text: string }> = [ private static readonly CARDS_GUIDE_PER_CARD_DURATION: number = 0.9;
{ key: "hero", getBtn: () => this.showHeros, text: "这里召唤英雄" }, /** 引导动画启动延迟:需大于 CHeroComp 发牌入场时长0.25sscale 0→1
{ key: "equip", getBtn: () => this.showEquips, text: "这里购买装备" }, * 否则基准缩放会在发牌未完成时被捕获为 0导致放大目标也为 0卡牌缩没 */
{ key: "skill", getBtn: () => this.showSkills, text: "这里购买技能卷轴" }, private static readonly CARDS_GUIDE_START_DELAY: number = 0.4;
{ key: "shop", getBtn: () => this.showShop, text: "这里购买药品" }, /** 引导动画基准缓存(触发时刻捕获的位置与缩放,停止时还原) */
]; private cardsGuideBaseStates: { node: Node; pos: Vec3; scale: Vec3 }[] = [];
/** 当前指引下标(-1 = 未在指引中) */
private guideStepIndex: number = -1;
/** 气泡原文缓存(指引结束后还原) */
private guideTipTextCache: Map<Node, string> = new Map();
// ======================== 生命周期 ======================== // ======================== 生命周期 ========================
@@ -276,8 +278,8 @@ export class MissionCardComp extends CCComp {
/** 组件销毁时解绑所有事件并清理英雄信息面板 */ /** 组件销毁时解绑所有事件并清理英雄信息面板 */
onDestroy() { onDestroy() {
super.onDestroy(); super.onDestroy();
// 清理新手按钮指引的调度与动画,防止回调泄漏 // 清理新手卡牌引导动画,防止回调泄漏
this.stopButtonGuide(); this.stopCardsGuideAnim();
this.unscheduleAllCallbacks(); this.unscheduleAllCallbacks();
if (this.cards_refresh && this.cards_refresh.isValid) { if (this.cards_refresh && this.cards_refresh.isValid) {
this.cards_refresh.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_refresh.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -363,10 +365,17 @@ export class MissionCardComp extends CCComp {
// 暂不启用 herobox 同步显示英雄信息(后续可能恢复),保留代码备查 // 暂不启用 herobox 同步显示英雄信息(后续可能恢复),保留代码备查
// this.refreshHeroBoxSlots(); // this.refreshHeroBoxSlots();
// 重置开局免费三选一次数,随后立即弹出第 1 组三选一 // 重置开局免费三选一次数与选择计数,随后立即弹出第 1 组三选一
this.openingFreePicks = MissionCardComp.OPENING_FREE_PICK_COUNT; this.openingFreePicks = MissionCardComp.OPENING_FREE_PICK_COUNT;
this.currentPickIndex = 1;
this.updatePickCountLabel();
this.showCardsPanel(); this.showCardsPanel();
// 新人第一次进入关卡3 张卡牌轮流播放抖动 + 放大引导动画(仅提醒 1 次)
if (!smc.data.tip_done.hero) {
this.playOpeningCardsGuideAnim();
}
mLogger.log(this.debugMode, "MissionCardComp", "mission start"); mLogger.log(this.debugMode, "MissionCardComp", "mission start");
} }
@@ -381,8 +390,8 @@ export class MissionCardComp extends CCComp {
comp.applyEmpty(); comp.applyEmpty();
} }
} }
// 任务结束清理:停止新手按钮指引的调度与动画 // 任务结束清理:停止新手卡牌引导动画
this.stopButtonGuide(); this.stopCardsGuideAnim();
this.unscheduleAllCallbacks(); this.unscheduleAllCallbacks();
if (this.node && this.node.isValid) { if (this.node && this.node.isValid) {
this.node.active = false; this.node.active = false;
@@ -498,13 +507,6 @@ export class MissionCardComp extends CCComp {
/** 战斗开始:保留卡牌面板,允许玩家在战斗阶段继续抽卡和召唤英雄 */ /** 战斗开始:保留卡牌面板,允许玩家在战斗阶段继续抽卡和召唤英雄 */
private onFightStart() { private onFightStart() {
this.enterBattlePhase(); this.enterBattlePhase();
// 首次进入战斗阶段:启动按钮逐个指引(仅提醒 1 次,全部完成则自动跳过)
const tips = smc.data.tip_done;
if (!tips.hero || !tips.equip || !tips.skill || !tips.shop) {
this.playButtonGuide();
}
} }
private onShowSmallTip(event: string, args: any) { private onShowSmallTip(event: string, args: any) {
@@ -893,6 +895,9 @@ export class MissionCardComp extends CCComp {
// 抽 1 张后关闭英雄卡池面板 // 抽 1 张后关闭英雄卡池面板
this.hideCardsPanel(); this.hideCardsPanel();
} }
// 本张已选完:选择计数 +1供下一组三选一显示"第X次"
this.currentPickIndex++;
this.updatePickCountLabel();
// 英雄数量变化后同步 showHeros 可见性(满员/次数耗尽即隐藏) // 英雄数量变化后同步 showHeros 可见性(满员/次数耗尽即隐藏)
this.updateShowHerosVisibility(); this.updateShowHerosVisibility();
mLogger.log(this.debugMode, "MissionCardComp", "refresh hero pool after buy", { mLogger.log(this.debugMode, "MissionCardComp", "refresh hero pool after buy", {
@@ -951,150 +956,69 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_refresh); this.playButtonResetAnim(this.cards_refresh);
} }
// ======================== 新手按钮指引 ======================== // ======================== 新手三选一卡牌引导(动画) ========================
/** /**
* 启动新手按钮指引:依次指引 英雄→装备→技能→药品 四个面板按钮。 * 新人第一次进入关卡的开局三选一引导:
* 每步播放 bg/icon 循环缩放脉动 + smalltip 气泡文案, * 3 张卡牌从左到右轮流播放"抖动 3 下 + 放大 1 下"动画,仅提醒 1 次。
* 已完成的步骤smc.data.tip_done 对应键为 true自动跳过仅提醒 1 次 * 替代旧版按钮脉动 + 气泡指引showHeros 按钮机制已移除,无气泡提示)
*/ */
private playButtonGuide() { private playOpeningCardsGuideAnim() {
// 确保无旧指引残留,再从头开始 // 播放即标记完成,仅新人第一次进入关卡时提醒
this.stopButtonGuide(); smc.data.tip_done.hero = true;
this.guideStepIndex = -1; this.cardsGuideBaseStates = [];
this.nextButtonGuideStep(); // 延迟启动等发牌入场动画scale 0→10.25s)播完,基准缩放才是稳定值
}
/** 推进到下一个未完成指引的步骤;全部完成则结束 */
private nextButtonGuideStep() {
this.guideStepIndex++;
// 跳过已完成的步骤
while (this.guideStepIndex < this.guideSteps.length
&& smc.data.tip_done[this.guideSteps[this.guideStepIndex].key]) {
this.guideStepIndex++;
}
if (this.guideStepIndex >= this.guideSteps.length) {
this.finishButtonGuide();
return;
}
this.playButtonGuideStep();
}
/** 播放当前步指引,并用 scheduleOnce 驱动进入下一步 */
private playButtonGuideStep() {
const step = this.guideSteps[this.guideStepIndex];
if (!step) return;
const btn = step.getBtn();
if (btn && btn.isValid) {
this.playGuidePulse(btn, true);
this.showGuideTip(btn, step.text);
}
this.scheduleOnce(() => { this.scheduleOnce(() => {
this.stopGuidePulse(btn); for (let i = 0; i < this.cardComps.length; i++) {
// 本步提醒完成:记录完成标记(即使中止也只标记已播完的步骤) const node = this.cardComps[i]?.node;
smc.data.tip_done[step.key] = true; if (!node || !node.isValid) continue;
this.nextButtonGuideStep(); this.scheduleOnce(() => this.playOneCardGuideAnim(node), i * MissionCardComp.CARDS_GUIDE_PER_CARD_DURATION);
}, MissionCardComp.BUTTON_GUIDE_STEP_DURATION);
}
/** 指引全部完成:还原气泡文案 */
private finishButtonGuide() {
this.restoreGuideTipTexts();
this.guideStepIndex = -1;
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.stopGuidePulse(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;
} }
} }, MissionCardComp.CARDS_GUIDE_START_DELAY);
this.guideTipTextCache.clear();
} }
/** /** 单张卡牌引导动画:左右抖动 3 下后放大 1 下回弹 */
* 对按钮 bg/icon 子节点播放循环缩放脉动动画。 private playOneCardGuideAnim(node: Node) {
* 使用 scale 往复 tween不修改 position避免与 Widget 布局冲突。 if (!node || !node.isValid) return;
* @param btn 面板按钮节点(内部含 bg/icon 子节点) // 触发时刻才捕获基准(发牌动画已结束),避免提前缓存到动画中的中间态
* @param repeat 是否无限循环true = 循环脉动) let base = this.cardsGuideBaseStates.find(s => s.node === node);
*/ if (!base) {
private playGuidePulse(btn: Node, repeat: boolean) { base = { node, pos: node.position.clone(), scale: node.scale.clone() };
const pulseScale = MissionCardComp.BUTTON_GUIDE_PULSE_SCALE; this.cardsGuideBaseStates.push(base);
for (const name of ["bg", "icon"]) {
const child = btn.getChildByName(name);
if (!child || !child.isValid) continue;
Tween.stopAllByTarget(child);
child.setScale(1, 1, 1);
// 单轮脉动约 0.6s,持续闪动与气泡停留节奏一致
const pulseTween = tween(child)
.to(0.3, { scale: new Vec3(pulseScale, pulseScale, 1) }, { easing: 'sineInOut' })
.to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: 'sineInOut' });
if (repeat) {
tween(child).repeatForever(pulseTween).start();
} else {
pulseTween.start();
}
} }
} const baseX = base.pos.x;
const baseY = base.pos.y;
const baseScale = base.scale;
const offset = MissionCardComp.CARDS_GUIDE_SHAKE_OFFSET;
const peakX = baseScale.x * MissionCardComp.CARDS_GUIDE_SCALE_PEAK;
const peakY = baseScale.y * MissionCardComp.CARDS_GUIDE_SCALE_PEAK;
/** 停止按钮 bg/icon 缩放脉动并复位缩放 */ Tween.stopAllByTarget(node);
private stopGuidePulse(btn: Node | null) { // 抖动 1 下 = 左偏 → 右偏 → 回中(结束后必回到基准位)
if (!btn || !btn.isValid) return; const shakeOnce = tween(node)
for (const name of ["bg", "icon"]) { .to(0.05, { position: new Vec3(baseX - offset, baseY, 0) })
const child = btn.getChildByName(name); .to(0.1, { position: new Vec3(baseX + offset, baseY, 0) })
if (!child || !child.isValid) continue; .to(0.05, { position: new Vec3(baseX, baseY, 0) });
Tween.stopAllByTarget(child); tween(node)
child.setScale(1, 1, 1); .repeat(MissionCardComp.CARDS_GUIDE_SHAKE_TIMES, shakeOnce)
} // 放大 1 下回弹(以触发时刻的基准缩放为锚,回到原值而非 0
} .to(0.15, { scale: new Vec3(peakX, peakY, baseScale.z) }, { easing: 'quadOut' })
.to(0.15, { scale: new Vec3(baseScale.x, baseScale.y, baseScale.z) }, { easing: 'quadIn' })
/**
* 在按钮节点上弹出 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(); .start();
} }
/** 停止卡牌引导动画并还原位置与缩放(任务结束 / 组件销毁时调用) */
private stopCardsGuideAnim() {
for (const state of this.cardsGuideBaseStates) {
if (state.node && state.node.isValid) {
Tween.stopAllByTarget(state.node);
state.node.setPosition(state.pos);
state.node.setScale(state.scale);
}
}
this.cardsGuideBaseStates = [];
}
// ======================== 面板底部划出系统 ======================== // ======================== 面板底部划出系统 ========================
/** 面板节点数组(顺序:英雄→装备→技能→药品) */ /** 面板节点数组(顺序:英雄→装备→技能→药品) */
@@ -2124,6 +2048,13 @@ export class MissionCardComp extends CCComp {
this.updateDrawCostUI(); this.updateDrawCostUI();
} }
/** 刷新选择英雄次数提示 Label"第X次"),未绑定或节点失效时静默跳过 */
private updatePickCountLabel() {
if (this.pickCountLabel && this.pickCountLabel.isValid) {
this.pickCountLabel.string = `${this.currentPickIndex}`;
}
}
private playCoinChangeAnim(isIncrease: boolean) { private playCoinChangeAnim(isIncrease: boolean) {
if (!this.coins_node || !this.coins_node.isValid) return; if (!this.coins_node || !this.coins_node.isValid) return;
const icon = this.coins_node.getChildByName("icon"); const icon = this.coins_node.getChildByName("icon");