feat(map): 重构任务面板为轮播切换模式

重构任务面板的显示逻辑,将原有单面板独立显隐改为水平轮播切换:
1. 新增面板轮播状态管理相关字段与方法
2. 替换原有单独显隐逻辑为统一滑动切换逻辑
3. 调整技能面板初始显示规则,改为始终激活仅通过滑动控制可见性
4. 统一所有面板的打开/关闭操作,优化交互一致性
This commit is contained in:
panFD
2026-07-22 22:43:26 +08:00
parent ce5fda3586
commit 516b937531
2 changed files with 20904 additions and 20069 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -183,6 +183,19 @@ export class MissionCardComp extends CCComp {
private purchasedEquipUuids: Set<number> = new Set();
/** 已购买商品的 UUID 集合(跨波次保持,防止重复购买) */
private purchasedItemUuids: Set<number> = new Set();
// ======================== 面板轮播状态 ========================
/** 面板滑动间距(单面板宽度,运行时由 initPanelLayout 推导) */
private panelSlideWidth: number = 720;
/** 当前激活的面板索引0=英雄, 1=装备, 2=技能, 3=药品 */
private currentPanelIndex: number = 0;
/** 各面板基准 Y 坐标缓存 */
private panelBaseYs: number[] = [];
/** 是否已初始化面板布局 */
private hasInitPanelLayout: boolean = false;
/** 面板是否处于隐藏状态(被 close 按钮关闭) */
private panelsHidden: boolean = false;
/** 是否已缓存卡牌面板基准缩放 */
private hasCachedCardsBaseScale: boolean = false;
/** 卡牌面板基准缩放(从场景读取) */
@@ -207,12 +220,14 @@ export class MissionCardComp extends CCComp {
* 2. 缓存 3 个 CardComp 子控制器引用。
* 3. 计算并设置槽位水平布局。
* 4. 初始化卡牌面板缩放参数。
* 5. 初始化面板轮播布局。
*/
onLoad() {
this.bindEvents();
this.cacheCardComps();
this.layoutCardSlots();
this.initCardsPanelPos();
this.initPanelLayout();
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
slots: this.cardComps.length,
});
@@ -504,16 +519,12 @@ export class MissionCardComp extends CCComp {
* 首次进入准备阶段时自动抽取一次技能卡:
* - 仅在任务开始时调用一次。
* - 后续抽卡只能通过技能刷新按钮触发。
* - 游戏刚开始时不激活技能面板(等召唤第一个英雄后才开放)
* - 面板轮播模式下技能面板始终 active仅通过滑动切换可见性
*/
private initSkillCardsOnce() {
if (this.skillCardComps.length === 0) {
this.cacheCardComps();
}
// 游戏刚开始时不显示技能卡牌面板(等召唤英雄后才开放)
if (this.skill_card_node) {
this.skill_card_node.active = false;
}
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
this.playSkillCardEnterAnim();
@@ -525,66 +536,31 @@ export class MissionCardComp extends CCComp {
}
/**
* 技能卡池显隐切换按钮回调:
* - 仅切换 skill_card_node 的 active 状态,不执行抽卡。
* - 后续刷新需通过技能刷新按钮skill_refresh触发。
* - 打开前关闭其他面板,确保同时只显示一个。
* 技能面板按钮回调:滑动到技能面板(index=2)。
*/
private onShowSkillsClick() {
if (!this.skill_card_node || !this.skill_card_node.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.skill_card_node.active;
if (visible) {
this.closeAllPanels();
}
this.skill_card_node.active = visible;
const activeChild = this.showSkills?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
this.slideToPanel(2);
}
/** 关闭技能卡池按钮回调 */
/** 关闭技能面板按钮回调:隐藏所有面板 */
private onCloseSkillsClick() {
if (!this.skill_card_node || !this.skill_card_node.isValid) return;
oops.audio.playEffect("music/button");
this.skill_card_node.active = false;
const activeChild = this.showSkills?.getChildByName("active");
if (activeChild) {
activeChild.active = false;
}
this.hideAllPanels();
}
// ======================== 商店面板 ========================
/**
* 商店显隐切换按钮回调:
* 切换 shopPanNode 的 active 状态。
* 打开前关闭其他面板,确保同时只显示一个。
* 商店面板按钮回调:滑动到药品商店面板(index=3)。
*/
private onShowShopClick() {
if (!this.shopPanNode || !this.shopPanNode.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.shopPanNode.active;
if (visible) {
this.closeAllPanels();
}
this.shopPanNode.active = visible;
const activeChild = this.showShop?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
this.slideToPanel(3);
}
/** 关闭商店按钮回调 */
/** 关闭商店面板按钮回调:隐藏所有面板 */
private onCloseShopClick() {
if (!this.shopPanNode || !this.shopPanNode.isValid) return;
oops.audio.playEffect("music/button");
this.shopPanNode.active = false;
const activeChild = this.showShop?.getChildByName("active");
if (activeChild) {
activeChild.active = false;
}
this.hideAllPanels();
}
/**
@@ -872,129 +848,162 @@ export class MissionCardComp extends CCComp {
this.playButtonResetAnim(this.cards_chou);
}
// ======================== 面板轮播系统 ========================
/** 面板节点数组(顺序:英雄→装备→技能→药品) */
private getPanelNodes(): Node[] {
return [this.cards_node, this.equipsPanNode, this.skill_card_node, this.shopPanNode];
}
/** 面板显示按钮数组(同上排序) */
private getPanelShowButtons(): Node[] {
return [this.showHeros, this.showEquips, this.showSkills, this.showShop];
}
/**
* 初始化面板轮播布局:
* 1. 缓存各面板基准 Y 坐标。
* 2. 推导面板滑动间距(取场景设计分辨率宽度或默认 720
* 3. 将所有面板排列为水平队列初始显示英雄面板index=0
*/
private initPanelLayout() {
if (this.hasInitPanelLayout) return;
const panels = this.getPanelNodes();
this.panelBaseYs = [];
// 以第一个有效面板的位置为基准,推导滑动间距
let refX = 0;
for (const p of panels) {
if (p && p.isValid) {
refX = p.getPosition().x;
break;
}
}
this.panelSlideWidth = Math.abs(refX) * 2 || 720;
if (this.panelSlideWidth < 400) this.panelSlideWidth = 720;
for (let i = 0; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) {
this.panelBaseYs.push(0);
continue;
}
node.active = true;
const pos = node.getPosition();
this.panelBaseYs.push(pos.y);
// 初始位置:当前面板(英雄)居中,其余按序排列
const targetX = (i - this.currentPanelIndex) * this.panelSlideWidth;
node.setPosition(targetX, pos.y, pos.z);
}
this.hasInitPanelLayout = true;
this.updatePanelButtonStates();
}
/**
* 滑动到指定面板。
* @param index 0=英雄, 1=装备, 2=技能, 3=药品
*/
private slideToPanel(index: number) {
const panels = this.getPanelNodes();
if (index < 0 || index >= panels.length) return;
// 如果面板被隐藏close先恢复显示
if (this.panelsHidden) {
this.panelsHidden = false;
for (const p of panels) {
if (p && p.isValid) p.active = true;
}
}
this.currentPanelIndex = index;
const duration = 0.25;
for (let i = 0; i < panels.length; i++) {
const node = panels[i];
if (!node || !node.isValid) continue;
const targetX = (i - index) * this.panelSlideWidth;
const baseY = this.panelBaseYs[i] ?? node.getPosition().y;
Tween.stopAllByTarget(node);
tween(node)
.to(duration, { position: new Vec3(targetX, baseY, 0) }, { easing: 'quadOut' })
.start();
}
this.updatePanelButtonStates();
}
/**
* 更新面板 tab 按钮的 active 子节点状态。
* 当前激活面板对应的按钮 active 高亮,其余关闭。
*/
private updatePanelButtonStates() {
const buttons = this.getPanelShowButtons();
for (let i = 0; i < buttons.length; i++) {
const btn = buttons[i];
if (!btn || !btn.isValid) continue;
const activeChild = btn.getChildByName("active");
if (activeChild) {
activeChild.active = (i === this.currentPanelIndex && !this.panelsHidden);
}
}
}
/**
* 隐藏所有面板(通过 close 按钮关闭)。
*/
private hideAllPanels() {
oops.audio.playEffect("music/button");
this.panelsHidden = true;
const panels = this.getPanelNodes();
for (const p of panels) {
if (p && p.isValid) p.active = false;
}
this.updatePanelButtonStates();
}
/**
* 更新面板按钮的可交互状态。
* 游戏刚开始时(未召唤英雄),只有英雄面板可打开;
* 召唤第一个英雄后,其他面板才允许点击。
* 通过 nock_node阻挡层控制按钮是否可点击,而非隐藏按钮本身
* 通过 nock_node阻挡层控制按钮是否可点击。
*/
private updatePanelButtonsInteractable() {
const interactable = this.hasCalledHero;
// nock_node 是覆盖在其他按钮上的阻挡层:
// 未召唤英雄时激活(阻挡点击),召唤英雄后隐藏(允许点击)
if (this.nock_node && this.nock_node.isValid) {
this.nock_node.active = !interactable;
}
}
/**
* 关闭所有面板(英雄卡池、技能卡池、装备商店、商品商店)
* 用于打开新面板前确保同时只显示一个面板。
* 同时关闭所有按钮的 active 子节点状态。
*/
private closeAllPanels() {
// 关闭英雄卡池
if (this.cards_node && this.cards_node.isValid) {
this.cards_node.active = false;
}
if (this.showHeros && this.showHeros.isValid) {
const activeChild = this.showHeros.getChildByName("active");
if (activeChild) activeChild.active = false;
}
// 关闭技能卡池
if (this.skill_card_node && this.skill_card_node.isValid) {
this.skill_card_node.active = false;
}
if (this.showSkills && this.showSkills.isValid) {
const activeChild = this.showSkills.getChildByName("active");
if (activeChild) activeChild.active = false;
}
// 关闭装备商店
if (this.equipsPanNode && this.equipsPanNode.isValid) {
this.equipsPanNode.active = false;
}
if (this.showEquips && this.showEquips.isValid) {
const activeChild = this.showEquips.getChildByName("active");
if (activeChild) activeChild.active = false;
}
// 关闭商品商店
if (this.shopPanNode && this.shopPanNode.isValid) {
this.shopPanNode.active = false;
}
if (this.showShop && this.showShop.isValid) {
const activeChild = this.showShop.getChildByName("active");
if (activeChild) activeChild.active = false;
}
}
/**
* 英雄卡池显隐切换按钮回调:
* - 切换 cards_node 的 active 状态。
* - 显示时激活 showHeros 下的 "active" 子节点,隐藏时关闭该子节点。
* - 打开前关闭其他面板,确保同时只显示一个。
* 英雄面板按钮回调:滑动到英雄面板(index=0)
*/
private onShowHerosClick() {
if (!this.cards_node || !this.cards_node.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.cards_node.active;
if (visible) {
this.closeAllPanels();
}
this.cards_node.active = visible;
const activeChild = this.showHeros?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
this.slideToPanel(0);
}
/**
* 关闭英雄卡池按钮回调:
* - 仅负责收起 cards_node不切换展开态
* - 同步关闭 showHeros 下的 "active" 子节点,保持与 toggle 状态一致。
* - 同步关闭已打开的英雄信息面板HInfo避免卡池收起后信息面板孤立悬浮。
* 关闭英雄面板按钮回调:隐藏所有面板。
* 同步关闭已打开的英雄信息面板HInfo避免卡池收起后信息面板孤立悬浮
*/
private onCloseHerosClick() {
if (!this.cards_node || !this.cards_node.isValid) return;
oops.audio.playEffect("music/button");
this.cards_node.active = false;
const activeChild = this.showHeros?.getChildByName("active");
if (activeChild) {
activeChild.active = false;
}
// 关闭英雄卡池时同步关闭已打开的英雄信息面板
this.hideAllPanels();
oops.gui.remove(UIID.HInfo);
}
// ======================== 装备商店面板 ========================
/**
* 装备商店显隐切换按钮回调:
* 切换 equipsPanNode 的 active 状态。
* 打开前关闭其他面板,确保同时只显示一个。
* 装备面板按钮回调:滑动到装备商店面板(index=1)。
*/
private onShowEquipsClick() {
if (!this.equipsPanNode || !this.equipsPanNode.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.equipsPanNode.active;
if (visible) {
this.closeAllPanels();
}
this.equipsPanNode.active = visible;
const activeChild = this.showEquips?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
this.slideToPanel(1);
}
/** 关闭装备商店按钮回调 */
/** 关闭装备面板按钮回调:隐藏所有面板 */
private onCloseEquipsClick() {
if (!this.equipsPanNode || !this.equipsPanNode.isValid) return;
oops.audio.playEffect("music/button");
this.equipsPanNode.active = false;
const activeChild = this.showEquips?.getChildByName("active");
if (activeChild) {
activeChild.active = false;
}
this.hideAllPanels();
}
/**
@@ -1143,22 +1152,20 @@ export class MissionCardComp extends CCComp {
/**
* 进入准备阶段:
* - 先将卡牌面板置为收起态,激活 showHeros 按钮可见
* - 再触发一次 onShowHerosClick等同玩家点击 showHeros 按钮),
* 让卡池以"按钮点击"的方式展开,保持与玩家手动点击一致的交互表现
* - 初始化面板轮播布局(如尚未初始化)
* - 滑动到英雄面板index=0作为默认展示。
* - 激活 showHeros 按钮可见性
*/
private enterPreparePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
// 先收起面板,确保后续 onShowHerosClick 能正确切换为展开
Tween.stopAllByTarget(this.cards_node);
this.cards_node.setScale(this.cardsShowScale);
this.cards_node.active = false;
// 确保面板布局已初始化
if (!this.hasInitPanelLayout) {
this.initPanelLayout();
}
// 显式激活「显示英雄卡池」按钮本身,让玩家可见可点
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.active = true;
const showActive = this.showHeros.getChildByName("active");
if (showActive) showActive.active = false;
}
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
@@ -1166,8 +1173,8 @@ export class MissionCardComp extends CCComp {
nobg.active = !this.canDrawCards();
}
}
// 触发一次 showHeros 按钮点击效果,自动展开卡池
this.onShowHerosClick();
// 滑动到英雄面板
this.slideToPanel(0);
}
private enterBattlePhase() {