refactor(missionCard): 重构抽卡逻辑与按钮命名

1.  重命名抽卡按钮相关变量与注释为刷新卡池
2.  调整抽卡规则:不再过滤已召唤英雄,支持同英雄二合一合成
3.  新增招募计数循环机制:每4次招募触发一次三选一
4.  添加近战保底逻辑,全远程阵容时提升近战卡权重
5.  移除冗余的已召唤英雄过滤逻辑
This commit is contained in:
pan
2026-08-12 15:31:27 +08:00
parent db98b60737
commit a5dc1ef540
2 changed files with 1133 additions and 1061 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
* 职责: * 职责:
* 1. **卡牌分发管理** —— 从卡池抽取 3 张英雄卡,分发到 3 个 CHeroComp 槽位。 * 1. **卡牌分发管理** —— 从卡池抽取 3 张英雄卡,分发到 3 个 CHeroComp 槽位。
* 抽卡规则:从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复; * 抽卡规则:从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复;
* 场上已召唤英雄不再刷出 * 不过滤已召唤英雄,抽到同 uuid 可触发二合一合成升级
* 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。 * 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。
* 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。 * 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。
* 4. **场上英雄信息面板HInfoComp 列表)同步** —— * 4. **场上英雄信息面板HInfoComp 列表)同步** ——
@@ -160,9 +160,9 @@ export class MissionCardComp extends CCComp {
/** 药品刷新按钮节点 */ /** 药品刷新按钮节点 */
@property(Node) @property(Node)
shop_refresh: Node = null!; shop_refresh: Node = null!;
/** 抽卡(刷新)按钮节点 */ /** 刷新卡池按钮节点 */
@property(Node) @property(Node)
cards_chou: Node = null! cards_refresh: Node = null!
/** 金币显示节点(含 icon + num 子节点) */ /** 金币显示节点(含 icon + num 子节点) */
@property(Node) @property(Node)
@@ -192,6 +192,13 @@ export class MissionCardComp extends CCComp {
private itemCardComps: ItemListComp[] = []; private itemCardComps: ItemListComp[] = [];
/** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */ /** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */
private hasCalledHero: boolean = false; private hasCalledHero: boolean = false;
/**
* 招募计数:每次点击招募 +1。
* 节奏为「每 4 次一循环」:前 3 次直接随机召唤,第 4 次走三选一。
*/
private recruitCount: number = 0;
/** 全远程阵容时近战卡的权重放大倍数(近战保底) */
private static readonly MELEE_PITY_WEIGHT_MUL = 3;
// ======================== 面板划出状态 ======================== // ======================== 面板划出状态 ========================
@@ -271,10 +278,10 @@ export class MissionCardComp extends CCComp {
// 清理新手按钮指引的调度与动画,防止回调泄漏 // 清理新手按钮指引的调度与动画,防止回调泄漏
this.stopButtonGuide(); this.stopButtonGuide();
this.unscheduleAllCallbacks(); this.unscheduleAllCallbacks();
if (this.cards_chou && this.cards_chou.isValid) { if (this.cards_refresh && this.cards_refresh.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_refresh.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this); this.cards_refresh.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this); this.cards_refresh.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
} }
if (this.showHeros && this.showHeros.isValid) { if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this); this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
@@ -324,7 +331,7 @@ export class MissionCardComp extends CCComp {
this.layoutCardSlots(); this.layoutCardSlots();
this.clearAllCards(); this.clearAllCards();
this.resetButtonScale(this.cards_chou); this.resetButtonScale(this.cards_refresh);
this.resetPanelOpenCosts(); this.resetPanelOpenCosts();
this.updateCoinAndCostUI(); this.updateCoinAndCostUI();
this.updateHeroNumUI(false, false); this.updateHeroNumUI(false, false);
@@ -399,7 +406,7 @@ export class MissionCardComp extends CCComp {
* 绑定所有事件监听: * 绑定所有事件监听:
* - 节点级事件MissionStart / MissionEnd / FightStart * - 节点级事件MissionStart / MissionEnd / FightStart
* - 全局消息CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard * - 全局消息CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard
* - 按钮触控:抽卡cards_chou * - 按钮触控:刷新卡池cards_refresh
*/ */
private bindEvents() { private bindEvents() {
/** 生命周期事件(节点级) */ /** 生命周期事件(节点级) */
@@ -422,9 +429,9 @@ export class MissionCardComp extends CCComp {
oops.message.on(GameEvent.HeroUpgrade, this.onHeroUpgrade, this); oops.message.on(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
/** 按钮触控事件:抽卡 */ /** 按钮触控事件:抽卡 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_refresh?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this); this.cards_refresh?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this); this.cards_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
/** 隐藏卡牌面板按钮 */ /** 隐藏卡牌面板按钮 */
@@ -512,7 +519,7 @@ export class MissionCardComp extends CCComp {
let targetNode: Node | null = null; let targetNode: Node | null = null;
switch (type) { switch (type) {
case "refresh_coin": case "refresh_coin":
targetNode = this.cards_chou; targetNode = this.cards_refresh;
break; break;
case "buy_coin": case "buy_coin":
targetNode = this.coins_node; targetNode = this.coins_node;
@@ -686,10 +693,10 @@ export class MissionCardComp extends CCComp {
oops.message.off(GameEvent.CardUsed, this.onCardUsed, this); oops.message.off(GameEvent.CardUsed, this.onCardUsed, this);
oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this); oops.message.off(GameEvent.HeroBoxEmptyClick, this.onHeroBoxEmptyClick, this);
oops.message.off(GameEvent.HeroUpgrade, this.onHeroUpgrade, this); oops.message.off(GameEvent.HeroUpgrade, this.onHeroUpgrade, this);
if (this.cards_chou && this.cards_chou.isValid) { if (this.cards_refresh && this.cards_refresh.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this); this.cards_refresh.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this); this.cards_refresh.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this); this.cards_refresh.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
} }
if (this.showHeros && this.showHeros.isValid) { if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this); this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
@@ -910,16 +917,16 @@ export class MissionCardComp extends CCComp {
/** 抽卡按钮按下反馈 */ /** 抽卡按钮按下反馈 */
private onDrawTouchStart() { private onDrawTouchStart() {
this.playButtonPressAnim(this.cards_chou); this.playButtonPressAnim(this.cards_refresh);
} }
/** 抽卡按钮释放 → 执行抽卡逻辑 */ /** 抽卡按钮释放 → 执行抽卡逻辑 */
private onDrawTouchEnd() { private onDrawTouchEnd() {
oops.audio.playEffect("music/button"); oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.cards_chou, () => this.onClickDraw()); this.playButtonClickAnim(this.cards_refresh, () => this.onClickDraw());
} }
/** 抽卡按钮取消 → 恢复缩放 */ /** 抽卡按钮取消 → 恢复缩放 */
private onDrawTouchCancel() { private onDrawTouchCancel() {
this.playButtonResetAnim(this.cards_chou); this.playButtonResetAnim(this.cards_refresh);
} }
// ======================== 新手按钮指引 ======================== // ======================== 新手按钮指引 ========================
@@ -1214,25 +1221,40 @@ export class MissionCardComp extends CCComp {
} }
/** /**
* 英雄面板按钮回调 * 英雄招募按钮(每 4 次一循环)
* 英雄面板herosPanNode为常驻基准面板固定不动 * - 前 3 次:扣面板开启费后直接随机召唤 1 个英雄(不开面板,无需玩家选)。
* 点击按钮收起装备/技能/药品覆盖面板并显示英雄抽卡面板cardPanNode * - 第 4 次:扣费后展开三选一面板(缩放 0→1玩家选 1
* 满员HERO_MAX_NUM时气泡提示并拦截不扣费、不计数。
*/ */
private onShowHerosClick() { private onShowHerosClick() {
oops.audio.playEffect("music/button"); oops.audio.playEffect("music/button");
// 已展开则不重复开启(不重复扣费) // 满员拦截:随机招募与三选一都禁止
if (this.isCardPanelShown) return;
// 英雄槽位已满:气泡提示并中止,不再展开三选一,也不扣开启费用
if (this.getAliveHeroCount() >= this.getMissionHeroMaxNum()) { if (this.getAliveHeroCount() >= this.getMissionHeroMaxNum()) {
this.showSmallTip("panel_hero_full", "英雄槽已满"); this.showSmallTip("panel_hero_full", "英雄槽已满");
return; return;
} }
// 开启英雄抽卡面板需支付金币,金币不足则提示并中止 // 随机招募与三选一都需支付面板开启费,金币不足则提示并中止
if (!this.tryPayPanelOpenCost(0)) return; if (!this.tryPayPanelOpenCost(0)) return;
// 收起装备/技能/药品覆盖面板
// 先计数再按节奏分支count % 4 == 0 展开三选一,其余直接随机召唤
this.recruitCount++;
const isPickOneOfThree = (this.recruitCount % 4) === 0;
mLogger.log(this.debugMode, "MissionCardComp", "hero recruit", {
recruitCount: this.recruitCount,
mode: isPickOneOfThree ? "pick_one_of_three" : "random_summon"
});
// 收起装备/技能/药品覆盖面板,回到英雄面板
this.slideToPanel(0); this.slideToPanel(0);
// 显示英雄抽卡面板(缩放 0→1并点亮按钮 active 高亮)
if (isPickOneOfThree) {
// 第 4 次:展开英雄抽卡面板(缩放 0→1点亮按钮 active 高亮)
this.showCardsPanel(); this.showCardsPanel();
} else {
// 前 3 次:直接随机召唤,不开面板
this.randomSummonOne();
}
} }
// ======================== 英雄信息盒herosBoxNode ======================== // ======================== 英雄信息盒herosBoxNode ========================
@@ -1752,10 +1774,10 @@ export class MissionCardComp extends CCComp {
// ======================== 核心业务:抽卡 ======================== // ======================== 核心业务:抽卡 ========================
/** /**
* 抽卡按钮核心逻辑: * 刷新卡池按钮核心逻辑:
* 1. 检查金币是否足够 → 不够则 toast 提示。 * 1. 检查金币/刷新石是否足够 → 不够则 toast 提示。
* 2. 扣除费用、播放金币动画。 * 2. 扣除费用、播放金币动画。
* 3. 重新布局槽位 → 从卡池构建 3 张卡 → 分发到槽位。 * 3. 重新布局槽位 → 从卡池构建 3 张卡 → 分发到槽位(三选一展示)
*/ */
private onClickDraw() { private onClickDraw() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost); const cost = MissionEconomy.getRefreshCost(this.refreshCost);
@@ -1770,7 +1792,7 @@ export class MissionCardComp extends CCComp {
return; return;
} }
mLogger.log(this.debugMode, "MissionCardComp", "click draw", { mLogger.log(this.debugMode, "MissionCardComp", "click refresh", {
cost, cost,
leftCoin: MissionEconomy.getCoin() leftCoin: MissionEconomy.getCoin()
}); });
@@ -1779,6 +1801,63 @@ export class MissionCardComp extends CCComp {
this.dispatchCardsToSlots(cards); this.dispatchCardsToSlots(cards);
} }
/**
* 直接随机召唤 1 个英雄:
* - 全英雄卡池按权重随机(不过滤已召唤英雄,配合二合一合成)。
* - 近战保底:场上已有英雄且全部为远程/中程(无近战)时,近战卡权重放大。
*/
private randomSummonOne() {
const pool = this.buildRandomSummonPool();
if (pool.length === 0) return;
const pick = this.weightedPick(pool);
if (!pick) return;
mLogger.log(this.debugMode, "MissionCardComp", "random summon", { uuid: pick.uuid });
oops.message.dispatchEvent(GameEvent.CallHero, {
uuid: pick.uuid,
hero_lv: 1,
card_lv: pick.card_lv ?? 1
});
}
/**
* 构建随机召唤卡池:全英雄卡,必要时按近战保底放大近战权重。
* @returns 带运行时权重的卡配置数组weight 已按保底调整)
*/
private buildRandomSummonPool(): CardConfig[] {
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
if (heroCards.length === 0) return [];
// 全远程判定:有存活英雄但没有任何近战 → 触发放大近战权重
const meleeMul = this.isAllRangedFormation()
? MissionCardComp.MELEE_PITY_WEIGHT_MUL
: 1;
if (meleeMul === 1) return heroCards;
return heroCards.map(c => {
const hero = HeroInfo[c.uuid];
const isMelee = hero && hero.type === HType.Melee;
// 复制一份避免污染卡池原始 weight
return isMelee ? { ...c, weight: (c.weight ?? 0) * meleeMul } : c;
});
}
/**
* 判断当前场上是否为「全远程阵容」:
* 有至少 1 个存活英雄且没有任何近战Melee英雄。
*/
private isAllRangedFormation(): boolean {
let heroCount = 0;
let meleeCount = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model || model.fac !== FacSet.HERO || model.is_dead) return;
heroCount++;
if (model.type === HType.Melee) meleeCount++;
});
return heroCount > 0 && meleeCount === 0;
}
// ======================== 阶段切换 ======================== // ======================== 阶段切换 ========================
/** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */ /** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */
@@ -1852,15 +1931,13 @@ export class MissionCardComp extends CCComp {
* *
* 规则: * 规则:
* 1. 从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复。 * 1. 从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复。
* 2. 过滤掉场上已召唤英雄(已召唤的英雄不再刷出,仅通过升级卡升级 * 2. 过滤已召唤英雄:抽到同 uuid 可触发二合一合成升级。
* 3. 不足 3 张时循环补齐(允许重复)。 * 3. 不足 3 张时循环补齐(允许重复)。
* *
* 注:升级卡机制已移除,英雄卡池只出普通英雄卡。 * 注:升级卡机制已移除,英雄卡池只出普通英雄卡。
*/ */
private buildDrawCards(): CardConfig[] { private buildDrawCards(): CardConfig[] {
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero); const availableHeroCards = CardPoolList.filter(c => c.type === CardType.Hero);
const aliveHeroUuids = this.getAliveHeroUuids();
const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid));
if (availableHeroCards.length === 0) return []; if (availableHeroCards.length === 0) return [];
@@ -1903,12 +1980,10 @@ export class MissionCardComp extends CCComp {
heroType, heroType,
unique: true, // 保证一次刷新内的英雄卡不重复 unique: true, // 保证一次刷新内的英雄卡不重复
}); });
// 过滤掉场上已召唤英雄的普通英雄卡(已召唤的英雄不再重复刷出) // 过滤已召唤英雄:抽到同 uuid 可触发二合一合成升级
const aliveHeroUuids = this.getAliveHeroUuids(); if (cards.length <= 0) return false;
const available = cards.filter(c => !aliveHeroUuids.has(c.uuid));
if (available.length <= 0) return false;
this.layoutCardSlots(); this.layoutCardSlots();
this.dispatchCardsToSlots(available.slice(0, 3)); this.dispatchCardsToSlots(cards.slice(0, 3));
return true; return true;
} }
@@ -2029,7 +2104,7 @@ export class MissionCardComp extends CCComp {
private updateDrawCostUI() { private updateDrawCostUI() {
const stones = MissionEconomy.getRefreshStone(); const stones = MissionEconomy.getRefreshStone();
const stoneCost = MissionEconomy.getRefreshStoneCost(); const stoneCost = MissionEconomy.getRefreshStoneCost();
this.updateOneRefreshBtnUI(this.cards_chou, stones, stoneCost); this.updateOneRefreshBtnUI(this.cards_refresh, stones, stoneCost);
this.updateOneRefreshBtnUI(this.equip_refresh, stones, stoneCost); this.updateOneRefreshBtnUI(this.equip_refresh, stones, stoneCost);
this.updateOneRefreshBtnUI(this.shop_refresh, stones, stoneCost); this.updateOneRefreshBtnUI(this.shop_refresh, stones, stoneCost);
this.updateOneRefreshBtnUI(this.skill_refresh, stones, stoneCost); this.updateOneRefreshBtnUI(this.skill_refresh, stones, stoneCost);
@@ -2163,22 +2238,6 @@ export class MissionCardComp extends CCComp {
return actors; return actors;
} }
/**
* 获取场上所有存活英雄的 hero_uuid 集合。
* 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出。
*/
private getAliveHeroUuids(): Set<number> {
const uuids = new Set<number>();
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model) return;
if (model.fac !== FacSet.HERO) return;
if (model.is_dead) return;
uuids.add(model.hero_uuid);
});
return uuids;
}
/** /**
* 按 eid 精确升级场上对应英雄实体。 * 按 eid 精确升级场上对应英雄实体。
* *
@@ -2343,7 +2402,7 @@ export class MissionCardComp extends CCComp {
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() { reset() {
this.resetButtonScale(this.cards_chou); this.resetButtonScale(this.cards_refresh);
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用 // 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.cardComps = []; this.cardComps = [];