refactor(MissionCardComp): 重构面板显示逻辑,改用缩放控制显隐

1. 重构英雄卡牌等覆盖面板的显示逻辑,从位置滑动改为缩放动画
2. 移除默认激活面板的代码,改为按需显示
3. 优化英雄槽位的显示逻辑,默认隐藏空槽位
4. 调整UI预制体的布局参数与文本内容
This commit is contained in:
panFD
2026-08-05 23:55:19 +08:00
parent 772d9af131
commit 2e09455f3e
9 changed files with 7984 additions and 22977 deletions

View File

@@ -183,10 +183,12 @@ export class MissionCardComp extends CCComp {
/** 当前激活的面板索引0=英雄, 1=装备, 2=技能, 3=药品 */
private currentPanelIndex: number = 0;
/** 面板显示位置(英雄面板基准位置,运行时从场景读取) */
private panelShowPos: Vec3 = new Vec3();
/** 覆盖面板(装备/技能/药品)的隐藏 Y 坐标(屏幕外底部,从场景初始位置读取) */
private panelHideYs: number[] = [];
/** 覆盖面板(卡牌/装备/技能/药品)显示位置:面板铺满屏幕,显示时固定位于 (0,0) */
private readonly panelShowPos: Vec3 = new Vec3(0, 0, 0);
/** 覆盖面板显示态缩放 (1,1) */
private readonly panelShowScale: Vec3 = new Vec3(1, 1, 1);
/** 覆盖面板隐藏态缩放 (0,0) */
private readonly panelHideScale: Vec3 = new Vec3(0, 0, 1);
/** 是否已初始化面板布局 */
private hasInitPanelLayout: boolean = false;
/** 是否已缓存卡牌面板基准缩放 */
@@ -197,9 +199,7 @@ export class MissionCardComp extends CCComp {
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌面板隐藏 Y 坐标(场景初始位置,屏幕外底部,由 initPanelLayout 缓存) */
private cardsPanHideY: number = 0;
/** 卡牌面板显示/隐藏动画时长 */
/** 覆盖面板显示/隐藏动画时长 */
private readonly cardPanelAnimDuration: number = 0.25;
// ======================== 生命周期 ========================
@@ -215,6 +215,7 @@ export class MissionCardComp extends CCComp {
onLoad() {
this.bindEvents();
this.cacheCardComps();
this.hideAllHeroBoxSlots();
this.initCardsPanelPos();
this.initPanelLayout();
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
@@ -557,11 +558,6 @@ export class MissionCardComp extends CCComp {
// 三等分水平布局
this.layoutSlotsHorizontal(this.itemCardComps);
// 默认显示商店面板
if (this.shopPanNode) {
this.shopPanNode.active = true;
}
mLogger.log(this.debugMode, "MissionCardComp", "populate shop items", {
count: items.length,
slotCount: this.itemCardComps.length
@@ -800,7 +796,7 @@ export class MissionCardComp extends CCComp {
if (heroIdx >= 0) {
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
// 召唤成功后收起英雄卡池面板
// 抽 1 张后关闭英雄卡池面板
this.hideCardsPanel();
mLogger.log(this.debugMode, "MissionCardComp", "refresh hero pool after buy", {
triggerSlot: heroIdx
@@ -811,6 +807,8 @@ export class MissionCardComp extends CCComp {
const equipIdx = this.equipCardComps.findIndex(c => c === source);
if (equipIdx >= 0) {
this.populateEquipments();
// 抽 1 张后关闭装备面板
this.hideOverlayPanel(1);
mLogger.log(this.debugMode, "MissionCardComp", "refresh equip pool after buy", {
triggerSlot: equipIdx
});
@@ -820,6 +818,8 @@ export class MissionCardComp extends CCComp {
const skillIdx = this.skillCardComps.findIndex(c => c === source);
if (skillIdx >= 0) {
this.populateSkillCards();
// 抽 1 张后关闭技能面板
this.hideOverlayPanel(2);
mLogger.log(this.debugMode, "MissionCardComp", "refresh skill pool after buy", {
triggerSlot: skillIdx
});
@@ -829,6 +829,8 @@ export class MissionCardComp extends CCComp {
const itemIdx = this.itemCardComps.findIndex(c => c === source);
if (itemIdx >= 0) {
this.populateShopItems();
// 抽 1 张后关闭药品面板
this.hideOverlayPanel(3);
mLogger.log(this.debugMode, "MissionCardComp", "refresh item pool after buy", {
triggerSlot: itemIdx
});
@@ -866,50 +868,75 @@ export class MissionCardComp extends CCComp {
/**
* 初始化面板布局:
* 1. 英雄面板位置为基准显示位置(英雄面板固定不动)
* 2. 缓存各覆盖面板(装备/技能/药品)的初始位置作为隐藏位置(屏幕外底部)。
* 3. 将覆盖面板归位到隐藏位置初始显示英雄面板index=0
* 1. 英雄面板index=0为常驻基准面板始终显示
* 2. 覆盖面板(卡牌/装备/技能/药品)铺满屏幕、位置固定 (0,0)
* 通过缩放 0↔1 控制显示/隐藏,初始缩放置 0 并停用
*/
private initPanelLayout() {
if (this.hasInitPanelLayout) return;
const panels = this.getPanelNodes();
this.panelHideYs = [];
const heroNode = panels[0];
if (heroNode && heroNode.isValid) {
heroNode.active = true;
this.panelShowPos.set(heroNode.getPosition());
}
// 覆盖面板缓存场景初始位置屏幕外底部作为隐藏位置X 对齐显示位置
// 覆盖面板从 index 1 开始:位置由代码接管,禁用 Widget 避免其在 lateUpdate 对齐覆盖 position
for (let i = 1; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) {
this.panelHideYs.push(this.panelShowPos.y);
continue;
}
node.active = true;
const pos = node.getPosition();
this.panelHideYs.push(pos.y);
// index 0 为英雄面板,隐藏列表从 index 1 起
node.setPosition(this.panelShowPos.x, pos.y, pos.z);
this.initOverlayPanel(panels[i]);
}
// 卡牌面板与其他覆盖面板同级缓存场景初始位置屏幕外底部作为隐藏位置X 对齐显示位置
if (this.cardPanNode && this.cardPanNode.isValid) {
const pos = this.cardPanNode.getPosition();
this.cardsPanHideY = pos.y;
this.cardPanNode.setPosition(this.panelShowPos.x, pos.y, pos.z);
}
// 卡牌面板与其他覆盖面板同级,同上处理
this.initOverlayPanel(this.cardPanNode);
this.hasInitPanelLayout = true;
this.updatePanelButtonStates();
}
/** 单个覆盖面板初始化:禁用 Widget、定位到显示位置、缩放置 0 并隐藏 */
private initOverlayPanel(node: Node | null) {
if (!node || !node.isValid) return;
const widget = node.getComponent(Widget);
if (widget) widget.enabled = false;
node.setPosition(this.panelShowPos);
node.setScale(this.panelHideScale);
node.active = false;
}
/**
* 显示覆盖面板:缩放从 0 动画放大到 1。
* @param node 覆盖面板节点(卡牌/装备/技能/药品)
*/
private showOverlayPanel(node: Node | null) {
if (!node || !node.isValid) return;
node.setPosition(this.panelShowPos);
node.setScale(this.panelHideScale);
node.active = true;
Tween.stopAllByTarget(node);
tween(node)
.to(this.cardPanelAnimDuration, { scale: this.panelShowScale }, { easing: 'quadOut' })
.start();
}
/**
* 隐藏覆盖面板:缩放从当前值动画缩小到 0结束后停用节点。
* @param node 覆盖面板节点(卡牌/装备/技能/药品)
*/
private hideOverlayPanelNode(node: Node | null) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
tween(node)
.to(this.cardPanelAnimDuration, { scale: this.panelHideScale }, { easing: 'quadIn' })
.call(() => {
if (node.isValid) node.active = false;
})
.start();
}
/**
* 切换到指定面板:
* 英雄面板index=0固定不动
* 选中的覆盖面板从底部划入到显示位置,其余覆盖面板划出回底部隐藏位置
* 英雄面板index=0为常驻基准面板
* 选中的覆盖面板缩放放大到 1 显示,其余覆盖面板缩放缩小到 0 隐藏
* @param index 0=英雄, 1=装备, 2=技能, 3=药品
*/
private slideToPanel(index: number) {
@@ -917,17 +944,16 @@ export class MissionCardComp extends CCComp {
if (index < 0 || index >= panels.length) return;
this.currentPanelIndex = index;
const duration = 0.25;
// 覆盖面板从 index 1 开始index 0 英雄面板固定不动)
// 覆盖面板从 index 1 开始index 0 英雄面板常驻不动)
for (let i = 1; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) continue;
const targetY = i === index ? this.panelShowPos.y : (this.panelHideYs[i - 1] ?? node.getPosition().y);
Tween.stopAllByTarget(node);
tween(node)
.to(duration, { position: new Vec3(this.panelShowPos.x, targetY, 0) }, { easing: 'quadOut' })
.start();
if (i === index) {
this.showOverlayPanel(node);
} else {
this.hideOverlayPanelNode(node);
}
}
this.updatePanelButtonStates();
@@ -975,8 +1001,8 @@ export class MissionCardComp extends CCComp {
/**
* 同步英雄信息盒槽位与场上英雄:
* 1. 刷新 herosBoxNode 下 HeroBoxComp 槽位缓存(编辑器已预放 3 个)。
* 2. 查询当前存活英雄列表,按序绑定到槽位。
* 3. 多余槽位显示空英雄信息
* 2. 查询当前存活英雄列表,按序绑定到槽位bindHero 内部激活节点)
* 3. 多余槽位清空数据并隐藏(默认全部隐藏,召唤一个激活一个)
*
* Why: 英雄召唤 / 死亡 / 卖出 / 升级后调用,保证面板与场上一一对应。
*/
@@ -1003,11 +1029,14 @@ export class MissionCardComp extends CCComp {
if (actor) {
comp.bindHero(actor.eid, actor.model);
} else {
// 空槽位默认隐藏:召唤一个英雄才激活一个槽位
comp.applyEmpty();
comp.node.active = false;
}
}
this.resizeListBox(this.herosBoxNode, Math.max(actors.length, 1));
// 列表高度只按可见槽位(存活英雄数)计算,空槽位已隐藏不再占位
this.resizeListBox(this.herosBoxNode, actors.length);
}
/**
@@ -1022,39 +1051,51 @@ export class MissionCardComp extends CCComp {
.filter((comp): comp is HeroBoxComp => !!comp);
}
/**
* 显示卡牌面板从底部向上滑入到面板显示位置panelShowPos
*/
private onShowCardsClick() {
oops.audio.playEffect("music/button");
if (!this.cardPanNode || !this.cardPanNode.isValid) return;
this.cardPanNode.active = true;
Tween.stopAllByTarget(this.cardPanNode);
tween(this.cardPanNode)
.to(this.cardPanelAnimDuration, { position: new Vec3(this.panelShowPos.x, this.panelShowPos.y, 0) }, { easing: 'quadOut' })
.start();
/** 英雄信息盒槽位默认全部隐藏,召唤一个英雄再激活一个 */
private hideAllHeroBoxSlots() {
this.ensureHeroBoxSlots();
for (const comp of this.heroBoxComps) {
if (comp && comp.node && comp.node.isValid) {
comp.node.active = false;
}
}
}
/**
* 隐藏卡牌面板:向下滑出到其隐藏位置cardsPanHideY屏幕外底部
* 显示卡牌面板:缩放从 0 动画放大到 1
*/
private onShowCardsClick() {
oops.audio.playEffect("music/button");
this.showOverlayPanel(this.cardPanNode);
}
/**
* 隐藏卡牌面板:缩放动画缩小到 0
*/
private onCloseCardsClick() {
oops.audio.playEffect("music/button");
this.hideCardsPanel();
}
/** 隐藏卡牌面板:向下滑出到隐藏位置(不含按钮音效,供系统逻辑复用) */
/** 隐藏卡牌面板:缩放缩小到 0(不含按钮音效,供系统逻辑复用) */
private hideCardsPanel() {
if (!this.cardPanNode || !this.cardPanNode.isValid) return;
Tween.stopAllByTarget(this.cardPanNode);
tween(this.cardPanNode)
.to(this.cardPanelAnimDuration, { position: new Vec3(this.panelShowPos.x, this.cardsPanHideY, 0) }, { easing: 'quadIn' })
.call(() => {
if (this.cardPanNode && this.cardPanNode.isValid) {
this.cardPanNode.active = false;
}
})
.start();
this.hideOverlayPanelNode(this.cardPanNode);
}
/**
* 隐藏指定覆盖面板(缩放缩小到 0 并停用):
* 用于抽 1 张卡成功后自动收起对应卡池面板,露出下层英雄面板。
* @param index 面板索引1=装备, 2=技能, 3=药品0 为英雄基准面板,不可隐藏)
*/
private hideOverlayPanel(index: number) {
const panels = this.getPanelNodes();
if (index < 1 || index >= panels.length) return;
const node = panels[index];
if (!node || !node.isValid) return;
this.hideOverlayPanelNode(node);
// 面板收起后 tab 高亮回归英雄面板
this.currentPanelIndex = 0;
this.updatePanelButtonStates();
}
// ======================== 装备商店面板 ========================
@@ -1118,11 +1159,6 @@ export class MissionCardComp extends CCComp {
// 三等分水平布局
this.layoutSlotsHorizontal(this.equipCardComps);
// 默认显示装备面板
if (this.equipsPanNode) {
this.equipsPanNode.active = true;
}
mLogger.log(this.debugMode, "MissionCardComp", "populate equipments", {
count: equips.length,
slotCount: this.equipCardComps.length
@@ -1196,11 +1232,6 @@ export class MissionCardComp extends CCComp {
// 三等分水平布局
this.layoutSlotsHorizontal(this.skillCardComps);
// 默认显示技能面板
if (this.skillPanNode) {
this.skillPanNode.active = true;
}
mLogger.log(this.debugMode, "MissionCardComp", "populate skill cards", {
count: skills.length,
slotCount: this.skillCardComps.length
@@ -1472,10 +1503,9 @@ export class MissionCardComp extends CCComp {
}
// 刷新所有刷新按钮的费用显示(含刷新石/金币切换与置灰状态)
this.updateDrawCostUI();
// 显示卡牌面板(默认进入准备阶段时展开,位置与其他面板保持一致
// 显示卡牌面板(默认进入准备阶段时展开,缩放放大到 1
if (this.cardPanNode && this.cardPanNode.isValid) {
this.cardPanNode.active = true;
this.cardPanNode.setPosition(this.panelShowPos.x, this.panelShowPos.y, 0);
this.showOverlayPanel(this.cardPanNode);
mLogger.log(this.debugMode, "MissionCardComp", "enterPreparePhase cardPanNode", {
active: this.cardPanNode.active,
position: this.cardPanNode.position,