refactor(heroCard): 重构英雄卡牌系统逻辑与UI布局

1.  重写抽卡逻辑,移除升级卡机制,改为从基础英雄池按权重抽取不重复英雄
2.  优化英雄卡入场、购买、刷新动画,新增按钮触摸反馈效果
3.  调整mission.prefab与cHero.prefab的UI元素位置与布局参数
4.  简化卡牌布局代码,交由prefab的Widget组件自动约束
This commit is contained in:
panFD
2026-07-26 14:44:43 +08:00
parent 16996b9805
commit d135aa9905
4 changed files with 213 additions and 177 deletions

View File

@@ -2244,7 +2244,7 @@
"value": { "value": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 0, "x": 0,
"y": -95.715, "y": -110.847,
"z": 0 "z": 0
} }
}, },
@@ -2523,14 +2523,14 @@
"node": { "node": {
"__id__": 1 "__id__": 1
}, },
"_enabled": true, "_enabled": false,
"__prefab": { "__prefab": {
"__id__": 150 "__id__": 150
}, },
"_alignFlags": 40, "_alignFlags": 0,
"_target": null, "_target": null,
"_left": 245, "_left": 310,
"_right": 245, "_right": 310,
"_top": 0, "_top": 0,
"_bottom": 0, "_bottom": 0,
"_horizontalCenter": 0, "_horizontalCenter": 0,

View File

