feat: 新增技能卡系统,优化卡牌操作逻辑

1.  调整任务开始按钮显示逻辑,新增nobg节点控制
2.  重构卡牌拖拽逻辑,技能卡改为点击使用,英雄卡保留上划使用
3.  修改技能卡牌初始消耗为0
4.  新增技能卡槽面板,在特定波次开放技能卡抽取
5.  新增技能卡刷新按钮与相关回调逻辑
6.  优化抽卡UI显示与费用更新逻辑
This commit is contained in:
panFD
2026-06-03 22:40:09 +08:00
parent 1b384572c6
commit 7e86aed500
5 changed files with 1836 additions and 1448 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -126,14 +126,14 @@ HeroList.forEach(uuid => {
// 添加非英雄卡牌 (技能、功能卡)
CardPoolList.push(
// 技能卡牌 (以增益/辅助为主,因为在备战期没有敌人)
{ uuid: 6401, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6401"), info: t("skill_info_6401"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6402, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6402"), info: t("skill_info_6402"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6403, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6403"), info: t("skill_info_6403"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6404, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6404"), info: t("skill_info_6404"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6405, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6405"), info: t("skill_info_6405"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6406, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6406"), info: t("skill_info_6406"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6304, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6304"), info: t("skill_info_6304"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6305, type: CardType.Skill, cost: 8, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6305"), info: t("skill_info_6305"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6401, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6401"), info: t("skill_info_6401"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6402, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6402"), info: t("skill_info_6402"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6403, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6403"), info: t("skill_info_6403"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6404, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 1, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6404"), info: t("skill_info_6404"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6405, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6405"), info: t("skill_info_6405"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6406, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 2, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6406"), info: t("skill_info_6406"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6304, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6304"), info: t("skill_info_6304"), is_inst: true, t_times: 1, t_inv: 0 },
{ uuid: 6305, type: CardType.Skill, cost: 0, weight: 20, pool_lv: 3, kind: CKind.Skill, card_lv: 1, name: t("skill_name_6305"), info: t("skill_info_6305"), is_inst: true, t_times: 1, t_inv: 0 },
);

View File

@@ -110,6 +110,8 @@ export class CardComp extends CCComp {
private readonly dragUseThreshold: number = 70;
/** 触摸起始 Y 坐标,用于计算拖拽距离 */
private touchStartY: number = 0;
/** 触摸起始 X 坐标,用于点击判断 */
private touchStartX: number = 0;
/** 当前是否正在拖拽 */
private isDragging: boolean = false;
/** 当前是否正在执行"使用"流程(防止重复触发) */
@@ -483,6 +485,7 @@ export class CardComp extends CCComp {
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.touchStartX = event.getUILocation().x;
this.isDragging = true;
this.isLongPressed = false;
this.unschedule(this.onLongPress);
@@ -503,19 +506,26 @@ export class CardComp extends CCComp {
oops.gui.remove(UIID.HInfo);
}
}
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
// 技能卡不支持上划移动
if (this.cardData.type !== CardType.Skill) {
this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z);
}
}
/**
* 触摸结束:
* - 上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 技能卡:点击即可使用
* - 英雄卡/其他:上拉距离 >= dragUseThreshold → 视为"使用卡牌"
* - 否则视为"点击"或者"长按结束"
*/
private onCardTouchEnd(event: EventTouch) {
this.unschedule(this.onLongPress);
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const endX = event.getUILocation().x;
const deltaY = endY - this.touchStartY;
const deltaX = endX - this.touchStartX;
this.isDragging = false;
if (this.isLongPressed) {
@@ -523,12 +533,24 @@ export class CardComp extends CCComp {
oops.gui.remove(UIID.HInfo);
}
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
// 技能卡改为点击使用
if (this.cardData.type === CardType.Skill) {
if (Math.abs(deltaY) < 20 && Math.abs(deltaX) < 20) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
} else {
// 英雄卡保持上划使用
if (deltaY >= this.dragUseThreshold) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
return;
}
this.playReboundAnim();

View File

@@ -137,6 +137,8 @@ export class MissionCardComp extends CCComp {
/** 四个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 技能卡槽控制器缓存 */
private skillCardComps: CardComp[] = [];
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
/** 是否已缓存卡牌面板基准缩放 */
@@ -228,6 +230,12 @@ export class MissionCardComp extends CCComp {
}
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if ([1, 5, 10, 15, 20].includes(wave)) {
this.showSkillCardPopup();
}
mLogger.log(this.debugMode, "MissionCardComp", "mission start", {
poolLv: this.poolLv
});
@@ -279,6 +287,7 @@ export class MissionCardComp extends CCComp {
oops.message.on(GameEvent.HeroDead, this.onHeroDead, this);
oops.message.on(GameEvent.HeroSell, this.onHeroSell, this);
oops.message.on(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.on(GameEvent.CardPoolUpgrade, this.onCardPoolUpgrade, this);
@@ -286,6 +295,14 @@ export class MissionCardComp extends CCComp {
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
/** 技能卡刷新按钮 */
this.skill_refresh?.on(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh?.on(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
this.skill_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onSkillDrawTouchCancel, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_START, this.onSkillAdDrawTouchStart, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
// this.cards_up?.on(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
// this.cards_up?.on(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
// this.cards_up?.on(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
@@ -351,6 +368,34 @@ export class MissionCardComp extends CCComp {
this.layoutCardSlots();
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
const wave = this.getCurrentWave();
if ([1, 5, 10, 15, 20].includes(wave)) {
this.showSkillCardPopup();
}
}
private showSkillCardPopup() {
if (!this.skill_card_node) return;
this.skill_card_node.active = true;
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
private dispatchCardsToSkillSlots(cards: CardConfig[]) {
if (!this.skillCardComps) return;
for (let i = 0; i < this.skillCardComps.length; i++) {
if (this.skillCardComps[i]) {
this.skillCardComps[i].applyDrawCard(cards[i] ?? null);
}
}
}
private onUseSkillCard(event: string, args: any) {
// 选择技能后关闭弹窗
if (this.skill_card_node && this.skill_card_node.isValid) {
this.skill_card_node.active = false;
}
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
@@ -360,6 +405,7 @@ export class MissionCardComp extends CCComp {
oops.message.off(GameEvent.HeroDead, this.onHeroDead, this);
oops.message.off(GameEvent.HeroSell, this.onHeroSell, this);
oops.message.off(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.off(GameEvent.CardPoolUpgrade, this.onCardPoolUpgrade, this);
if (this.cards_chou && this.cards_chou.isValid) {
@@ -367,6 +413,16 @@ export class MissionCardComp extends CCComp {
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
}
if (this.skill_refresh && this.skill_refresh.isValid) {
this.skill_refresh.off(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh.off(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
this.skill_refresh.off(NodeEventType.TOUCH_CANCEL, this.onSkillDrawTouchCancel, this);
}
if (this.skill_ad_refresh && this.skill_ad_refresh.isValid) {
this.skill_ad_refresh.off(NodeEventType.TOUCH_START, this.onSkillAdDrawTouchStart, this);
this.skill_ad_refresh.off(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh.off(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
}
// this.cards_up?.off(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
// this.cards_up?.off(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
// this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
@@ -522,6 +578,44 @@ export class MissionCardComp extends CCComp {
private onDrawTouchCancel() {
this.playButtonResetAnim(this.cards_chou);
}
// ======================== 技能抽卡按钮回调 ========================
private onSkillDrawTouchStart() {
this.playButtonPressAnim(this.skill_refresh);
}
private onSkillDrawTouchEnd() {
this.playButtonClickAnim(this.skill_refresh, () => this.onClickSkillRefresh());
}
private onSkillDrawTouchCancel() {
this.playButtonResetAnim(this.skill_refresh);
}
private onSkillAdDrawTouchStart() {
this.playButtonPressAnim(this.skill_ad_refresh);
}
private onSkillAdDrawTouchEnd() {
this.playButtonClickAnim(this.skill_ad_refresh, () => this.onClickSkillAdRefresh());
}
private onSkillAdDrawTouchCancel() {
this.playButtonResetAnim(this.skill_ad_refresh);
}
private onClickSkillRefresh() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
oops.gui.toast(`金币不足,刷新需要${cost}`);
return;
}
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
private onClickSkillAdRefresh() {
// TODO: 接入广告 SDK 逻辑,目前先直接刷新
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
// /** 升级按钮按下反馈 */
// private onUpgradeTouchStart() {
// this.playButtonPressAnim(this.cards_up);
@@ -544,6 +638,11 @@ export class MissionCardComp extends CCComp {
this.cardComps = nodes
.map(node => node?.getComponent(CardComp))
.filter((comp): comp is CardComp => !!comp);
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
this.skillCardComps = skillNodes
.map(node => node?.getComponent(CardComp))
.filter((comp): comp is CardComp => !!comp);
}
// ======================== 核心业务:抽卡 & 升级 ========================
@@ -555,10 +654,10 @@ export class MissionCardComp extends CCComp {
* 3. 重新布局槽位 → 从卡池构建 4 张卡 → 分发到槽位。
*/
private onClickDraw() {
// if (this.isBattlePhase) {
// oops.gui.toast("战斗阶段无法抽卡");
// return;
// }
if (this.isBattlePhase) {
oops.gui.toast("战斗阶段无法抽卡");
return;
}
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
@@ -631,11 +730,23 @@ export class MissionCardComp extends CCComp {
this.cards_node.active = true;
Tween.stopAllByTarget(this.cards_node);
this.cards_node.setScale(this.cardsShowScale);
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
}
}
private enterBattlePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = true;
}
}
// 战斗阶段不再隐藏抽卡面板
// Tween.stopAllByTarget(this.cards_node);
// tween(this.cards_node)
@@ -650,12 +761,7 @@ export class MissionCardComp extends CCComp {
/** 构建本次抽卡结果保证最终可分发3条数据 */
private buildDrawCards(): CardConfig[] {
let targetType: CardType | CardType[] | undefined = undefined;
if (this.isBattlePhase) {
targetType = CardType.Skill;
} else {
targetType = [CardType.Hero, CardType.SpecialRefresh];
}
const targetType = [CardType.Hero, CardType.SpecialRefresh];
const cards = getCardsByLv(this.poolLv, targetType);
/** 正常情况下直接取前3 */
@@ -670,6 +776,20 @@ export class MissionCardComp extends CCComp {
return filled;
}
private buildSkillDrawCards(): CardConfig[] {
const targetType = CardType.Skill;
const cards = getCardsByLv(this.poolLv, targetType);
if (cards.length >= 3) return cards.slice(0, 3);
const filled = [...cards];
while (filled.length < 3) {
const fallback = getCardsByLv(this.poolLv, targetType);
if (fallback.length === 0) break;
filled.push(fallback[filled.length % fallback.length]);
}
return filled;
}
private tryRefreshHeroCards(heroType?: HType, targetPoolLv?: number): boolean {
const cards = drawCardsByRule(this.poolLv, {
count: 3,
@@ -716,6 +836,11 @@ export class MissionCardComp extends CCComp {
this.cardComps.forEach(comp => {
if (comp) comp.clearBySystem();
});
if (this.skillCardComps) {
this.skillCardComps.forEach(comp => {
if (comp) comp.clearBySystem();
});
}
}
private layoutCardSlots() {
@@ -808,15 +933,28 @@ export class MissionCardComp extends CCComp {
}
private updateDrawCostUI() {
if (!this.cards_chou) return;
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
if (this.cards_chou) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = this.isBattlePhase ? true : !this.canDrawCards();
}
const coinNode = this.cards_chou.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
const coinNode = this.cards_chou.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
if (this.skill_refresh) {
const nobg = this.skill_refresh.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
const coinNode = this.skill_refresh.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
}
@@ -1014,6 +1152,7 @@ export class MissionCardComp extends CCComp {
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.cardComps = [] as any;
this.skillCardComps = [] as any;
if (this.node && this.node.isValid) {
this.node.destroy();

View File

@@ -432,7 +432,11 @@ export class MissionComp extends CCComp {
break;
case MissionPhase.Prepare:
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true;
if (this.start_btn && this.start_btn.isValid) {
this.start_btn.active = true;
const nobg = this.start_btn.getChildByName("nobg");
if (nobg) nobg.active = false;
}
break;
case MissionPhase.PrepareEnd:
@@ -449,9 +453,10 @@ export class MissionComp extends CCComp {
smc.mission.stop_spawn_mon = false;
smc.mission.in_fight = true;
smc.vmdata.mission_data.in_fight = true;
// 战斗阶段:隐藏开始按钮
// 战斗阶段:隐藏开始按钮,激活 nobg
if (this.start_btn && this.start_btn.isValid) {
this.start_btn.active = false;
const nobg = this.start_btn.getChildByName("nobg");
if (nobg) nobg.active = true;
}
oops.message.dispatchEvent(GameEvent.FightStart);
break;
@@ -505,7 +510,10 @@ export class MissionComp extends CCComp {
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
smc.mission.stop_spawn_mon = false;
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
if (this.start_btn && this.start_btn.isValid) {
const nobg = this.start_btn.getChildByName("nobg");
if (nobg) nobg.active = true;
}
break;
}