feat(MissionCard): 添加新手按钮引导功能
1. 新增新手引导配置与相关工具方法,实现英雄、装备、技能、药品四个面板按钮的依次引导 2. 修复预制体文件中冗余的节点与组件配置,清理无效的ebars_node引用 3. 新增引导结束标记逻辑,确保仅首次进入战斗阶段时触发引导 4. 在组件销毁与任务结束时自动清理引导调度与动画,防止回调泄漏
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -23937,7 +23937,6 @@
|
||||
"refresh_stone_num_node": {
|
||||
"__id__": 209
|
||||
},
|
||||
"ebars_node": null,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// ======================== 面板底部划出系统 ========================
|
||||
|
||||
/** 面板节点数组(顺序:英雄→装备→技能→药品) */
|
||||
|
||||
Reference in New Issue
Block a user