@@ -1456,7 +1456,7 @@
"_lpos": { "_lpos": {
"__type__": "cc.Vec3", "__type__": "cc.Vec3",
"x": 306.634, "x": 306.634,
"y": 451.513, "y": 651.253,
"z": 0 "z": 0
}, },
"_lrot": { "_lrot": {
@@ -1975,7 +1975,7 @@
"_left": -8.317000000000007, "_left": -8.317000000000007,
"_right": -6.6340000000000146, "_right": -6.6340000000000146,
"_top": 89.63, "_top": 89.63,
"_bottom": 396.513, "_bottom": 596.253,
"_horizontalCenter": 0, "_horizontalCenter": 0,
"_verticalCenter": 0, "_verticalCenter": 0,
"_isAbsLeft": true, "_isAbsLeft": true,

View File

@@ -16,7 +16,7 @@
* - 英雄卡效果事件为 GameEvent.CallHero而非 UseSkillCard。 * - 英雄卡效果事件为 GameEvent.CallHero而非 UseSkillCard。
*/ */
import { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3 } from "cc"; import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardConfig, CardType } from "../common/config/CardSet"; import { CardConfig, CardType } from "../common/config/CardSet";
@@ -88,6 +88,8 @@ export class CHeroComp extends CCComp {
private fixedBaseZ: number = 0; private fixedBaseZ: number = 0;
/** 静止位置(由 MissionCardComp 布局决定) */ /** 静止位置(由 MissionCardComp 布局决定) */
private restPosition: Vec3 = new Vec3(); private restPosition: Vec3 = new Vec3();
/** 透明度组件(用于淡入淡出) */
private opacityComp: UIOpacity | null = null;
// ======================== 生命周期 ======================== // ======================== 生命周期 ========================
@@ -106,7 +108,12 @@ export class CHeroComp extends CCComp {
fied3: !!this.fied3, fied3: !!this.fied3,
buy_node: !!this.buy_node buy_node: !!this.buy_node
}); });
this.buy_node?.on(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this); this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node?.on(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
// 缓存透明度组件(用于淡入淡出动画)
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
// 补偿首次渲染addComponent 当帧 onLoad 尚未触发, // 补偿首次渲染addComponent 当帧 onLoad 尚未触发,
// 若 MissionCardComp 在 onLoad 前已 applyCardData此处绑定就绪后重刷一次 // 若 MissionCardComp 在 onLoad 前已 applyCardData此处绑定就绪后重刷一次
@@ -118,7 +125,9 @@ export class CHeroComp extends CCComp {
onDestroy() { onDestroy() {
super.onDestroy(); super.onDestroy();
if (this.buy_node && this.buy_node.isValid) { if (this.buy_node && this.buy_node.isValid) {
this.buy_node.off(NodeEventType.TOUCH_START, this.onBuyTouchStart, this);
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this); this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
} }
} }
@@ -127,6 +136,10 @@ export class CHeroComp extends CCComp {
/** /**
* 初始化英雄卡数据并渲染 UI * 初始化英雄卡数据并渲染 UI
* *
* 若槽位已有旧卡且处于显示状态,先播放旧卡"逝去"动画(缩小淡出),
* 动画结束后再切换为新卡数据并播放入场动画。
* 首次赋值node 未激活)直接渲染,无逝去动画。
*
* @param data 卡牌配置,传 null 时清空显示 * @param data 卡牌配置,传 null 时清空显示
*/ */
applyCardData(data: CardConfig | null): void { applyCardData(data: CardConfig | null): void {
@@ -142,6 +155,36 @@ export class CHeroComp extends CCComp {
icon: data?.icon ?? null, icon: data?.icon ?? null,
nodeActiveBefore: this.node?.active nodeActiveBefore: this.node?.active
}); });
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
if (this.isPurchased) {
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
return;
}
// 旧卡存在且当前显示中 → 播逝去动画,结束后再应用新卡
if (this.cardData && this.node.active) {
this.playRefreshExitAnim(() => {
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
});
return;
}
// 首次赋值或槽位为空 → 直接渲染
this.cardData = data; this.cardData = data;
this.isPurchased = false; this.isPurchased = false;
if (data) { if (data) {
@@ -154,6 +197,12 @@ export class CHeroComp extends CCComp {
/** 清空 UI 显示 */ /** 清空 UI 显示 */
private applyEmptyUI() { private applyEmptyUI() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.node.setScale(1, 1, 1);
if (this.name_label) this.name_label.string = ""; if (this.name_label) this.name_label.string = "";
if (this.level_label) this.level_label.string = ""; if (this.level_label) this.level_label.string = "";
const fieldLabels = [this.fied1, this.fied2, this.fied3]; const fieldLabels = [this.fied1, this.fied2, this.fied3];
@@ -252,6 +301,122 @@ export class CHeroComp extends CCComp {
costLabel.string = `${this.cardData.cost ?? 0}`; costLabel.string = `${this.cardData.cost ?? 0}`;
} }
} }
// 入场动画:从静止位置下方升起 + 淡入
this.playEnterAnim();
}
// ======================== 动画效果 ========================
/** 入场动画时长(秒) */
private readonly enterDuration: number = 0.25;
/** 购买动画时长(秒) */
private readonly purchaseDuration: number = 0.2;
/**
* 播放入场动画由小变大scale 0→1+ 淡入。
* 不依赖位置,与 Widget 布局完全解耦。
*/
private playEnterAnim() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 0;
}
this.node.setScale(0, 0, 1);
tween(this.node)
.to(this.enterDuration, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.enterDuration, { opacity: 255 })
.start();
}
}
/**
* 播放购买动画卡牌放大scale 1→1.1+ 淡出,结束后隐藏节点。
* 动画结束由 onBuyClick 的后续逻辑CardUsed 刷新卡池)接管。
*/
private playPurchaseAnim() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
}
tween(this.node)
.to(this.purchaseDuration, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadIn' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.purchaseDuration, { opacity: 0 })
.start();
}
}
/**
* 播放普通刷新"逝去"动画旧卡缩小scale 1→0+ 淡出,结束后回调切换新卡。
* 与购买动画区分:购买是放大逝去,普通刷新是缩小逝去。
* @param onComplete 动画结束回调,用于应用新卡数据
*/
private playRefreshExitAnim(onComplete: () => void) {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
}
tween(this.node)
.to(this.purchaseDuration, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
.start();
if (this.opacityComp) {
tween(this.opacityComp)
.to(this.purchaseDuration, { opacity: 0 })
.call(onComplete)
.start();
} else {
// 无透明度组件时直接延迟回调
this.scheduleOnce(onComplete, this.purchaseDuration);
}
}
// ======================== 按钮动画 ========================
/** 按钮正常缩放 */
private readonly btnNormalScale: number = 1;
/** 按钮按下缩放 */
private readonly btnPressScale: number = 0.92;
/** 按钮点击回弹峰值缩放 */
private readonly btnClickScale: number = 1.08;
/** 购买按钮按下:缩小反馈 */
private onBuyTouchStart() {
this.playBtnScaleTo(this.btnPressScale, 0.06);
}
/** 购买按钮取消:恢复缩放 */
private onBuyTouchCancel() {
this.playBtnScaleTo(this.btnNormalScale, 0.08);
}
/** 按钮缩放动画(通用) */
private playBtnScaleTo(scale: number, duration: number) {
if (!this.buy_node || !this.buy_node.isValid) return;
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
.to(duration, { scale: new Vec3(scale, scale, 1) })
.start();
}
/** 按钮点击回弹动画:先弹到峰值再回到正常 */
private playBtnClickAnim(onComplete?: () => void) {
if (!this.buy_node || !this.buy_node.isValid) {
onComplete?.();
return;
}
Tween.stopAllByTarget(this.buy_node);
tween(this.buy_node)
.to(0.05, { scale: new Vec3(this.btnClickScale, this.btnClickScale, 1) })
.to(0.08, { scale: new Vec3(this.btnNormalScale, this.btnNormalScale, 1) })
.call(() => onComplete?.())
.start();
} }
/** /**
@@ -298,15 +463,19 @@ export class CHeroComp extends CCComp {
/** /**
* 购买点击处理: * 购买点击处理:
* 1. 英雄卡先通过 UseHeroCard 事件做数量上限校验guard 模式,可被取消) * 1. 播放按钮点击回弹动画
* 2. 扣除金币(失败则提示并中止)。 * 2. 英雄卡先通过 UseHeroCard 事件做数量上限校验guard 模式,可被取消)。
* 3. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard * 3. 扣除金币(失败则提示并中止)
* 4. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池 * 4. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard
* 5. 标记已购买,隐藏购买按钮 * 5. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池
* 6. 标记已购买,隐藏购买按钮。
*/ */
private onBuyClick() { private onBuyClick() {
if (!this.cardData || this.isPurchased) return; if (!this.cardData || this.isPurchased) return;
// 播放按钮点击回弹动画(视觉反馈,不阻塞后续逻辑)
this.playBtnClickAnim();
oops.audio.playEffect("music/button"); oops.audio.playEffect("music/button");
// 英雄卡数量上限校验guard 模式MissionCardComp 可设 cancel=true 阻止使用 // 英雄卡数量上限校验guard 模式MissionCardComp 可设 cancel=true 阻止使用
@@ -353,6 +522,9 @@ export class CHeroComp extends CCComp {
break; break;
} }
// 播放购买动画(缩小淡出),刷新卡池由 CardUsed 事件触发
this.playPurchaseAnim();
// 标记已购买 // 标记已购买
this.isPurchased = true; this.isPurchased = true;
if (this.buy_node) { if (this.buy_node) {

View File

@@ -3,9 +3,9 @@
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层) * @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
* *
* 职责: * 职责:
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CHeroComp 槽位。 * 1. **卡牌分发管理** —— 从卡池抽取 3 张英雄卡,分发到 3 个 CHeroComp 槽位。
* 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡" * 抽卡规则:从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复
* 场上无英雄时仅抽取英雄卡和刷新卡 * 场上已召唤的英雄不再刷出
* 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。 * 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。
* 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。 * 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。
* 4. **场上英雄信息面板HInfoComp 列表)同步** —— * 4. **场上英雄信息面板HInfoComp 列表)同步** ——
@@ -17,7 +17,7 @@
* 关键设计: * 关键设计:
* - 3 个 CHeroComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[] * - 3 个 CHeroComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[]
* 之后所有分发、清空操作均通过此数组进行。 * 之后所有分发、清空操作均通过此数组进行。
* - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。 * - buildDrawCards() 从基础英雄卡池按权重抽取 3 张,不足时循环补齐。
* - 英雄上限校验onUseHeroCard采用 guard/cancel 模式: * - 英雄上限校验onUseHeroCard采用 guard/cancel 模式:
* CHeroComp 发出 UseHeroCard 事件并传入 guard 对象, * CHeroComp 发出 UseHeroCard 事件并传入 guard 对象,
* 本组件可通过 guard.cancel=true 阻止使用。 * 本组件可通过 guard.cancel=true 阻止使用。
@@ -25,7 +25,7 @@
* 历史: * 历史:
* 旧版本曾包含"卡池等级poolLv"机制和"三合一合成腾位"判断,已全部移除: * 旧版本曾包含"卡池等级poolLv"机制和"三合一合成腾位"判断,已全部移除:
* - 卡牌不再分级,所有英雄卡统一 lv1。 * - 卡牌不再分级,所有英雄卡统一 lv1。
* - 英雄升级仅通过升级卡SpecialUpgrade触发,按 UUID 精确升级场上对应英雄 * - 升级卡机制已移除,英雄卡池只出普通英雄卡;英雄升级仅通过升级卡SpecialUpgrade触发。
* *
* 依赖: * 依赖:
* - CHeroComp —— 单卡槽位 * - CHeroComp —— 单卡槽位
@@ -39,7 +39,7 @@ import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, Tween, twe
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { GameEvent } from "../common/config/GameEvent"; import { GameEvent } from "../common/config/GameEvent";
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet"; import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType } from "../common/config/CardSet";
import { drawSkillCards } from "../common/config/SCardSet"; import { drawSkillCards } from "../common/config/SCardSet";
import { drawEquipCards } from "../common/config/EquipSet"; import { drawEquipCards } from "../common/config/EquipSet";
import { drawItemCards } from "../common/config/ICardSet"; import { drawItemCards } from "../common/config/ICardSet";
@@ -70,8 +70,6 @@ const { ccclass, property } = _decorator;
export class MissionCardComp extends CCComp { export class MissionCardComp extends CCComp {
/** 是否启用调试日志 */ /** 是否启用调试日志 */
private debugMode: boolean = false; private debugMode: boolean = false;
/** 卡牌槽位宽度(像素),用于水平等距布局 */
private readonly cardWidth: number = 175;
/** 按钮正常缩放 */ /** 按钮正常缩放 */
private readonly buttonNormalScale: number = 1; private readonly buttonNormalScale: number = 1;
/** 按钮按下缩放 */ /** 按钮按下缩放 */
@@ -217,14 +215,6 @@ export class MissionCardComp extends CCComp {
private cardsShowScale: Vec3 = new Vec3(1, 1, 1); private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板收起态缩放scale=0 隐藏) */ /** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1); private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌原始定位点 */
private cardsPos = [-220, 0, 220]
/**
* 必出升级卡的轮询索引:每次刷新按场上可升级英雄 UUID 升序轮询选 1 张。
* 每次 buildDrawCards 取模更新,保证多次刷新时升级卡依次切换。
*/
private upgradeRotationIndex: number = 0;
/** 卡牌面板显示位置 Y 坐标 */ /** 卡牌面板显示位置 Y 坐标 */
private readonly cardPanelShowY: number = 0; private readonly cardPanelShowY: number = 0;
/** 卡牌面板隐藏位置 Y 坐标(向下 700 */ /** 卡牌面板隐藏位置 Y 坐标(向下 700 */
@@ -801,8 +791,6 @@ export class MissionCardComp extends CCComp {
if (!uuid) return; if (!uuid) return;
let success = false; let success = false;
if (type === CardType.SpecialUpgrade) { if (type === CardType.SpecialUpgrade) {
const template = SpecialUpgradeCardList[uuid];
if (!template) return;
const targetHeroEid = Number(payload?.target_hero_eid ?? 0); const targetHeroEid = Number(payload?.target_hero_eid ?? 0);
success = this.tryUpgradeHeroByEid(targetHeroEid); success = this.tryUpgradeHeroByEid(targetHeroEid);
if (!success) { if (!success) {
@@ -1337,123 +1325,42 @@ export class MissionCardComp extends CCComp {
} }
/** /**
* 构建本次抽卡结果,保证最终可分发 3 条数据 * 构建本次抽卡结果,返回 3 张英雄卡
* *
* 抽卡规则: * 规则:
* 1. **必出规则**:当场上有可升级英雄时,每次刷新必出 1 张升级卡,并放在返回数组首位(对应卡池最左侧槽位) * 1. 从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复
* 多次刷新时,按场上英雄 target_hero_eid 升序循环切换升级卡upgradeRotationIndex)。 * 2. 过滤掉场上已召唤的英雄(已召唤的英雄不再刷出,仅通过升级卡升级)。
* 2. 其余 2 张从混合池(剩余升级卡 + 基础英雄卡)按权重抽取 * 3. 不足 3 张时循环补齐(允许重复)
* *
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,全部出升级卡(若全部已满级则降级回混合池) * 注:升级卡机制已移除,英雄卡池只出普通英雄卡
*/ */
private buildDrawCards(): CardConfig[] { private buildDrawCards(): CardConfig[] {
const upgradeCards = this.buildHeroUpgradeCards();
const aliveHeroCount = this.getAliveHeroCount();
const heroMax = this.getMissionHeroMaxNum();
// 英雄已满员且有可升级的英雄 → 只出升级卡
if (aliveHeroCount >= heroMax && upgradeCards.length > 0) {
const picked = this.pickMixedCards(upgradeCards, 3, upgradeCards);
if (picked.length >= 3) return picked.slice(0, 3);
/** 兜底:不足 3 张时循环补齐 */
const filled = [...picked];
while (filled.length < 3) {
filled.push(upgradeCards[filled.length % upgradeCards.length]);
}
return filled;
}
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero); const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
// 过滤掉场上已召唤英雄的普通英雄卡:已召唤的英雄只能通过升级卡升级,不再刷出普通卡
const aliveHeroUuids = this.getAliveHeroUuids(); const aliveHeroUuids = this.getAliveHeroUuids();
const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid)); const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid));
// 英雄池只刷英雄卡(含动态升级卡 + 未召唤英雄的普通卡) if (availableHeroCards.length === 0) return [];
const mixedPool: CardConfig[] = [...upgradeCards, ...availableHeroCards];
if (mixedPool.length === 0) return [];
const result: CardConfig[] = []; const result: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>(); const usedUuids = new Set<number>();
const usedHeroUuids = new Set<number>();
// 必出 1 张升级卡,按 UUID 升序轮询,放在返回数组首位(对应最左侧卡槽) // 按权重抽取,同一次抽卡内英雄不重复
if (upgradeCards.length > 0) {
const idx = this.upgradeRotationIndex % upgradeCards.length;
this.upgradeRotationIndex = (this.upgradeRotationIndex + 1) % upgradeCards.length;
const mandatory = upgradeCards[idx];
result.push(mandatory);
usedUpgradeTargets.add(mandatory.target_hero_eid ?? 0);
}
// 剩余张数从混合池抽取,排除已选升级卡 target_hero_eid 和已选英雄卡 uuid
const remainingCount = 3 - result.length;
if (remainingCount > 0) {
const remainingPool = mixedPool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
return !usedUpgradeTargets.has(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
return !usedHeroUuids.has(c.uuid);
}
return true;
});
const rest = this.pickMixedCards(remainingPool, remainingCount, upgradeCards);
rest.forEach(c => {
result.push(c);
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
usedUpgradeTargets.add(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
usedHeroUuids.add(c.uuid);
}
});
}
// 兜底:不足 3 张时从未使用的英雄卡中补齐,避免出现重复英雄
while (result.length < 3) { while (result.length < 3) {
const fillCard = availableHeroCards.find(c => !usedHeroUuids.has(c.uuid)); const pool = availableHeroCards.filter(c => !usedUuids.has(c.uuid));
if (!fillCard) break; if (pool.length === 0) break;
result.push(fillCard); const pick = this.weightedPick(pool);
usedHeroUuids.add(fillCard.uuid); if (!pick) break;
result.push(pick);
usedUuids.add(pick.uuid);
}
// 兜底:不足 3 张时循环补齐(允许重复)
while (result.length < 3) {
result.push(availableHeroCards[result.length % availableHeroCards.length]);
} }
return result.slice(0, 3); return result.slice(0, 3);
} }
/**
* 从混合池中按权重抽取 n 张卡。
* 升级卡之间强制 unique同一个 target_hero_eid 只能出现一次)。
* 英雄卡之间强制 unique同一个 uuid 只能出现一次),避免同一次发牌出现重复英雄。
*/
private pickMixedCards(pool: CardConfig[], count: number, upgradeCards: CardConfig[]): CardConfig[] {
if (pool.length === 0 || count <= 0) return [];
const selected: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>();
const usedHeroUuids = new Set<number>();
while (selected.length < count) {
const available = pool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
return !usedUpgradeTargets.has(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
return !usedHeroUuids.has(c.uuid);
}
return true;
});
if (available.length === 0) break;
const pick = this.weightedPick(available);
if (!pick) break;
selected.push(pick);
if (pick.type === CardType.SpecialUpgrade && pick.target_hero_eid) {
usedUpgradeTargets.add(pick.target_hero_eid);
}
if (pick.type === CardType.Hero) {
usedHeroUuids.add(pick.uuid);
}
}
return selected;
}
/** 单次按权重抽取一张卡 */ /** 单次按权重抽取一张卡 */
private weightedPick(cards: CardConfig[]): CardConfig | null { private weightedPick(cards: CardConfig[]): CardConfig | null {
if (cards.length === 0) return null; if (cards.length === 0) return null;
@@ -1466,38 +1373,6 @@ export class MissionCardComp extends CCComp {
return cards[cards.length - 1]; return cards[cards.length - 1];
} }
/**
* 扫描场上存活英雄,为每个英雄实体生成一张升级卡(按 eid 绑定)。
* - 已达 HERO_MAX_LV 的英雄不出卡。
* - 同 eid 只生成一张eid 本就唯一,不会重复)。
* - cost = 模板 cost + (当前等级 - 1) * BASE_COST等级越高升级越贵。
*/
private buildHeroUpgradeCards(): CardConfig[] {
const template = SpecialUpgradeCardList[7001];
if (!template) return [];
const actors = this.queryAliveHeroActors();
if (actors.length === 0) return [];
const result: CardConfig[] = [];
for (const actor of actors) {
const eid = actor.eid;
const lv = actor.model.lv;
if (lv >= FightSet.HERO_MAX_LV) continue; // 已达上限不再出升级卡
const dynamicCost = template.cost + (lv - 1) * FightSet.BASE_COST;
result.push({
...template,
cost: dynamicCost,
weight: template.weight,
target_hero_eid: eid,
hero_lv: lv + 1,
});
}
// 按 target_hero_eid 升序排序,保证返回顺序稳定(便于 buildDrawCards 按顺序轮询)
result.sort((a, b) => (a.target_hero_eid ?? 0) - (b.target_hero_eid ?? 0));
return result;
}
/** /**
* 构建技能卡抽卡结果,返回 3 张。 * 构建技能卡抽卡结果,返回 3 张。
* *
@@ -1571,18 +1446,7 @@ export class MissionCardComp extends CCComp {
} }
private layoutCardSlots() { private layoutCardSlots() {
if (!this.cardComps) return; // 布局由 cHero.prefab 根节点 Widget 约束控制,代码不再覆盖位置
const count = this.cardComps.length;
if (count === 0) return;
for (let i = 0; i < count; i++) {
if (this.cardComps[i]) {
this.cardComps[i].setSlotPosition(this.cardsPos[i]);
}
}
mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", {
count,
cardWidth: this.cardWidth
});
} }
private playButtonPressAnim(node: Node | null) { private playButtonPressAnim(node: Node | null) {
@@ -1727,7 +1591,7 @@ export class MissionCardComp extends CCComp {
/** /**
* 获取场上所有存活英雄的 hero_uuid 集合。 * 获取场上所有存活英雄的 hero_uuid 集合。
* 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出,只能通过升级卡升级 * 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出。
*/ */
private getAliveHeroUuids(): Set<number> { private getAliveHeroUuids(): Set<number> {
const uuids = new Set<number>(); const uuids = new Set<number>();