移除了EquipListComp、SCardComp、CHeroComp、ItemListComp四个文件中的长按弹出信息框相关代码,包括事件监听、长按计时逻辑和信息框打开关闭代码,同时删除了未使用的UIID导入。
676 lines
25 KiB
TypeScript
676 lines
25 KiB
TypeScript
/**
|
||
* @file CHeroComp.ts
|
||
* @description 英雄卡池单个卡槽组件(UI 视图层 + 购买/召唤逻辑)
|
||
*
|
||
* 职责:
|
||
* 1. 渲染单个英雄卡的显示信息(名称、等级、描述、图标、费用),
|
||
* 展示方式与 SCardComp 一致(图集静态图标 + ★等级 + 描述词条)。
|
||
* 同时支持动态升级卡(SpecialUpgrade + target_hero_eid)按英雄卡样式渲染。
|
||
* 2. 处理购买点击 → UseHeroCard 上限校验(guard) → 扣费 → 派发 CallHero 召唤英雄。
|
||
* 特殊卡(升级/刷新)则派发 UseSpecialCard。
|
||
* 3. 使用成功后派发 CardUsed 事件,通知 MissionCardComp 刷新英雄卡池。
|
||
*
|
||
* 与 SCardComp 的差异:
|
||
* - 数据源为 HeroInfo(heroSet),而非 SkillCardList / SkillSet。
|
||
* - 购买前需通过 GameEvent.UseHeroCard 做英雄数量上限校验。
|
||
* - 英雄卡效果事件为 GameEvent.CallHero,而非 UseSkillCard。
|
||
*/
|
||
import { mLogger } from "../common/Logger";
|
||
import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity, SpriteFrame, resources, AnimationClip } 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;
|
||
|
||
/** 消费金币标签(未绑定时回退查找 buy_node 下的 num/Label 子节点) */
|
||
@property(Label)
|
||
private cost_label: Label = 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();
|
||
/** 透明度组件(用于淡入淡出) */
|
||
private opacityComp: UIOpacity | null = null;
|
||
/** 图标视觉令牌(异步加载竞态保护) */
|
||
private iconVisualToken: number = 0;
|
||
/** 当前显示的英雄 UUID(避免相同 UUID 重复加载动画) */
|
||
private iconHeroUuid: number = 0;
|
||
|
||
// ======================== 生命周期 ========================
|
||
|
||
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_START, this.onBuyTouchStart, 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 尚未触发,
|
||
// 若 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_START, this.onBuyTouchStart, this);
|
||
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
|
||
this.buy_node.off(NodeEventType.TOUCH_CANCEL, this.onBuyTouchCancel, this);
|
||
}
|
||
}
|
||
|
||
// ======================== 数据初始化 ========================
|
||
|
||
/**
|
||
* 初始化英雄卡数据并渲染 UI
|
||
*
|
||
* 若槽位已有旧卡且处于显示状态,先播放旧卡"逝去"动画(缩小淡出),
|
||
* 动画结束后再切换为新卡数据并播放入场动画。
|
||
* 首次赋值(node 未激活)直接渲染,无逝去动画。
|
||
*
|
||
* @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
|
||
});
|
||
|
||
// 刚购买(已播放大逝去动画)→ 不播普通逝去动画,直接应用新卡
|
||
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.isPurchased = false;
|
||
if (data) {
|
||
this.node.active = true;
|
||
this.updateUI();
|
||
} else {
|
||
this.applyEmptyUI();
|
||
}
|
||
}
|
||
|
||
/** 清空 UI 显示 */
|
||
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.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) : "";
|
||
}
|
||
}
|
||
|
||
// 背景:按 card_lv 激活对应颜色子节点(1→green 2→blue 3→purple 4→red 5→yellow)
|
||
this.updateBgNode();
|
||
|
||
// 描述词条(最多显示 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) {
|
||
this.iconVisualToken += 1;
|
||
this.updateHeroAnimation(this.icon_node, renderUuid, this.iconVisualToken);
|
||
}
|
||
|
||
// 购买按钮费用显示
|
||
if (this.buy_node) {
|
||
this.buy_node.active = true;
|
||
const costLabel = this.cost_label
|
||
|| this.buy_node.getChildByName("num")?.getComponent(Label)
|
||
|| this.buy_node.getComponentInChildren(Label);
|
||
if (costLabel) {
|
||
costLabel.string = `${this.cardData.cost ?? 0}`;
|
||
}
|
||
}
|
||
|
||
// 入场动画:从静止位置下方升起 + 淡入
|
||
this.playEnterAnim();
|
||
}
|
||
|
||
/**
|
||
* 根据 card_lv 切换 bg_node 下的颜色子节点显示。
|
||
* 等级与颜色对应关系:
|
||
* card_lv 1 → green
|
||
* card_lv 2 → blue
|
||
* card_lv 3 → purple
|
||
* card_lv 4 → red
|
||
* card_lv 5 → yellow
|
||
*/
|
||
private updateBgNode() {
|
||
if (!this.bg_node || !this.cardData) return;
|
||
const lvToColor: Record<number, string> = {
|
||
1: "green",
|
||
2: "blue",
|
||
3: "purple",
|
||
4: "red",
|
||
5: "yellow",
|
||
};
|
||
const targetColor = lvToColor[this.cardData.card_lv ?? 1];
|
||
this.bg_node.children.forEach(child => {
|
||
child.active = (child.name === targetColor);
|
||
});
|
||
}
|
||
|
||
// ======================== 动画效果 ========================
|
||
|
||
/** 入场动画时长(秒) */
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* 通过 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;
|
||
}
|
||
|
||
/**
|
||
* 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示。
|
||
* 与 HInfoComp 的 updateHeroAnimation 一致,零运行时开销。
|
||
* @param node 图标节点
|
||
* @param uuid 英雄 UUID
|
||
* @param token 视觉令牌
|
||
*/
|
||
private updateHeroAnimation(node: Node, uuid: number, token: number) {
|
||
if (!node || !node.isValid) return;
|
||
|
||
// 相同英雄 UUID 不重复加载,避免刷新卡池时闪烁
|
||
if (this.iconHeroUuid === uuid) return;
|
||
this.iconHeroUuid = uuid;
|
||
|
||
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||
if (sprite) sprite.spriteFrame = null;
|
||
|
||
const hero = HeroInfo[uuid];
|
||
if (!hero) {
|
||
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation hero not found", {
|
||
nodeName: this.node?.name,
|
||
uuid
|
||
});
|
||
return;
|
||
}
|
||
|
||
const path = `game/heros/hero/${hero.path}/idle`;
|
||
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation load", {
|
||
nodeName: this.node?.name,
|
||
uuid,
|
||
path,
|
||
token
|
||
});
|
||
|
||
resources.load(path, AnimationClip, (err, clip) => {
|
||
if (err || !clip) {
|
||
mLogger.log(this.debugMode, "CHeroComp", "updateHeroAnimation load failed", {
|
||
nodeName: this.node?.name,
|
||
uuid,
|
||
path,
|
||
message: err?.message ?? null
|
||
});
|
||
return;
|
||
}
|
||
// 竞态保护:仅允许最新一次图标请求落地
|
||
if (token !== this.iconVisualToken) return;
|
||
if (!node || !node.isValid) return;
|
||
const target = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
|
||
if (!target || !target.isValid) return;
|
||
|
||
// 帧动画数据存于 spriteFrame 轨道的 _channel._curve._values(裸 SpriteFrame 数组)
|
||
let firstFrame: SpriteFrame | undefined;
|
||
for (const track of clip.tracks) {
|
||
const curve = (track as unknown as { channel?: { curve?: { _values?: unknown[] } } }).channel?.curve;
|
||
const frame = curve?._values?.[0];
|
||
if (frame instanceof SpriteFrame) {
|
||
firstFrame = frame;
|
||
break;
|
||
}
|
||
}
|
||
if (firstFrame) {
|
||
target.spriteFrame = firstFrame;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ======================== 购买逻辑 ========================
|
||
|
||
/**
|
||
* 购买点击处理:
|
||
* 1. 播放按钮点击回弹动画。
|
||
* 2. 英雄卡先通过 UseHeroCard 事件做数量上限校验(guard 模式,可被取消)。
|
||
* 3. 扣除金币(失败则提示并中止)。
|
||
* 4. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。
|
||
* 5. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。
|
||
* 6. 标记已购买,隐藏购买按钮。
|
||
*/
|
||
private onBuyClick() {
|
||
if (!this.cardData || this.isPurchased) return;
|
||
|
||
// 播放按钮点击回弹动画(视觉反馈,不阻塞后续逻辑)
|
||
this.playBtnClickAnim();
|
||
|
||
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_card_lv ?? this.cardData.card_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;
|
||
}
|
||
|
||
// 播放购买动画(缩小淡出),刷新卡池由 CardUsed 事件触发
|
||
this.playPurchaseAnim();
|
||
|
||
// 标记已购买
|
||
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 并隐藏节点。
|
||
*
|
||
* 仅在 setSlotPosition 初始化过静止位置后才恢复位置,
|
||
* 否则 restPosition 为默认 (0,0,0),会把所有槽位重置到中心。
|
||
*/
|
||
clearBySystem() {
|
||
this.cardData = null;
|
||
this.isPurchased = false;
|
||
if (this.hasFixedBasePosition) {
|
||
this.node.setPosition(this.restPosition);
|
||
}
|
||
this.applyEmptyUI();
|
||
this.node.active = false;
|
||
}
|
||
|
||
/** ECS 组件移除时销毁节点 */
|
||
reset() {
|
||
if (this.node && this.node.isValid) {
|
||
this.node.destroy();
|
||
}
|
||
}
|
||
}
|