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