feat(mission): 新增装备和商店刷新功能,重构抽卡逻辑

1. 新增按权重抽取装备卡的工具函数,支持去重后补齐
2. 重构任务界面卡牌组件:
   - 拆分英雄/装备/商店面板节点绑定,调整属性顺序
   - 新增装备和商店刷新按钮的交互逻辑
   - 替换原固定加载装备/商品列表为按权重随机抽取3个
3. 重构技能卡牌组件,改为技能商店卡项实现,简化原有逻辑
4. 修复prefab中的节点属性偏移和引用关系
This commit is contained in:
panFD
2026-07-25 21:44:17 +08:00
parent 9c520cf917
commit ffafefcc72
4 changed files with 393 additions and 387 deletions

View File

@@ -41,8 +41,8 @@ import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/modu
import { GameEvent } from "../common/config/GameEvent";
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { drawSkillCards } from "../common/config/SCardSet";
import { EquipPoolList } from "../common/config/EquipSet";
import { ItemCardList, drawItemCards } from "../common/config/ICardSet";
import { drawEquipCards } from "../common/config/EquipSet";
import { drawItemCards } from "../common/config/ICardSet";
import { CardComp } from "./CardComp";
import { SCardComp } from "./SCardComp";
import { EquipListComp } from "./EquipListComp";
@@ -85,7 +85,9 @@ export class MissionCardComp extends CCComp {
// ======================== 编辑器绑定节点 ========================
/** 卡牌面板根节点(战斗阶段收起,准备阶段展开) */
/** 英雄卡牌面板根节点(战斗阶段收起,准备阶段展开) */
@property(Node)
showCards: Node = null!
@property(Node)
cards_node: Node = null!
/** 卡牌槽位 1 节点 */
@@ -97,36 +99,47 @@ export class MissionCardComp extends CCComp {
/** 卡牌槽位 3 节点 */
@property(Node)
card3: Node = null!
/** 卡牌槽位 4 节点 */
@property(Node)
card4: Node = null!
/** 抽卡(刷新)按钮节点 */
@property(Node)
cards_chou: Node = null!
@property(Node)
nock_node: Node = null!
/** 英雄卡牌池cards_node显示隐藏 */
/** 英雄面板cards_node显示隐藏 */
@property(Node)
showHeros: Node = null!
@property(Node)
closeHeros: Node = null!
@property(Node)
herosPanNode: Node = null!
@property(Node)
herosBoxNode: Node = null!
@property(Prefab)
heroPrefab: Prefab = null!
@property(Node)
showEquips: Node = null!
@property(Node)
closeEquips: Node = null!
@property(Node)
showSkills: Node = null!
@property(Node)
closeSkills: Node = null!
@property(Node)
equipsPanNode: Node = null!
@property(Node)
equipsBoxNode: Node = null!
@property(Prefab)
equipPrefab: Prefab = null!
@property(Node)
showSkills: Node = null!
@property(Node)
closeSkills: Node = null!
@property(Node)
skillPanNode: Node = null!
@property(Node)
skillBoxNode: Node = null!
@property(Prefab)
skillPrefab: Prefab = null!
@property(Node)
showShop: Node = null!
@property(Node)
@@ -138,6 +151,7 @@ export class MissionCardComp extends CCComp {
@property(Prefab)
itemPrefab: Prefab = null!
/** 卡池升级按钮节点(已废弃,保留节点引用防止 editor 报错) */
@property(Node)
cards_up: Node = null!
/** 金币显示节点(含 icon + num 子节点) */
@@ -171,6 +185,13 @@ export class MissionCardComp extends CCComp {
@property(Node)
skill_refresh_num_node: Node = null!
/** 装备刷新按钮节点 */
@property(Node)
equip_refresh: Node = null!;
/** 药品刷新按钮节点 */
@property(Node)
shop_refresh: Node = null!;
// ======================== 运行时状态 ========================
/** 三个槽位对应的 CardComp 控制器缓存(有序数组) */
@@ -413,6 +434,16 @@ export class MissionCardComp extends CCComp {
this.skill_ad_refresh?.on(NodeEventType.TOUCH_START, this.onSkillAdDrawTouchStart, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
/** 装备刷新按钮 */
this.equip_refresh?.on(NodeEventType.TOUCH_START, this.onEquipDrawTouchStart, this);
this.equip_refresh?.on(NodeEventType.TOUCH_END, this.onEquipDrawTouchEnd, this);
this.equip_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onEquipDrawTouchCancel, this);
/** 药品刷新按钮 */
this.shop_refresh?.on(NodeEventType.TOUCH_START, this.onShopDrawTouchStart, this);
this.shop_refresh?.on(NodeEventType.TOUCH_END, this.onShopDrawTouchEnd, this);
this.shop_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onShopDrawTouchCancel, this);
}
// ======================== 事件回调 ========================
@@ -565,7 +596,7 @@ export class MissionCardComp extends CCComp {
/**
* 填充商品列表:
* 从 ICardSet 的 ItemCardList 获取所有商品卡,
* 从 ICardSet 按权重抽取 3 张商品卡,
* 实例化 itemPrefab 到 shopBoxNode
* 每个商品项由 ItemListComp 渲染并处理购买。
*
@@ -578,8 +609,9 @@ export class MissionCardComp extends CCComp {
// 清空旧列表
this.shopBoxNode.removeAllChildren();
// 从商品卡池获取所有商品
for (const item of ItemCardList) {
// 从商品卡池按权重抽取 3 张
const items = drawItemCards(3);
for (const item of items) {
const node = instantiate(this.itemPrefab);
this.shopBoxNode.addChild(node);
const comp = node.getComponent(ItemListComp) || node.addComponent(ItemListComp);
@@ -592,7 +624,7 @@ export class MissionCardComp extends CCComp {
}
// 根据物品数量设置列表面板高度
this.resizeListBox(this.shopBoxNode, ItemCardList.length);
this.resizeListBox(this.shopBoxNode, items.length);
// 默认显示商店面板
if (this.shopPanNode) {
@@ -600,7 +632,7 @@ export class MissionCardComp extends CCComp {
}
mLogger.log(this.debugMode, "MissionCardComp", "populate shop items", {
count: ItemCardList.length
count: items.length
});
}
@@ -615,7 +647,7 @@ export class MissionCardComp extends CCComp {
if (!this.skillCardComps) return;
for (let i = 0; i < this.skillCardComps.length; i++) {
if (this.skillCardComps[i]) {
this.skillCardComps[i].applyDrawCard(cards[i] ?? null);
this.skillCardComps[i].applyCardData(cards[i] ?? null);
}
}
}
@@ -701,6 +733,16 @@ export class MissionCardComp extends CCComp {
if (this.closeShop && this.closeShop.isValid) {
this.closeShop.off(NodeEventType.TOUCH_END, this.onCloseShopClick, this);
}
if (this.equip_refresh && this.equip_refresh.isValid) {
this.equip_refresh.off(NodeEventType.TOUCH_START, this.onEquipDrawTouchStart, this);
this.equip_refresh.off(NodeEventType.TOUCH_END, this.onEquipDrawTouchEnd, this);
this.equip_refresh.off(NodeEventType.TOUCH_CANCEL, this.onEquipDrawTouchCancel, this);
}
if (this.shop_refresh && this.shop_refresh.isValid) {
this.shop_refresh.off(NodeEventType.TOUCH_START, this.onShopDrawTouchStart, this);
this.shop_refresh.off(NodeEventType.TOUCH_END, this.onShopDrawTouchEnd, this);
this.shop_refresh.off(NodeEventType.TOUCH_CANCEL, this.onShopDrawTouchCancel, this);
}
}
/**
@@ -1025,7 +1067,7 @@ export class MissionCardComp extends CCComp {
/**
* 填充装备列表:
* 从 CardPoolList 获取所有装备卡trigger_type=Field
* 从 EquipPoolList 按权重抽取 3 张装备卡
* 实例化 equipPrefab 到 equipsBoxNode
* 每个装备项由 EquipListComp 渲染并处理购买。
*
@@ -1037,8 +1079,9 @@ export class MissionCardComp extends CCComp {
// 清空旧列表
this.equipsBoxNode.removeAllChildren();
// 从独立装备卡池获取所有装备
for (const equip of EquipPoolList) {
// 从装备卡池按权重抽取 3 张
const equips = drawEquipCards(3);
for (const equip of equips) {
const node = instantiate(this.equipPrefab);
this.equipsBoxNode.addChild(node);
const comp = node.getComponent(EquipListComp) || node.addComponent(EquipListComp);
@@ -1051,7 +1094,7 @@ export class MissionCardComp extends CCComp {
}
// 根据物品数量设置列表面板高度
this.resizeListBox(this.equipsBoxNode, EquipPoolList.length);
this.resizeListBox(this.equipsBoxNode, equips.length);
// 默认显示装备面板
if (this.equipsPanNode) {
@@ -1059,7 +1102,7 @@ export class MissionCardComp extends CCComp {
}
mLogger.log(this.debugMode, "MissionCardComp", "populate equipments", {
count: EquipPoolList.length
count: equips.length
});
}
@@ -1110,11 +1153,54 @@ export class MissionCardComp extends CCComp {
this.dispatchCardsToSkillSlots(cards);
}
// ======================== 装备/药品刷新按钮回调 ========================
private onEquipDrawTouchStart() {
this.playButtonPressAnim(this.equip_refresh);
}
private onEquipDrawTouchEnd() {
oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.equip_refresh, () => this.onClickEquipRefresh());
}
private onEquipDrawTouchCancel() {
this.playButtonResetAnim(this.equip_refresh);
}
private onShopDrawTouchStart() {
this.playButtonPressAnim(this.shop_refresh);
}
private onShopDrawTouchEnd() {
oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.shop_refresh, () => this.onClickShopRefresh());
}
private onShopDrawTouchCancel() {
this.playButtonResetAnim(this.shop_refresh);
}
/** 装备刷新:扣费后重新抽取 3 张装备展示 */
private onClickEquipRefresh() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
this.showSmallTip("refresh_coin");
return;
}
this.populateEquipments();
}
/** 药品刷新:扣费后重新抽取 3 张商品展示 */
private onClickShopRefresh() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
this.showSmallTip("refresh_coin");
return;
}
this.populateShopItems();
}
/** 将三个卡槽节点映射为 CardComp形成固定顺序控制数组 */
private cacheCardComps() {
if (this.card4) {
this.card4.active = false;
}
const nodes = [this.card1, this.card2, this.card3];
this.cardComps = nodes
.map(node => node?.getComponent(CardComp))
@@ -1431,7 +1517,7 @@ export class MissionCardComp extends CCComp {
});
if (this.skillCardComps) {
this.skillCardComps.forEach(comp => {
if (comp) comp.clearBySystem();
if (comp) comp.applyCardData(null);
});
}
}
@@ -1502,6 +1588,30 @@ export class MissionCardComp extends CCComp {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
if (this.equip_refresh) {
const nobg = this.equip_refresh.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
const coinNode = this.equip_refresh.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
if (this.shop_refresh) {
const nobg = this.shop_refresh.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
const coinNode = this.shop_refresh.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
}
private updateCoinAndCostUI() {

View File

@@ -1,392 +1,198 @@
/**
* @file SCardComp.ts
* @description 技能卡牌槽位组件UI 视图层)
* @description 技能商店单个技能卡项组件UI 视图层 + 购买逻辑
*
* 职责:
* 1. 管理技能卡牌槽位的显示和交互(点击使用)。
* 2. 渲染技能卡面
* 3. 触发使用时扣除费用并分发 UseSkillCard 事件。
* 1. 渲染单个技能卡的显示信息(名称、等级、描述词条、图标)。
* 2. 处理购买点击 → 扣费 → 派发 UseSkillCard 事件触发技能生效
*
* 技能使用机制:
* 购买后派发 GameEvent.UseSkillCard
* 由 MissSkillsComp 接收并创建 SBox 实体 / SkillBoxComp 执行技能逻辑。
*/
import { mLogger } from "../common/Logger";
import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, UITransform, Widget } from "cc";
import { _decorator, Node, Sprite, Label, NodeEventType } 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, CKind } from "../common/config/CardSet";
import { CardConfig } from "../common/config/CardSet";
import { SkillCardList } from "../common/config/SCardSet";
import { CardBgComp } from "./CardBgComp";
import { FieldSkillSet, SkillSet } from "../common/config/SkillSet";
import { GameEvent } from "../common/config/GameEvent";
import { FieldSkillSet, SkillSet, SkillConfig } from "../common/config/SkillSet";
import { oops } from "db://oops-framework/core/Oops";
import { GameEvent } from "../common/config/GameEvent";
import { smc } from "../common/SingletonModuleComp";
import { MissionEconomy } from "./MissionEconomy";
const { ccclass, property } = _decorator;
/**
* SCardComp —— 技能商店单个技能卡项
*
* 由 MissionCardComp.dispatchCardsToSkillSlots() 实例化。
* 以图标 + 名称 + 描述词条 + 购买按钮的形式呈现。
*/
@ccclass('SCardComp')
@ecs.register('SCardComp', false)
export class SCardComp extends CCComp {
private debugMode: boolean = true;
private debugMode: boolean = false;
@property(Node)
name_node: Node = null!
@property(Node)
icon_node: Node = null!
@property(Node)
cost_node: Node = null!
@property(Node)
Ckind_node: Node = null!
@property(Node)
BG_node: Node = null!
@property(Node)
info_node: Node = null!
// ======================== 编辑器绑定节点 ========================
card_cost: number = 0
card_type: CardType = CardType.Skill
card_uuid: number = 0
/** 技能图标节点 */
@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;
// ======================== 运行时状态 ========================
/** 当前技能的卡牌配置 */
private cardData: CardConfig | null = null;
private touchStartY: number = 0;
private touchStartX: number = 0;
private isDragging: boolean = false;
private isUsing: boolean = false;
private restPosition: Vec3 = new Vec3();
private hasFixedBasePosition: boolean = false;
private fixedBaseY: number = 0;
private fixedBaseZ: number = 0;
private opacityComp: UIOpacity | null = null;
private iconVisualToken: number = 0;
/** 是否已购买 */
private isPurchased: boolean = false;
// ======================== 生命周期 ========================
onLoad() {
this.bindEvents();
this.restPosition = this.node.position.clone();
this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity);
this.opacityComp.opacity = 255;
this.applyEmptyUI();
this.buy_node?.on(NodeEventType.TOUCH_END, this.onBuyClick, this);
}
onDestroy() {
super.onDestroy();
this.unbindEvents();
if (this.buy_node && this.buy_node.isValid) {
this.buy_node.off(NodeEventType.TOUCH_END, this.onBuyClick, this);
}
}
init() { }
start() {
this.node.active = true;
}
// ======================== 数据初始化 ========================
applyDrawCard(data: CardConfig | null): boolean {
if (!data) return false;
/**
* 初始化技能卡数据并渲染 UI
*
* @param data 技能卡配置,传 null 时清空显示
*/
applyCardData(data: CardConfig | null): void {
this.cardData = data;
this.card_uuid = data.uuid;
this.card_type = data.type;
this.card_cost = Math.floor(data.cost ?? 0);
this.node.active = true;
this.applyCardUI();
this.playRefreshAnim();
mLogger.log(this.debugMode, "SCardComp", "skill card updated", {
uuid: this.card_uuid,
cost: this.card_cost
});
return true;
}
useCard(): CardConfig | null {
if (!this.cardData || this.isUsing) return null;
const cardCost = this.card_cost;
const success = MissionEconomy.spendCoin(cardCost);
if (!success) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
this.playReboundAnim();
return null;
}
smc.vmdata.scores.refresh_hit_count++;
this.isUsing = true;
const used = this.cardData;
this.playUseDisappearAnim(() => {
this.clearAfterUse();
this.isUsing = false;
oops.message.dispatchEvent(GameEvent.UseSkillCard, used);
// 派发 CardUsed 让 MissionCardComp 刷新整个技能卡池
oops.message.dispatchEvent(GameEvent.CardUsed, this);
});
return used;
}
hasCard(): boolean {
return !!this.cardData;
}
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);
if (!this.isDragging && !this.isUsing) {
this.node.setPosition(this.restPosition);
}
}
clearBySystem() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.isDragging = false;
this.isUsing = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.applyEmptyUI();
this.node.active = false;
}
private clearAfterUse() {
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
}
this.cardData = null;
this.card_uuid = 0;
this.card_cost = 0;
this.isDragging = false;
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(1, 1, 1));
this.applyEmptyUI();
this.node.active = false;
}
private bindEvents() {
this.node.on(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.on(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.on(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.on(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
}
private unbindEvents() {
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_START, this.onCardTouchStart, this);
this.node.off(NodeEventType.TOUCH_MOVE, this.onCardTouchMove, this);
this.node.off(NodeEventType.TOUCH_END, this.onCardTouchEnd, this);
this.node.off(NodeEventType.TOUCH_CANCEL, this.onCardTouchCancel, this);
}
}
private onCardTouchStart(event: EventTouch) {
if (!this.cardData || this.isUsing) return;
this.touchStartY = event.getUILocation().y;
this.touchStartX = event.getUILocation().x;
this.isDragging = true;
}
private onCardTouchMove(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
// 技能卡不支持上拉移动
}
private onCardTouchEnd(event: EventTouch) {
if (!this.isDragging || !this.cardData || this.isUsing) return;
const endY = event.getUILocation().y;
const endX = event.getUILocation().x;
const deltaY = endY - this.touchStartY;
const deltaX = endX - this.touchStartX;
this.isDragging = false;
// 点击触发
if (Math.abs(deltaY) < 20 && Math.abs(deltaX) < 20) {
const used = this.useCard();
if (!used) {
this.playReboundAnim();
}
return;
}
this.playReboundAnim();
}
private onCardTouchCancel() {
if (!this.isDragging || this.isUsing) return;
this.isDragging = false;
this.playReboundAnim();
}
private applyCardUI() {
if (!this.cardData) {
this.isPurchased = false;
if (data) {
this.node.active = true;
this.updateUI();
} else {
this.applyEmptyUI();
return;
}
this.iconVisualToken += 1;
if (this.opacityComp) this.opacityComp.opacity = 255;
this.node.setPosition(this.restPosition.x, this.restPosition.y, this.restPosition.z);
const kindName = CKind[this.cardData.kind];
if (this.BG_node) {
const bgLv = this.cardData.base_pool_lv ?? this.cardData.pool_lv;
this.BG_node.children.forEach(child => {
child.active = (child.name === kindName);
const bg = child.getComponent(CardBgComp);
if (bg) child.active ? bg.apply(bgLv) : bg.clear();
});
}
if (this.Ckind_node) {
this.Ckind_node.children.forEach(child => {
child.active = (child.name === kindName);
});
}
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
child.children.forEach(subChild => {
const subWidget = subChild.getComponent(Widget);
if (subWidget) subWidget.updateAlignment();
});
});
const s_uuid = this.cardData.skill ?? this.card_uuid;
const skill = SkillSet[s_uuid];
const skillCard = SkillCardList.find(c => c.uuid === this.card_uuid);
const card_lv = Math.max(1, Math.floor(this.cardData.card_lv ?? 1));
const spSuffix = card_lv >= 2 ? "★".repeat(card_lv - 1) : "";
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
if (this.info_node) {
this.info_node.active = true;
// 驻场技能卡描述取 FieldSkillSet其他卡牌取卡牌/技能配置
let desc = "";
if (this.cardData.field && this.cardData.field.length > 0) {
desc = FieldSkillSet[this.cardData.field[0]]?.info || "";
}
if (!desc) {
desc = skillCard?.info || skill?.info || this.cardData?.info || "";
}
// 与 HlistComp 保持一致:优先查找名为 "info" 的子节点上的 Label
const infoLabel = this.info_node.getChildByName("info")?.getComponent(Label)
|| this.info_node.getComponent(Label)
|| this.info_node.getComponentInChildren(Label);
if (infoLabel) infoLabel.string = desc;
}
if (this.cost_node) {
this.cost_node.active = true;
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, `${this.card_cost}`);
}
}
const iconNode = this.icon_node as Node;
if (iconNode) {
iconNode.setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(iconNode);
// 驻场技能卡(skill=undefined 但有 field)使用 FieldSkillSet 中的图标
let iconId: string;
if (!this.cardData.skill && this.cardData.field && this.cardData.field.length > 0) {
const fieldUuid = this.cardData.field[0];
iconId = FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
} else {
iconId = skill?.icon || `${s_uuid}`;
}
this.updateIcon(iconNode, iconId);
}
}
private playRefreshAnim() {
Tween.stopAllByTarget(this.node);
this.node.setPosition(this.restPosition);
this.node.setScale(new Vec3(0.92, 0.92, 1));
tween(this.node)
.to(0.08, { scale: new Vec3(1.06, 1.06, 1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.start();
}
private playReboundAnim() {
Tween.stopAllByTarget(this.node);
tween(this.node)
.to(0.12, {
position: new Vec3(this.restPosition.x, this.restPosition.y, this.restPosition.z),
scale: new Vec3(1, 1, 1)
})
.start();
}
private playUseDisappearAnim(onComplete: () => void) {
const targetPos = new Vec3(this.restPosition.x, this.restPosition.y + 120, this.restPosition.z);
Tween.stopAllByTarget(this.node);
if (this.opacityComp) {
Tween.stopAllByTarget(this.opacityComp);
this.opacityComp.opacity = 255;
tween(this.opacityComp)
.to(0.18, { opacity: 0 })
.start();
}
tween(this.node)
.to(0.18, {
position: targetPos,
scale: new Vec3(0.8, 0.8, 1)
})
.call(onComplete)
.start();
}
/** 清空 UI 显示 */
private applyEmptyUI() {
if (this.BG_node) {
this.BG_node.children.forEach(child => {
child.active = false;
const bg = child.getComponent(CardBgComp);
if (bg) bg.clear();
});
}
this.node.children.forEach(child => {
const widget = child.getComponent(Widget);
if (widget) widget.updateAlignment();
child.children.forEach(subChild => {
const subWidget = subChild.getComponent(Widget);
if (subWidget) subWidget.updateAlignment();
});
});
this.iconVisualToken += 1;
this.setLabel(this.name_node, "");
if (this.cost_node) {
const numNode = this.cost_node.getChildByName("num");
if (numNode) {
this.setLabel(numNode, "");
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;
}
this.cost_node.active = false;
}
if (this.Ckind_node) {
this.Ckind_node.children.forEach(child => {
child.active = false;
});
}
if (this.info_node) this.info_node.active = false;
if (this.icon_node) {
(this.icon_node as Node).setScale(new Vec3(1, 1, 1));
this.clearIconAnimation(this.icon_node as 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;
}
private setLabel(node: Node | null, value: string) {
if (!node) return;
const label = node.getComponent(Label) || node.getComponentInChildren(Label);
if (label) label.string = value;
// ======================== UI 渲染 ========================
updateUI() {
if (!this.cardData) return;
const skillCard = SkillCardList.find(c => c.uuid === this.cardData!.uuid);
const skill = this.cardData.skill ? SkillSet[this.cardData.skill] : null;
// 名称
if (this.name_label) {
this.name_label.string = skillCard?.name || skill?.name || this.cardData.name || "";
}
// 等级标签(★ 表示卡牌等级)
if (this.level_label) {
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 = this.getDescription(skillCard, skill);
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(skill);
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}`;
}
}
}
/** 获取技能描述:驻场技能取 FieldSkillSet其余取技能/卡牌配置 */
private getDescription(skillCard: CardConfig | undefined, skill: SkillConfig | null): string {
if (!this.cardData) return "";
if (this.cardData.field && this.cardData.field.length > 0) {
return FieldSkillSet[this.cardData.field[0]]?.info || "";
}
return skillCard?.info || skill?.info || this.cardData.info || "";
}
/** 获取技能图标 id驻场技能取 FieldSkillSet.icon其余取 SkillSet.icon */
private getIconId(skill: SkillConfig | null): string {
if (!this.cardData) return "";
if (!this.cardData.skill && this.cardData.field && this.cardData.field.length > 0) {
const fieldUuid = this.cardData.field[0];
return FieldSkillSet[fieldUuid]?.icon || `${fieldUuid}`;
}
return skill?.icon || `${this.cardData.skill ?? this.cardData.uuid}`;
}
/** 更新图标精灵 */
private updateIcon(node: Node, iconId: string) {
if (!node || !iconId) return;
const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite);
@@ -397,17 +203,60 @@ export class SCardComp extends CCComp {
}
}
private clearIconAnimation(node: Node) {
const anim = node?.getComponent(Animation);
if (!anim) return;
anim.stop();
const clips = (anim as any).clips as AnimationClip[] | undefined;
if (!clips || clips.length === 0) return;
[...clips].forEach(clip => anim.removeClip(clip, true));
// ======================== 购买逻辑 ========================
/**
* 购买点击处理:
* 1. 扣除金币(失败则提示并中止)。
* 2. 派发 UseSkillCard 事件,触发技能生效流程。
* 3. 标记已购买,隐藏购买按钮。
*/
private onBuyClick() {
if (!this.cardData || this.isPurchased) return;
oops.audio.playEffect("music/button");
// 扣费
const cost = this.cardData.cost ?? 0;
const success = MissionEconomy.spendCoin(cost);
if (!success) {
oops.message.dispatchEvent(GameEvent.ShowSmallTip, "buy_coin");
mLogger.log(this.debugMode, "SCardComp", "purchase failed: not enough coin", {
uuid: this.cardData.uuid,
cost
});
return;
}
smc.vmdata.scores.refresh_hit_count++;
// 派发 UseSkillCard技能生效流程由 MissSkillsComp 接收处理
oops.message.dispatchEvent(GameEvent.UseSkillCard, this.cardData);
// 标记已购买
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
mLogger.log(this.debugMode, "SCardComp", "skill card purchased", {
uuid: this.cardData.uuid,
cost
});
}
/** ECS 组件移除时的释放钩子:销毁节点 */
/** 标记为已购买状态 */
setPurchased(): void {
this.isPurchased = true;
if (this.buy_node) {
this.buy_node.active = false;
}
}
/** ECS 组件移除时销毁节点 */
reset() {
this.node.destroy();
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}