diff --git a/assets/resources/gui/element/mission.prefab b/assets/resources/gui/element/mission.prefab index 4bf88d10..f682fb00 100644 --- a/assets/resources/gui/element/mission.prefab +++ b/assets/resources/gui/element/mission.prefab @@ -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": "" }, { diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index db142773..830cd8e4 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -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 = 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");