Files
pixelheros/assets/script/game/map/CHeroComp.ts
panFD 16996b9805 refactor(mission): 重构卡牌系统为英雄卡槽组件
1. 新增CHeroComp作为英雄卡槽专用组件,替换原CardComp
2. 调整mission.prefab布局与预制体引用,适配新卡槽组件
3. 优化MissionCardComp的卡槽缓存与布局逻辑
4. 更新所有相关注释与类型引用,统一代码语义
2026-07-26 12:52:58 +08:00

425 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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();
}
}
}