diff --git a/assets/resources/gui/element/barbtn.prefab b/assets/resources/gui/element/barbtn.prefab index 66d3a999..7b0c4172 100644 --- a/assets/resources/gui/element/barbtn.prefab +++ b/assets/resources/gui/element/barbtn.prefab @@ -77,9 +77,6 @@ "_children": [ { "__id__": 3 - }, - { - "__id__": 9 } ], "_active": true, @@ -134,18 +131,22 @@ "_parent": { "__id__": 2 }, - "_children": [], + "_children": [ + { + "__id__": 4 + } + ], "_active": true, "_components": [ { - "__id__": 4 + "__id__": 10 }, { - "__id__": 6 + "__id__": 12 } ], "_prefab": { - "__id__": 8 + "__id__": 14 }, "_lpos": { "__type__": "cc.Vec3", @@ -176,117 +177,31 @@ }, "_id": "" }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 5 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 106, - "height": 96 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "bdIoXh/hdNirkf1KcWeGiB" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 3 - }, - "_enabled": true, - "__prefab": { - "__id__": 7 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@24ff5", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 0, - "_fillType": 0, - "_sizeMode": 1, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "efGswFY3FAVL1sj4VKXGeV" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "86S3E4+VBNF5Q4sYq5WENB", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, { "__type__": "cc.Node", "_name": "Label", "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 3 }, "_children": [], "_active": true, "_components": [ { - "__id__": 10 + "__id__": 5 }, { - "__id__": 12 + "__id__": 7 } ], "_prefab": { - "__id__": 14 + "__id__": 9 }, "_lpos": { "__type__": "cc.Vec3", "x": 0, - "y": -28.541, + "y": -47.568333333333385, "z": 0 }, "_lrot": { @@ -298,9 +213,9 @@ }, "_lscale": { "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 + "x": 1.6666666666666665, + "y": 1.6666666666666665, + "z": 0.9999999999999999 }, "_mobility": 0, "_layer": 1073741824, @@ -318,11 +233,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 9 + "__id__": 4 }, "_enabled": true, "__prefab": { - "__id__": 11 + "__id__": 6 }, "_contentSize": { "__type__": "cc.Size", @@ -346,11 +261,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 9 + "__id__": 4 }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 8 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -421,6 +336,92 @@ "targetOverrides": null, "nestedPrefabInstanceRoots": null }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 11 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 106, + "height": 96 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "bdIoXh/hdNirkf1KcWeGiB" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 3 + }, + "_enabled": true, + "__prefab": { + "__id__": 13 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_spriteFrame": { + "__uuid__": "6165ffc9-a838-4a33-b569-bdbaaab0e6b4@24ff5", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 0, + "_fillType": 0, + "_sizeMode": 1, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "efGswFY3FAVL1sj4VKXGeV" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "86S3E4+VBNF5Q4sYq5WENB", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.UITransform", "_name": "", @@ -1125,6 +1126,7 @@ "__id__": 0 }, "fileId": "cfWG6+EKVJ3IlHCuz5Pa35", + "instance": null, "targetOverrides": null } ] \ No newline at end of file diff --git a/assets/resources/gui/element/mission.prefab b/assets/resources/gui/element/mission.prefab index 4ebdc100..a0856d43 100644 --- a/assets/resources/gui/element/mission.prefab +++ b/assets/resources/gui/element/mission.prefab @@ -23937,7 +23937,6 @@ "refresh_stone_num_node": { "__id__": 209 }, - "ebars_node": null, "_id": "" }, { diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index c0bf7f0b..567166a0 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -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 = 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(); + } + // ======================== 面板底部划出系统 ======================== /** 面板节点数组(顺序:英雄→装备→技能→药品) */