refactor(mission): 重构卡牌系统为英雄卡槽组件

1. 新增CHeroComp作为英雄卡槽专用组件,替换原CardComp
2. 调整mission.prefab布局与预制体引用,适配新卡槽组件
3. 优化MissionCardComp的卡槽缓存与布局逻辑
4. 更新所有相关注释与类型引用,统一代码语义
This commit is contained in:
panFD
2026-07-26 12:52:58 +08:00
parent 399e25c3a6
commit 16996b9805
6 changed files with 5298 additions and 42 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "f2f9a3e0-d0f5-46f4-84bd-dd54c2248470",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "cHero"
}
}

View File

@@ -5906,7 +5906,7 @@
},
{
"__type__": "cc.Node",
"_name": "cards",
"_name": "cbox",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
@@ -5931,7 +5931,7 @@
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 296.948,
"y": 280,
"z": 0
},
"_lrot": {
@@ -5971,8 +5971,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 519.4,
"height": 300
"width": 720,
"height": 340
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -5983,7 +5983,7 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "d52ar8fGRCcJCkEGKYqLmS"
"fileId": "78ZkzaYgNLrq5b2cza8aKb"
},
{
"__type__": "cc.Widget",
@@ -5997,14 +5997,14 @@
"__prefab": {
"__id__": 352
},
"_alignFlags": 42,
"_alignFlags": 45,
"_target": null,
"_left": 100.30000000000001,
"_right": 100.30000000000001,
"_top": -491.51300000000003,
"_bottom": 491.51300000000003,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 110,
"_horizontalCenter": 0,
"_verticalCenter": 71.94799999999998,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
@@ -6012,14 +6012,14 @@
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 100,
"_originalHeight": 300,
"_originalHeight": 100,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "4dDc0+apdNPrjk0bxsb5yf"
"fileId": "6d6l1qXbRM3alP2Uq/Ylu4"
},
{
"__type__": "cc.Layout",
@@ -6029,11 +6029,11 @@
"node": {
"__id__": 348
},
"_enabled": false,
"_enabled": true,
"__prefab": {
"__id__": 354
},
"_resizeMode": 1,
"_resizeMode": 0,
"_layoutType": 1,
"_cellSize": {
"__type__": "cc.Size",
@@ -6041,11 +6041,11 @@
"height": 40
},
"_startAxis": 0,
"_paddingLeft": 0,
"_paddingRight": 0,
"_paddingLeft": 10,
"_paddingRight": 10,
"_paddingTop": 0,
"_paddingBottom": 0,
"_spacingX": 4.7,
"_spacingX": 5,
"_spacingY": 0,
"_verticalDirection": 1,
"_horizontalDirection": 0,
@@ -6057,7 +6057,7 @@
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "81yCxsR/BPCro5/jgovbJu"
"fileId": "986NWAVihCn4rQg8WZ0eck"
},
{
"__type__": "cc.PrefabInfo",
@@ -6067,7 +6067,7 @@
"asset": {
"__id__": 0
},
"fileId": "f2mrMK3fBJT4tB+8JEKln0",
"fileId": "52iIpQHPNPd56azk49eqsK",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
@@ -18390,7 +18390,7 @@
"__id__": 348
},
"cardPrefab": {
"__uuid__": "8aaff904-8c27-4173-9716-7161893b336e",
"__uuid__": "f2f9a3e0-d0f5-46f4-84bd-dd54c2248470",
"__expectedType__": "cc.Prefab"
},
"cards_chou": {
@@ -18405,8 +18405,12 @@
"showHeros": {
"__id__": 924
},
"herosPanNode": null,
"herosBoxNode": null,
"herosPanNode": {
"__id__": 227
},
"herosBoxNode": {
"__id__": 247
},
"heroPrefab": null,
"showEquips": {
"__id__": 953
@@ -18424,9 +18428,16 @@
"showSkills": {
"__id__": 985
},
"skillPanNode": null,
"skillBoxNode": null,
"skillPrefab": null,
"skillPanNode": {
"__id__": 621
},
"skillBoxNode": {
"__id__": 662
},
"skillPrefab": {
"__uuid__": "78a5b2d1-a34b-4190-895d-7d0aa22037cc",
"__expectedType__": "cc.Prefab"
},
"showShop": {
"__id__": 1014
},

View File

@@ -0,0 +1,424 @@
/**
* @file CHeroComp.ts
* @description 英雄卡池单个卡槽组件UI 视图层 + 购买/召唤逻辑)
*
* 职责:
* 1. 渲染单个英雄卡的显示信息(名称、等级、描述、图标、费用),
* 展示方式与 SCardComp 一致(图集静态图标 + ★等级 + 描述词条)。
* 同时支持动态升级卡SpecialUpgrade + target_hero_eid按英雄卡样式渲染。
* 2. 处理购买点击 → UseHeroCard 上限校验(guard) → 扣费 → 派发 CallHero 召唤英雄。
* 特殊卡(升级/刷新)则派发 UseSpecialCard。
* 3. 使用成功后派发 CardUsed 事件,通知 MissionCardComp 刷新英雄卡池。
*
* 与 SCardComp 的差异:
* - 数据源为 HeroInfoheroSet而非 SkillCardList / SkillSet。
* - 购买前需通过 GameEvent.UseHeroCard 做英雄数量上限校验。
* - 英雄卡效果事件为 GameEvent.CallHero而非 UseSkillCard。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
import { CardConfig, CardType } from "../common/config/CardSet";
import { HeroInfo } from "../common/config/heroSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FieldSkillType } from "../common/config/SkillSet";
import { FieldSkillHelper } from "../hero/FieldSkillHelper";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
/**
* CHeroComp —— 英雄卡池单个英雄卡项
*
* 由 MissionCardComp.cacheCardComps() 实例化。
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
*/
@ccclass('CHeroComp')
@ecs.register('CHeroComp', false)
export class CHeroComp extends CCComp {
private debugMode: boolean = true;
// ======================== 编辑器绑定节点 ========================
/** 英雄图标节点 */
@property({ type: Node })
private icon_node: Node = null;
/** 背景节点 */
@property({ type: Node })
private bg_node: Node = null;
/** 英雄名称 */
@property(Label)
private name_label: Label = null;
/** 描述词条 1 */
@property(Label)
private fied1: Label = null;
/** 描述词条 2 */
@property(Label)
private fied2: Label = null;
/** 描述词条 3 */
@property(Label)
private fied3: Label = null;
/** 等级标签 */
@property(Label)
private level_label: Label = null;
/** 购买按钮节点 */
@property({ type: Node })
private buy_node: Node = null;
// ======================== 运行时状态 ========================
/** 当前槽位承载的卡牌配置null 表示空槽 */
private cardData: CardConfig | null = null;
/** 是否已购买 */
private isPurchased: boolean = false;
/** 是否已缓存基准 Y/Z 坐标(首次 setSlotPosition 时确定) */
private hasFixedBasePosition: boolean = false;
/** 槽位基准 Y 坐标 */
private fixedBaseY: number = 0;
/** 槽位基准 Z 坐标 */
private fixedBaseZ: number = 0;
/** 静止位置(由 MissionCardComp 布局决定) */
private restPosition: Vec3 = new Vec3();
// ======================== 生命周期 ========================
onLoad() {
mLogger.log(this.debugMode, "CHeroComp", "onLoad bindings", {
nodeName: this.node?.name,
nodeUuid: this.node?.uuid,
hasCardData: !!this.cardData,
cardUuid: this.cardData?.uuid ?? null,
icon_node: !!this.icon_node,
bg_node: !!this.bg_node,
name_label: !!this.name_label,
level_label: !!this.level_label,
fied1: !!this.fied1,
fied2: !!this.fied2,
fied3: !!this.fied3,
buy_node: !!this.buy_node
});
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
// 补偿首次渲染addComponent 当帧 onLoad 尚未触发,
// 若 MissionCardComp 在 onLoad 前已 applyCardData此处绑定就绪后重刷一次
if (this.cardData) {
this.updateUI();
}
}
onDestroy() {
super.onDestroy();
if (this.buy_node && this.buy_node.isValid) {
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
}
}
// ======================== 数据初始化 ========================
/**
* 初始化英雄卡数据并渲染 UI
*
* @param data 卡牌配置,传 null 时清空显示
*/
applyCardData(data: CardConfig | null): void {
mLogger.log(this.debugMode, "CHeroComp", "applyCardData", {
nodeName: this.node?.name,
nodeUuid: this.node?.uuid,
uuid: data?.uuid ?? null,
type: data?.type ?? null,
name: data?.name ?? null,
card_lv: data?.card_lv ?? null,
hero_lv: data?.hero_lv ?? null,
cost: data?.cost ?? null,
icon: data?.icon ?? null,
nodeActiveBefore: this.node?.active
});
this.cardData = data;
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
}
}
/** 清空 UI 显示 */
private applyEmptyUI() {
if (this.name_label) this.name_label.string = "";
if (this.level_label) this.level_label.string = "";
const fieldLabels = [this.fied1, this.fied2, this.fied3];
for (const label of fieldLabels) {
if (label) {
label.string = "";
label.node.active = false;
}
}
if (this.icon_node) {
const sprite = this.icon_node.getComponent(Sprite) || this.icon_node.getComponentInChildren(Sprite);
if (sprite) sprite.spriteFrame = null;
}
if (this.buy_node) {
this.buy_node.active = false;
}
this.node.active = false;
}
// ======================== UI 渲染 ========================
updateUI() {
if (!this.cardData) {
mLogger.log(this.debugMode, "CHeroComp", "updateUI skip: no cardData", { nodeName: this.node?.name });
return;
}
// 动态升级卡SpecialUpgrade + target_hero_eid按英雄卡样式渲染
// 通过 eid 反查场上实体得到 hero_uuid实体不存在时降级为本卡 uuid
const isUpgradeCard = this.cardData.type === CardType.SpecialUpgrade && !!this.cardData.target_hero_eid;
const renderUuid = isUpgradeCard
? (this.queryHeroUuidByEid(this.cardData.target_hero_eid as number) ?? this.cardData.uuid)
: this.cardData.uuid;
const hero = HeroInfo[renderUuid];
mLogger.log(this.debugMode, "CHeroComp", "updateUI resolve", {
nodeName: this.node?.name,
uuid: this.cardData.uuid,
type: this.cardData.type,
isUpgradeCard,
renderUuid,
heroFound: !!hero,
heroName: hero?.name ?? null,
heroPath: hero?.path ?? null,
heroInfo: hero?.info ?? null
});
// 名称
if (this.name_label) {
const finalName = hero?.name || this.cardData.name || "";
this.name_label.string = finalName;
mLogger.log(this.debugMode, "CHeroComp", "name_label assigned", {
nodeName: this.node?.name,
finalName,
labelStringAfter: this.name_label.string,
labelNodeActive: this.name_label.node?.active,
labelNodeParentActive: this.name_label.node?.parent?.active,
labelNodeParentName: this.name_label.node?.parent?.name
});
}
// 等级标签(★ 表示卡牌等级;升级卡显示英雄目标等级)
if (this.level_label) {
if (isUpgradeCard) {
const heroLv = Math.max(1, this.cardData.hero_lv ?? 1);
this.level_label.string = heroLv > 1 ? `Lv.${heroLv - 1} -> Lv.${heroLv}` : `Lv.${heroLv}`;
} else {
const lv = this.cardData.card_lv ?? 1;
this.level_label.string = lv >= 2 ? "★".repeat(lv - 1) : "";
}
}
// 描述词条(最多显示 3 条,对应 fied1/fied2/fied3
const fieldLabels = [this.fied1, this.fied2, this.fied3];
const desc = hero?.info || this.cardData.info || "";
for (let i = 0; i < fieldLabels.length; i++) {
if (!fieldLabels[i]) continue;
fieldLabels[i].string = i === 0 ? desc : "";
fieldLabels[i].node.active = i === 0 && !!desc;
}
// 图标
if (this.icon_node) {
const iconId = this.getIconId(renderUuid);
if (iconId) {
this.updateIcon(this.icon_node, iconId);
}
}
// 购买按钮费用显示
if (this.buy_node) {
this.buy_node.active = true;
const costLabel = this.buy_node.getChildByName("num")?.getComponent(Label)
|| this.buy_node.getComponentInChildren(Label);
if (costLabel) {
costLabel.string = `${this.cardData.cost ?? 0}`;
}
}
}
/**
* 通过 eid 反查场上英雄实体的 hero_uuid。
* 用于动态升级卡渲染:升级卡绑定的是 eid但显示需要 HeroInfo[uuid]。
* @returns 找到则返回 hero_uuid找不到实体已销毁等返回 null
*/
private queryHeroUuidByEid(eid: number): number | null {
if (!eid) return null;
const entity = ecs.getEntityByEid(eid);
if (!entity) return null;
const model = entity.get(HeroAttrsComp);
if (!model) return null;
return model.hero_uuid ?? null;
}
/** 获取英雄图标 id优先卡牌自定义 icon其次英雄 path兜底 uuid */
private getIconId(renderUuid: number): string {
if (!this.cardData) return "";
if (this.cardData.icon) return this.cardData.icon;
const hero = HeroInfo[renderUuid];
return hero?.path || `${renderUuid}`;
}
/** 更新图标精灵 */
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
const frame = smc.uiconsAtlas ? smc.uiconsAtlas.getSpriteFrame(iconId) : null;
mLogger.log(this.debugMode, "CHeroComp", "updateIcon", {
nodeName: this.node?.name,
iconId,
spriteFound: !!sprite,
atlasReady: !!smc.uiconsAtlas,
frameFound: !!frame
});
if (!sprite) return;
if (smc.uiconsAtlas) {
sprite.spriteFrame = frame || null;
}
}
// ======================== 购买逻辑 ========================
/**
* 购买点击处理:
* 1. 英雄卡先通过 UseHeroCard 事件做数量上限校验guard 模式,可被取消)。
* 2. 扣除金币(失败则提示并中止)。
* 3. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。
* 4. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。
* 5. 标记已购买,隐藏购买按钮。
*/
private onBuyClick() {
if (!this.cardData || this.isPurchased) return;
oops.audio.playEffect("music/button");
// 英雄卡数量上限校验guard 模式MissionCardComp 可设 cancel=true 阻止使用
if (this.cardData.type === CardType.Hero) {
const guard = {
cancel: false,
reason: "",
uuid: this.cardData.uuid,
hero_lv: this.cardData.hero_lv ?? 1,
card_lv: this.cardData.base_pool_lv ?? this.cardData.pool_lv ?? 1
};
oops.message.dispatchEvent(GameEvent.UseHeroCard, guard);
if (guard.cancel) return;
}
// 扣费(英雄卡享受驻场"购买优惠"折扣)
let cost = this.cardData.cost ?? 0;
if (this.cardData.type === CardType.Hero) {
const discount = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.BuyDiscount);
cost = Math.max(0, cost - discount);
}
const success = MissionEconomy.spendCoin(Math.floor(cost));
if (!success) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
mLogger.log(this.debugMode, "CHeroComp", "purchase failed: not enough coin", {
uuid: this.cardData.uuid,
cost
});
return;
}
smc.vmdata.scores.refresh_hit_count++;
const used = this.cardData;
// 按卡牌类型分发效果事件
switch (used.type) {
case CardType.Hero:
oops.message.dispatchEvent(GameEvent.CallHero, used);
break;
case CardType.SpecialUpgrade:
case CardType.SpecialRefresh:
oops.message.dispatchEvent(GameEvent.UseSpecialCard, used);
break;
}
// 标记已购买
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
// 通知 MissionCardComp本槽位卡牌已使用刷新英雄卡池
oops.message.dispatchEvent(GameEvent.CardUsed, this);
mLogger.log(this.debugMode, "CHeroComp", "hero card purchased", {
uuid: used.uuid,
type: used.type,
cost
});
}
/** 标记为已购买状态 */
setPurchased(): void {
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
}
/**
* 接收 MissionCardComp 分发的卡牌并刷新显示。
* @param data 卡牌配置null 时清空显示
* @returns 是否成功接收true = 接收并刷新 UI
*/
applyDrawCard(data: CardConfig | null): boolean {
this.applyCardData(data);
return !!data;
}
/**
* 设置槽位的水平位置(由 MissionCardComp 布局计算后调用)。
* 首次调用时缓存基准 Y/Z后续仅更新 X。
* @param x 目标水平坐标
*/
setSlotPosition(x: number) {
const current = this.node.position;
if (!this.hasFixedBasePosition) {
this.fixedBaseY = current.y;
this.fixedBaseZ = current.z;
this.hasFixedBasePosition = true;
}
this.restPosition = new Vec3(x, this.fixedBaseY, this.fixedBaseZ);
this.node.setPosition(this.restPosition);
}
/**
* 系统清槽:任务开始/结束时强制重置。
* 清空数据与 UI 并隐藏节点。
*/
clearBySystem() {
this.cardData = null;
this.isPurchased = false;
this.node.setPosition(this.restPosition);
this.applyEmptyUI();
this.node.active = false;
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "62e81154-704c-4ce5-ab9d-19c12d539517",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -3,7 +3,7 @@
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
*
* 职责:
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CardComp 槽位。
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CHeroComp 槽位。
* 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡"
* 场上无英雄时仅抽取英雄卡和刷新卡。
* 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。
@@ -15,11 +15,11 @@
* 6. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
*
* 关键设计:
* - 3 个 CardComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[]
* - 3 个 CHeroComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[]
* 之后所有分发、清空操作均通过此数组进行。
* - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。
* - 英雄上限校验onUseHeroCard采用 guard/cancel 模式:
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
* CHeroComp 发出 UseHeroCard 事件并传入 guard 对象,
* 本组件可通过 guard.cancel=true 阻止使用。
*
* 历史:
@@ -28,7 +28,7 @@
* - 英雄升级仅通过升级卡SpecialUpgrade触发按 UUID 精确升级场上对应英雄。
*
* 依赖:
* - CardComp —— 单卡槽位
* - CHeroComp —— 单卡槽位
* - HInfoComp —— 英雄信息面板
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
* - HeroAttrsComp —— 英雄属性(升级)
@@ -43,7 +43,7 @@ import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCard
import { drawSkillCards } from "../common/config/SCardSet";
import { drawEquipCards } from "../common/config/EquipSet";
import { drawItemCards } from "../common/config/ICardSet";
import { CardComp } from "./CardComp";
import { CHeroComp } from "./CHeroComp";
import { SCardComp } from "./SCardComp";
import { EquipListComp } from "./EquipListComp";
import { oops } from "db://oops-framework/core/Oops";
@@ -188,8 +188,8 @@ export class MissionCardComp extends CCComp {
// ======================== 运行时状态 ========================
/** 三个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 三个槽位对应的 CHeroComp 控制器缓存(有序数组) */
private cardComps: CHeroComp[] = [];
/** 技能卡槽控制器缓存 */
private skillCardComps: SCardComp[] = [];
/** 是否已召唤过英雄(用于控制其他面板按钮是否可点击) */
@@ -237,7 +237,7 @@ export class MissionCardComp extends CCComp {
/**
* 组件加载:
* 1. 绑定生命周期事件和按钮交互事件。
* 2. 缓存 3 个 CardComp 子控制器引用。
* 2. 缓存 3 个 CHeroComp 子控制器引用。
* 3. 计算并设置槽位水平布局。
* 4. 初始化卡牌面板缩放参数。
* 5. 初始化面板轮播布局。
@@ -767,7 +767,7 @@ export class MissionCardComp extends CCComp {
}
/**
* 使用英雄卡的 guard 校验(由 CardComp 通过 UseHeroCard 事件调用):
* 使用英雄卡的 guard 校验(由 CHeroComp 通过 UseHeroCard 事件调用):
* - 当前英雄数 < 上限 → 允许使用。
* - 已满 → 阻止使用cancel=true弹 toast。
*
@@ -823,7 +823,7 @@ export class MissionCardComp extends CCComp {
/**
* 单张卡牌被成功使用后的整池刷新回调:
* - payload 为该卡槽组件实例CardComp 或 SCardComp
* - payload 为该卡槽组件实例CHeroComp 或 SCardComp
* - 命中英雄卡槽 → 重新构建 3 张英雄池卡牌并分发到所有英雄槽。
* - 命中技能卡槽 → 重新构建 3 张技能卡并分发到所有技能槽。
* - 未命中任何槽位 → 忽略。
@@ -831,7 +831,7 @@ export class MissionCardComp extends CCComp {
* Why: 购买一张卡后立即刷新整个卡池(不扣金币),让玩家持续从新池中挑选;
* 英雄卡池与技能卡池遵循同一逻辑。
* SpecialRefresh 自身效果onUseSpecialCard 中的 tryRefreshHeroCards
* 已是整池刷新,因此 CardComp 对其不派发 CardUsed避免双重刷新。
* 已是整池刷新,因此 CHeroComp 对其不派发 CardUsed避免双重刷新。
*/
private onCardUsed(event: string, args: any) {
const source = args;
@@ -1175,8 +1175,19 @@ export class MissionCardComp extends CCComp {
this.populateShopItems();
}
/** 将 cardPrefab 实例化为 3 个卡槽并映射为 CardComp形成固定顺序控制数组 */
/** 将 cardPrefab 实例化为 3 个卡槽并映射为 CHeroComp形成固定顺序控制数组(幂等:已缓存则不重复实例化) */
private cacheCardComps() {
// 幂等保护:卡槽已存在且节点有效时,仅刷新 skillCardComps 缓存,不重复实例化英雄卡槽
const hasValidHeroComps = this.cardComps.length === 3
&& this.cardComps.every(c => c && c.node && c.node.isValid);
if (hasValidHeroComps) {
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
this.skillCardComps = skillNodes
.map(node => node?.getComponent(SCardComp))
.filter((comp): comp is SCardComp => !!comp);
return;
}
// 清理旧卡槽(如果存在)
for (const comp of this.cardComps) {
if (comp && comp.node && comp.node.isValid) {
@@ -1190,7 +1201,7 @@ export class MissionCardComp extends CCComp {
for (let i = 0; i < 3; i++) {
const node = instantiate(this.cardPrefab);
this.cards_node.addChild(node);
const comp = node.getComponent(CardComp) || node.addComponent(CardComp);
const comp = node.getComponent(CHeroComp) || node.addComponent(CHeroComp);
this.cardComps.push(comp);
mLogger.log(this.debugMode, "MissionCardComp", "cacheCardComps instantiate", {
index: i,
@@ -1524,7 +1535,7 @@ export class MissionCardComp extends CCComp {
return undefined;
}
/** 全量分发给 3 槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
/** 全量分发给 3 槽;每个槽位是否接收由 CHeroComp 自己判断(锁定可跳过) */
private dispatchCardsToSlots(cards: CardConfig[]) {
if (!this.cardComps) return;
for (let i = 0; i < this.cardComps.length; i++) {
@@ -1851,8 +1862,8 @@ export class MissionCardComp extends CCComp {
this.resetButtonScale(this.cards_chou);
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.cardComps = [] as any;
this.skillCardComps = [] as any;
this.cardComps = [];
this.skillCardComps = [];
this.purchasedEquipUuids.clear();
this.purchasedItemUuids.clear();