From ffafefcc720b7fcded25845748597060a42ea57a Mon Sep 17 00:00:00 2001 From: panFD Date: Sat, 25 Jul 2026 21:44:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(mission):=20=E6=96=B0=E5=A2=9E=E8=A3=85?= =?UTF-8?q?=E5=A4=87=E5=92=8C=E5=95=86=E5=BA=97=E5=88=B7=E6=96=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E9=87=8D=E6=9E=84=E6=8A=BD=E5=8D=A1=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增按权重抽取装备卡的工具函数,支持去重后补齐 2. 重构任务界面卡牌组件: - 拆分英雄/装备/商店面板节点绑定,调整属性顺序 - 新增装备和商店刷新按钮的交互逻辑 - 替换原固定加载装备/商品列表为按权重随机抽取3个 3. 重构技能卡牌组件,改为技能商店卡项实现,简化原有逻辑 4. 修复prefab中的节点属性偏移和引用关系 --- assets/resources/gui/element/mission.prefab | 25 +- assets/script/game/common/config/EquipSet.ts | 40 ++ assets/script/game/map/MissionCardComp.ts | 164 +++++- assets/script/game/map/SCardComp.ts | 551 +++++++------------ 4 files changed, 393 insertions(+), 387 deletions(-) diff --git a/assets/resources/gui/element/mission.prefab b/assets/resources/gui/element/mission.prefab index cd044585..5fada362 100644 --- a/assets/resources/gui/element/mission.prefab +++ b/assets/resources/gui/element/mission.prefab @@ -7949,7 +7949,7 @@ "propertyPath": [ "_left" ], - "value": 480 + "value": 480.0000000000002 }, { "__type__": "cc.TargetInfo", @@ -7965,7 +7965,7 @@ "propertyPath": [ "_right" ], - "value": 10 + "value": 9.999999999999773 }, { "__type__": "CCPropertyOverrideInfo", @@ -20978,20 +20978,20 @@ "__id__": 853 }, "closeEquips": null, - "showSkills": { - "__id__": 885 - }, - "closeSkills": null, "equipsPanNode": { "__id__": 315 }, "equipsBoxNode": { - "__id__": 374 + "__id__": 400 }, "equipPrefab": { "__uuid__": "f30f91b1-3100-4d9e-befb-cdde5f86e6f8", "__expectedType__": "cc.Prefab" }, + "showSkills": { + "__id__": 885 + }, + "closeSkills": null, "showShop": { "__id__": 914 }, @@ -20999,8 +20999,13 @@ "shopPanNode": { "__id__": 637 }, - "shopBoxNode": null, - "itemPrefab": null, + "shopBoxNode": { + "__id__": 678 + }, + "itemPrefab": { + "__uuid__": "abae16b3-c2a2-4971-95b1-fc121f769554", + "__expectedType__": "cc.Prefab" + }, "cards_up": null, "coins_node": { "__id__": 129 @@ -21020,6 +21025,8 @@ }, "skill_ad_refresh": null, "skill_refresh_num_node": null, + "equip_refresh": null, + "shop_refresh": null, "_id": "" }, { diff --git a/assets/script/game/common/config/EquipSet.ts b/assets/script/game/common/config/EquipSet.ts index e513344a..6b79afdf 100644 --- a/assets/script/game/common/config/EquipSet.ts +++ b/assets/script/game/common/config/EquipSet.ts @@ -116,6 +116,46 @@ export const EquipPoolList: CardConfig[] = EquipRawList.map(data => ({ trigger_type: CardTriggerType.Field, })); +/** + * 按权重抽取 N 张装备卡(unique 保证一次刷新内不重复,不足时允许重复补齐)。 + * @param count 需要抽取的数量 + */ +export function drawEquipCards(count: number): CardConfig[] { + const safeCount = Math.max(0, Math.floor(count)); + if (EquipPoolList.length === 0 || safeCount <= 0) return []; + + const picked: CardConfig[] = []; + let available = [...EquipPoolList]; + while (picked.length < safeCount) { + if (available.length === 0) break; + const pick = weightedPick(available); + if (!pick) break; + picked.push(pick); + available = available.filter(c => c.uuid !== pick.uuid); + } + + // 不足时允许重复补齐 + const filled = [...picked]; + while (filled.length < safeCount) { + const fallback = weightedPick(EquipPoolList); + if (!fallback) break; + filled.push(fallback); + } + return filled; +} + +/** 单次按权重抽取一张卡 */ +function weightedPick(cards: CardConfig[]): CardConfig | null { + if (cards.length === 0) return null; + const totalWeight = cards.reduce((total, card) => total + (card.weight ?? 0), 0); + let random = Math.random() * totalWeight; + for (const card of cards) { + random -= (card.weight ?? 0); + if (random <= 0) return card; + } + return cards[cards.length - 1]; +} + /** * 按 UUID 查找装备卡配置 * @param uuid 装备卡 UUID diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index c87b70a9..576f18f3 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -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() { diff --git a/assets/script/game/map/SCardComp.ts b/assets/script/game/map/SCardComp.ts index 30898d28..3b94d323 100644 --- a/assets/script/game/map/SCardComp.ts +++ b/assets/script/game/map/SCardComp.ts @@ -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(); + } } }