import { mLogger } from "../common/Logger"; import { _decorator, Animation, AnimationClip, EventTouch, Label, Node, NodeEventType, Sprite, SpriteAtlas, Tween, tween, UIOpacity, Vec3, resources } 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, SpecialCardList } from "../common/config/CardSet"; import { CardUseComp } from "./CardUseComp"; import { HeroInfo } from "../common/config/heroSet"; import { SkillSet } from "../common/config/SkillSet"; import { GameEvent } from "../common/config/GameEvent"; import { oops } from "db://oops-framework/core/Oops"; import { smc } from "../common/SingletonModuleComp"; import { UIID } from "../common/config/GameUIConfig"; const { ccclass, property } = _decorator; /** 视图层对象 */ @ccclass('CardComp') @ecs.register('CardComp', false) export class CardComp extends CCComp { private debugMode: boolean = true; /** 锁定态图标节点(显示时表示本槽位锁定) */ @property(Node) Lock: Node = null! @property(Node) unLock: Node = null! @property(Node) info_node=null! @property(Node) oinfo_node=null! @property(Node) name_node=null! @property(Node) icon_node=null! @property(Node) cost_node=null! card_cost:number=0 card_type:CardType=CardType.Hero card_uuid:number=0 /** 是否处于锁定状态(锁定且有卡时,抽卡分发会被跳过) */ private isLocked: boolean = false; /** 图标图集缓存(后续接图标资源时直接复用) */ private uiconsAtlas: SpriteAtlas | null = null; /** 当前槽位承载的卡牌数据,null 表示空槽 */ private cardData: CardConfig | null = null; private readonly dragUseThreshold: number = 70; private touchStartY: number = 0; private isDragging: boolean = false; private isUsing: boolean = false; private restPosition: Vec3 = new Vec3(); private opacityComp: UIOpacity | null = null; private cardUseComp: CardUseComp | null = null; private iconVisualToken: number = 0; onLoad() { /** 初始阶段只做UI状态准备,不触发业务逻辑 */ this.bindEvents(); this.restPosition = this.node.position.clone(); this.opacityComp = this.node.getComponent(UIOpacity) || this.node.addComponent(UIOpacity); this.cardUseComp = this.resolveCardUseComp(); this.opacityComp.opacity = 255; this.updateLockUI(); this.applyEmptyUI(); } onDestroy() { this.unbindEvents(); } init(){ this.onMissionStart(); } /** 游戏开始初始化 */ onMissionStart() { } /** 游戏结束清理 */ onMissionEnd() { } start() { /** 单卡节点常驻,由数据控制显示内容 */ this.node.active = true; } /** 兼容旧接口:外部通过该入口更新卡牌 */ updateCardInfo(card:Node, data: CardConfig){ this.applyDrawCard(data); } private updateIcon(node: Node, iconId: string) { if (!node || !iconId) return; const sprite = node.getComponent(Sprite) || node.getComponentInChildren(Sprite); if (!sprite) return; if (this.uiconsAtlas) { const frame = this.uiconsAtlas.getSpriteFrame(iconId); if (frame) { sprite.spriteFrame = frame; } else { sprite.spriteFrame = null; } return; } resources.load("gui/uicons", SpriteAtlas, (err, atlas) => { if (err || !atlas) { mLogger.log(this.debugMode, "CardComp", "load uicons atlas failed", err); return; } this.uiconsAtlas = atlas; const frame = atlas.getSpriteFrame(iconId); if (frame) { sprite.spriteFrame = frame; } else { sprite.spriteFrame = null; } }); } /** 兼容旧接口:按索引更新卡牌(当前由 MissionCardComp 顺序分发) */ updateCardData(index: number, data: CardConfig) { this.applyDrawCard(data); } /** 兼容按钮回调入口:触发单卡使用 */ selectCard(e: any, index: string) { this.useCard(); } /** * 关闭界面 */ close() { } /** 抽卡分发入口:返回 true 表示本次已成功接收新卡 */ applyDrawCard(data: CardConfig | null): boolean { if (!data) return false; /** 锁定且已有旧卡时,跳过本次刷新,保持老卡 */ if (this.isLocked && this.cardData) { mLogger.log(this.debugMode, "CardComp", "slot locked, skip update", this.card_uuid); return false; } this.cardData = data; this.card_uuid = data.uuid; this.card_type = data.type; this.card_cost = data.cost; this.node.active = true; this.applyCardUI(); this.playRefreshAnim(); mLogger.log(this.debugMode, "CardComp", "card updated", { uuid: this.card_uuid, type: this.card_type, cost: this.card_cost }); return true; } useCard(): CardConfig | null { if (!this.cardData || this.isUsing) return null; const cardCost = Math.max(0, Math.floor(this.cardData.cost ?? 0)); const currentCoin = this.getMissionCoin(); if (currentCoin < cardCost) { oops.gui.toast(`金币不足,召唤需要${cardCost}`); this.playReboundAnim(); mLogger.log(this.debugMode, "CardComp", "use card coin not enough", { uuid: this.cardData.uuid, type: this.cardData.type, cardCost, currentCoin }); return null; } if (this.cardData.type === CardType.Hero) { const guard = { cancel: false, reason: "", uuid: this.cardData.uuid, hero_lv: this.cardData.hero_lv }; oops.message.dispatchEvent(GameEvent.UseHeroCard, guard); if (guard.cancel) { this.playReboundAnim(); return null; } } this.setMissionCoin(currentCoin - cardCost); oops.message.dispatchEvent(GameEvent.CoinAdd, { syncOnly: true, delta: -cardCost }); this.isUsing = true; const used = this.cardData; mLogger.log(this.debugMode, "CardComp", "use card", { uuid: used.uuid, type: used.type, cost: cardCost, leftCoin: this.getMissionCoin() }); this.playUseDisappearAnim(() => { this.cardUseComp?.onCardUsed(used); this.clearAfterUse(); this.isUsing = false; }); return used; } /** 查询槽位是否有卡 */ hasCard(): boolean { return !!this.cardData; } /** 外部设置锁定态 */ setLocked(value: boolean) { this.isLocked = value; this.updateLockUI(); } /** 外部读取当前锁定态 */ isSlotLocked(): boolean { return this.isLocked; } setSlotPosition(x: number) { const current = this.node.position; this.restPosition = new Vec3(x, current.y, current.z); 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.card_type = CardType.Hero; this.isLocked = false; this.isDragging = false; this.isUsing = false; this.node.setPosition(this.restPosition); this.node.setScale(new Vec3(1, 1, 1)); this.updateLockUI(); 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.card_type = CardType.Hero; this.isLocked = false; this.isDragging = false; this.node.setPosition(this.restPosition); this.node.setScale(new Vec3(1, 1, 1)); this.updateLockUI(); 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); this.Lock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this); this.unLock?.on(NodeEventType.TOUCH_END, this.onToggleLock, this); } /** 解绑触控,防止节点销毁后残留回调 */ private unbindEvents() { 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); this.Lock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this); this.unLock?.off(NodeEventType.TOUCH_END, this.onToggleLock, this); } private onCardTouchStart(event: EventTouch) { if (!this.cardData || this.isUsing) return; this.touchStartY = event.getUILocation().y; this.isDragging = true; } private onCardTouchMove(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; const currentY = event.getUILocation().y; const deltaY = Math.max(0, currentY - this.touchStartY); this.node.setPosition(this.restPosition.x, this.restPosition.y + deltaY, this.restPosition.z); } /** 上拉超过阈值才视为使用,否则回弹原位 */ private onCardTouchEnd(event: EventTouch) { if (!this.isDragging || !this.cardData || this.isUsing) return; const endY = event.getUILocation().y; const deltaY = endY - this.touchStartY; this.isDragging = false; if (deltaY >= this.dragUseThreshold) { this.useCard(); return; } this.openHeroInfoIBox(); this.playReboundAnim(); } private onCardTouchCancel() { if (!this.isDragging || this.isUsing) return; this.isDragging = false; this.playReboundAnim(); } /** 点击锁控件:切换锁态;空槽不允许锁定 */ private onToggleLock(event?: EventTouch) { if (!this.cardData) return; this.isLocked = !this.isLocked; this.updateLockUI(); mLogger.log(this.debugMode, "CardComp", "toggle lock", { uuid: this.card_uuid, locked: this.isLocked }); const stopPropagation = (event as any)?.stopPropagation; if (typeof stopPropagation === "function") { stopPropagation.call(event); } } /** 根据锁态刷新 Lock / unLock 显示(Lock=可点击上锁,unLock=可点击解锁) */ private updateLockUI() { if (this.Lock) this.Lock.active = !this.isLocked; if (this.unLock) this.unLock.active = this.isLocked; } /** 根据当前 cardData 渲染卡面文字与图标 */ private applyCardUI() { if (!this.cardData) { this.applyEmptyUI(); return; } this.iconVisualToken += 1; if (this.opacityComp) this.opacityComp.opacity = 255; this.node.setPosition(this.restPosition); if(this.card_type===CardType.Hero){ this.setLabel(this.name_node, `${HeroInfo[this.card_uuid].name}Lv.${this.cardData.hero_lv}`); this.info_node.active = true; this.oinfo_node.active = false; this.info_node.getChildByName("ap").getChildByName("val").getComponent(Label).string = `${HeroInfo[this.card_uuid].ap*this.cardData.hero_lv}`; this.info_node.getChildByName("hp").getChildByName("val").getComponent(Label).string = `${HeroInfo[this.card_uuid].hp*this.cardData.hero_lv}`; }else{ this.setLabel(this.name_node, `${SpecialCardList[this.card_uuid].name}Lv.${this.cardData.lv}`); this.info_node.active = false; this.oinfo_node.active = true; this.oinfo_node.getChildByName("info").getComponent(Label).string = `${SpecialCardList[this.card_uuid].info}`; } this.setLabel(this.cost_node, `${this.card_cost}`); const iconNode = this.icon_node as Node; if (this.card_type === CardType.Hero) { this.updateHeroAnimation(iconNode, this.card_uuid, this.iconVisualToken); return; } this.clearIconAnimation(iconNode); const iconId = this.resolveCardIconId(this.card_type, this.card_uuid); if (iconId) { this.updateIcon(iconNode, iconId); } else { const sprite = iconNode?.getComponent(Sprite) || iconNode?.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; } } 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: this.restPosition, 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(); } /** 渲染空槽状态 */ private applyEmptyUI() { this.iconVisualToken += 1; this.setLabel(this.name_node, ""); this.setLabel(this.cost_node, ""); if (this.info_node) this.info_node.active = false; if (this.oinfo_node) this.oinfo_node.active = false; this.clearIconAnimation(this.icon_node as Node); const sprite = this.icon_node?.getComponent(Sprite) || this.icon_node?.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; } /** 安全设置文本,兼容节点上或子节点上的 Label */ private setLabel(node: Node | null, value: string) { if (!node) return; const label = node.getComponent(Label) || node.getComponentInChildren(Label); if (label) label.string = value; } private resolveCardUseComp(): CardUseComp | null { let current: Node | null = this.node.parent; while (current) { const comp = current.getComponent(CardUseComp); if (comp) return comp; current = current.parent; } mLogger.log(this.debugMode, "CardComp", "CardUseComp not found for", this.node.name); return null; } private resolveCardIconId(type: CardType, uuid: number): string { if (type === CardType.Skill) { return SkillSet[uuid]?.icon || `${uuid}`; } if (type === CardType.Hero) { return HeroInfo[uuid]?.icon || `${uuid}`; } return `${uuid}`; } private openHeroInfoIBox() { if (!this.cardData) return; if (this.cardData.type !== CardType.Hero) return; const hero = HeroInfo[this.cardData.uuid]; if (!hero) return; const heroLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? hero.lv ?? 1)); oops.gui.remove(UIID.IBox); oops.gui.open(UIID.IBox, { heroUuid: this.cardData.uuid, heroLv }); } private updateHeroAnimation(node: Node, uuid: number, token: number) { const sprite = node?.getComponent(Sprite) || node?.getComponentInChildren(Sprite); if (sprite) sprite.spriteFrame = null; const hero = HeroInfo[uuid]; if (!hero) return; const anim = node.getComponent(Animation) || node.addComponent(Animation); this.clearAnimationClips(anim); const path = `game/heros/hero/${hero.path}/idle`; resources.load(path, AnimationClip, (err, clip) => { if (err || !clip) { mLogger.log(this.debugMode, "CardComp", `load hero animation failed ${uuid}`, err); return; } if (token !== this.iconVisualToken || !this.cardData || this.card_type !== CardType.Hero || this.card_uuid !== uuid) { return; } this.clearAnimationClips(anim); anim.addClip(clip); anim.play("idle"); }); } private clearIconAnimation(node: Node) { const anim = node?.getComponent(Animation); if (!anim) return; anim.stop(); this.clearAnimationClips(anim); } private clearAnimationClips(anim: Animation) { const clips = (anim as any).clips as AnimationClip[] | undefined; if (!clips || clips.length === 0) return; [...clips].forEach(clip => anim.removeClip(clip, true)); } private getMissionCoin(): number { const missionData = smc?.vmdata?.mission_data; return Math.max(0, Math.floor(missionData?.coin ?? 0)); } private setMissionCoin(value: number) { const missionData = smc?.vmdata?.mission_data; if (!missionData) return; missionData.coin = Math.max(0, Math.floor(value)); } /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { this.node.destroy(); } }