import { mLogger } from "../common/Logger"; import { _decorator, Label, Node, tween, Vec3, Color, Sprite, Tween, SpriteAtlas, 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 { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { smc } from "../common/SingletonModuleComp"; import { CardType, FightSet, CardKind } from "../common/config/GameSet"; import { getCardOptions, ICardInfo } from "../common/config/CardSet"; import { TalComp } from "../hero/TalComp"; import { HeroSkillsComp } from "../hero/HeroSkills"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { BuffConf } from "../common/config/SkillSet"; import { BType } from "../common/config/HeroAttrs"; import { AttrCards, PotionCards } from "../common/config/AttrSet"; import { HeroMasterComp } from "../hero/HeroMasterComp"; const { ccclass, property } = _decorator; interface ICardEvent { type?: CardType; level?: number; } /** 视图层对象 */ @ccclass('MissionCardComp') @ecs.register('MissionCard', false) export class MissionCardComp extends CCComp { @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = true; /** 视图层逻辑代码分离演示 */ @property(Node) card1:Node = null! @property(Node) card2:Node = null! @property(Node) card3:Node = null! @property(Node) card4:Node = null! @property(Node) btnClose: Node = null! @property(Node) Lock: Node = null! @property(Node) unLock: Node = null! @property(Node) noStop: Node = null! card1_data: ICardInfo = null! card2_data: ICardInfo = null! card3_data: ICardInfo = null! card4_data: ICardInfo = null! // 当前卡片类型 (用于特殊获取模式) curCardType: CardType | null = null; // 是否处于锁定状态 private isLocked: boolean = true; // 是否永久解锁(本局) private isAdUnlocked: boolean = false; // 图标图集缓存 private uiconsAtlas: SpriteAtlas | null = null; onLoad() { if (this.btnClose) { this.btnClose.on(Node.EventType.TOUCH_END, this.onGiveUp, this); } oops.message.on(GameEvent.TalentSelect, this.onTalentSelect, this); oops.message.on(GameEvent.AttrSelect, this.onAttrSelect, this); oops.message.on(GameEvent.HeroSkillSelect, this.onHeroSkillSelect, this); oops.message.on(GameEvent.ShopOpen, this.onShopOpen, this); oops.message.on(GameEvent.MissionStart, this.onMissionStart, this); oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this); oops.message.on(GameEvent.ToCallFriend, this.onCallFriend, this); } onDestroy() { if (this.btnClose) { this.btnClose.off(Node.EventType.TOUCH_END, this.onGiveUp, this); } oops.message.off(GameEvent.TalentSelect, this.onTalentSelect, this); oops.message.off(GameEvent.AttrSelect, this.onAttrSelect, this); oops.message.off(GameEvent.HeroSkillSelect, this.onHeroSkillSelect, this); oops.message.off(GameEvent.ShopOpen, this.onShopOpen, this); oops.message.off(GameEvent.MissionStart, this.onMissionStart, this); oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this); oops.message.off(GameEvent.ToCallFriend, this.onCallFriend, this); this.ent.destroy(); } init(){ this.onMissionStart(); } /** 游戏开始初始化 */ onMissionStart() { this.isLocked = true; this.isAdUnlocked = false; this.noStop.active = false; if (this.Lock) this.Lock.active = false; // 初始不显示,等待 showCardType if(this.unLock) this.unLock.active=false this.eventQueue = []; } /** 游戏结束清理 */ onMissionEnd() { this.eventQueue = []; this.node.active = false; this.hasSelected = false; // 停止所有卡片动画 const cards = [this.card1, this.card2, this.card3, this.card4]; cards.forEach(card => { if (card) { Tween.stopAllByTarget(card); const selected = card.getChildByName("selected"); if (selected) Tween.stopAllByTarget(selected); } }); } start() { // 初始隐藏或显示逻辑 this.node.active = false; this.resetCardStates(); } private resetCardStates() { const cards = [this.card1, this.card2, this.card3, this.card4]; cards.forEach(card => { if (card) { const selected = card.getChildByName("selected"); if (selected) selected.active = false; // 恢复缩放和颜色 card.setScale(1, 1, 1); const sprite = card.getComponent(Sprite); if (sprite) sprite.color = new Color(255, 255, 255); } }); } // 是否已经选择了天赋 private hasSelected: boolean = false; // 事件队列 private eventQueue: ICardEvent[] = []; private onShopOpen(event: string, args: any) { this.eventQueue.push({ type: CardType.Potion }); this.checkQueue(); } private onAttrSelect(event: string, args: any) { this.eventQueue.push({ type: CardType.Attr }); this.checkQueue(); } private onTalentSelect(event: string, args: any) { this.eventQueue.push({ type: CardType.Talent }); this.checkQueue(); } private onHeroSkillSelect(event: string, args: any) { this.eventQueue.push({ type: CardType.Skill }); this.checkQueue(); } private onCallFriend(event: string, args: any) { this.eventQueue.push({ type: CardType.Partner }); this.checkQueue(); } private checkQueue() { if (this.node.active) return; if (this.eventQueue.length === 0) return; const event = this.eventQueue.shift(); if (event) { if (event.type !== undefined) { this.showCardType(event.type); } } } /** * 显示指定类型的卡牌(特殊获取模式) */ private showCardType(type: CardType) { this.curCardType = type; // 获取当前英雄等级作为参考,或者默认1级 const level = smc.vmdata.hero.lv || 1; this.fetchCards(level, type); this.openUI(); } private openUI() { this.node.active = true; this.hasSelected = false; // 根据锁定状态显示 Lock 节点 (仅在特殊模式下可能需要锁定?或者统一逻辑) // 原逻辑:Lock.active = this.isLocked if (this.Lock) { this.Lock.active = this.isLocked; } // 显示 noStop 节点 if (this.noStop) { this.noStop.active = true; this.checkNoStop() } // 如果没有开启 noStop,则暂停怪物行动 if (!smc.data.noStop) { smc.mission.stop_mon_action = true; } this.resetCardStates(); this.playShowAnimation(); } checkNoStop(){ this.noStop.getChildByName("no").active=!smc.data.noStop // 更新暂停状态 if (this.node.active) { smc.mission.stop_mon_action = !smc.data.noStop; } } switchNoStop(){ smc.data.noStop=!smc.data.noStop this.checkNoStop() } private playShowAnimation() { const cards = [this.card1, this.card2, this.card3, this.card4]; cards.forEach((card, index) => { if (card) { card.setScale(Vec3.ZERO); tween(card) .delay(index * 0.1) .to(0.4, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' }) .start(); } }); } /** * 专门的获取卡牌方法 * @param level 等级 * @param forcedType 强制类型 (可选) */ fetchCards(level: number, forcedType?: CardType){ // 使用 CardSet 的 getCardOptions 获取卡牌 // 这里我们要获取 4 张卡牌 const options = getCardOptions(level, 4, [], forcedType); mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 获取到的卡牌选项: ${JSON.stringify(options)}`); // 更新卡片数据 if (options.length > 0) this.updateCardData(1, options[0]); if (options.length > 1) this.updateCardData(2, options[1]); if (options.length > 2) this.updateCardData(3, options[2]); if (options.length > 3) this.updateCardData(4, options[3]); // 如果获取不足4张,隐藏多余的卡片节点 (UI可能需要处理空数据) if (options.length < 4 && this.card4) this.card4.active = false; if (options.length < 3 && this.card3) this.card3.active = false; if (options.length < 2 && this.card2) this.card2.active = false; if (options.length < 1 && this.card1) this.card1.active = false; } updateCardInfo(card:Node, data: ICardInfo){ if(!card) return card.active = true; // 隐藏选中状态 const selected = card.getChildByName("selected"); if(selected) selected.active = false; let name = card.getChildByName("name") if(name){ name.getComponent(Label)!.string = data.name } let info = card.getChildByName("info")?.getChildByName("Label") if(info){ // ICardInfo 已经标准化了 desc,直接使用 info.getComponent(Label)!.string = data.desc || ""; } // 先隐藏所有类型标识 const typeNodes = ["Atk", "Atked", "Buff", "Attr", "Skill", "Hp", "Dead", "Partner"]; // 1. 处理 card 直接子节点 typeNodes.forEach(nodeName => { const node = card.getChildByName(nodeName); if (node) node.active = false; }); // 2. 处理 card/type 下的子节点 const typeContainer = card.getChildByName("type"); if (typeContainer) { typeNodes.forEach(nodeName => { const node = typeContainer.getChildByName(nodeName); if (node) node.active = false; }); } // 根据 kind 激活对应节点 let activeNodeName = ""; switch (data.kind) { case CardKind.Atk: activeNodeName = "Atk"; break; case CardKind.Atted: activeNodeName = "Atked"; break; case CardKind.Buff: activeNodeName = "Buff"; break; case CardKind.Attr: activeNodeName = "Attr"; break; case CardKind.Skill: activeNodeName = "Skill"; break; case CardKind.Hp: activeNodeName = "Hp"; break; case CardKind.Dead: activeNodeName = "Dead"; break; case CardKind.Partner: activeNodeName = "Partner"; break; } if (activeNodeName) { // 激活 card 下的节点 const activeNode = card.getChildByName(activeNodeName); if (activeNode) activeNode.active = true; // 激活 card/type 下的节点 if (typeContainer) { const activeTypeNode = typeContainer.getChildByName(activeNodeName); if (activeTypeNode) activeTypeNode.active = true; } } // 更新图标 (如果存在 icon 节点) // 注意:根据 Prefab 分析,icon 可能在 card/Mask/icon 路径下,也可能在各个分类节点下 // 这里尝试统一查找 icon 节点 let iconNode: Node | null = null; // 1. 尝试查找通用的 mask/icon (根据之前 card.prefab 分析,有个 Mask/icon 节点) const maskNode = card.getChildByName("Mask"); if (maskNode) { iconNode = maskNode.getChildByName("icon"); } if (iconNode && data.icon) { this.updateIcon(iconNode, data.icon); } } private updateIcon(node: Node, iconId: string) { if (!node || !iconId) return; const sprite = node.getComponent(Sprite); if (!sprite) return; if (this.uiconsAtlas) { const frame = this.uiconsAtlas.getSpriteFrame(iconId); if (frame) { sprite.spriteFrame = frame; } } else { // 加载图集 resources.load("gui/uicons", SpriteAtlas, (err, atlas) => { if (err) { mLogger.error(this.debugMode, 'MissionCard', "[MissionCardComp] Failed to load uicons atlas", err); return; } this.uiconsAtlas = atlas; const frame = atlas.getSpriteFrame(iconId); if (frame) { sprite.spriteFrame = frame; } }); } } updateCardData(index: number, data: ICardInfo) { // 使用动态属性访问 (this as any)[`card${index}_data`] = data; this.updateCardInfo((this as any)[`card${index}`], data); } selectCard(e: any, index: string) { mLogger.log(this.debugMode, 'MissionCard', "selectCard", index) let _index = parseInt(index); // 如果已经选择过,则不再处理 if (this.hasSelected) return; // 动态获取数据和节点 let selectedData: ICardInfo = (this as any)[`card${_index}_data`]; let selectedCardNode: Node | null = (this as any)[`card${_index}`]; if (selectedData && selectedCardNode) { this.hasSelected = true; mLogger.log(this.debugMode, 'MissionCard', "选择卡片:", selectedData.name, "类型:", selectedData.type); // 未选中的卡片缩小 const cards = [this.card1, this.card2, this.card3, this.card4]; cards.forEach(card => { if (card && card !== selectedCardNode) { tween(card).to(0.2, { scale: Vec3.ZERO }).start(); } }); // 显示当前选中的 selected 节点 const selected = selectedCardNode.getChildByName("selected"); if(selected) { selected.active = true; selected.setScale(Vec3.ZERO); tween(selected).to(0.2, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' }).start(); } // 选中卡片动效后触发逻辑 tween(selectedCardNode) .to(0.1, { scale: new Vec3(1.1, 1.1, 1.1) }) .to(0.1, { scale: new Vec3(1, 1, 1) }) .delay(0.5) .call(() => { // 使用 HeroMasterComp 查找主角实体 // @ts-ignore const entities = ecs.query(ecs.allOf(HeroMasterComp)); let role = entities.length > 0 ? entities[0] : null; if (!role) { mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 未找到挂载 HeroMasterComp 的主角实体`); } else { mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 成功定位主角实体: ${role.eid}`); } if (role) { switch (selectedData.type) { case CardType.Talent: smc.addTalentRecord(selectedData.uuid); // 直接调用 TalComp 添加天赋 const talComp = role.get(TalComp); if (talComp) { const beforeCount = Object.keys(talComp.Tals).length; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Talent Before: Count=${beforeCount}, Tals=${JSON.stringify(talComp.Tals)}`); talComp.addTal(selectedData.uuid); const afterCount = Object.keys(talComp.Tals).length; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Talent After: Count=${afterCount}, Added=${selectedData.uuid}, Tals=${JSON.stringify(talComp.Tals)}`); } break; case CardType.Skill: smc.addSkillRecord(selectedData.uuid); // 直接调用 HeroSkillsComp 添加技能 const skillComp = role.get(HeroSkillsComp); if (skillComp) { const beforeCount = Object.keys(skillComp.skills).length; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Skill Before: Count=${beforeCount}, Skills=${JSON.stringify(skillComp.skills)}`); skillComp.addSkill(selectedData.uuid); const afterCount = Object.keys(skillComp.skills).length; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Skill After: Count=${afterCount}, Added=${selectedData.uuid}, Skills=${JSON.stringify(skillComp.skills)}`); } break; case CardType.Partner: // 伙伴是召唤新实体,依然适合用事件,或者直接调用 summon 方法 oops.message.dispatchEvent(GameEvent.CallFriend, { uuid: selectedData.uuid }); break; case CardType.Potion: // 药水直接作用于 HeroAttrsComp const attrsComp = role.get(HeroAttrsComp); if (attrsComp) { const potion = PotionCards[selectedData.uuid]; if (potion) { const beforeVal = attrsComp.Attrs[potion.attr] || 0; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Potion Before: Attr[${potion.attr}]=${beforeVal}, Attrs=${JSON.stringify(attrsComp.Attrs)}`); const buffConf: BuffConf = { buff: potion.attr, value: potion.value, BType: BType.RATIO, time: potion.duration, chance: 1, }; attrsComp.addBuff(buffConf); smc.updateHeroInfo(attrsComp); mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Potion Applied: ${potion.desc}, Value=${potion.value}, Attrs=${JSON.stringify(attrsComp.Attrs)}`); oops.gui.toast(potion.desc); } } break; case CardType.Attr: // 属性卡:使用 addBuff 添加永久属性加成 const attrCard = AttrCards[selectedData.uuid]; if (attrCard) { const attrsComp = role.get(HeroAttrsComp); if (attrsComp) { // 记录变更前状态 const roleBefore = attrsComp.Attrs[attrCard.attr] || 0; // 构造永久 Buff (time: 0) const buffConf: BuffConf = { buff: attrCard.attr, value: attrCard.value, // 注意:这里需要根据属性类型决定是 VALUE 还是 RATIO // 通常 AttrCards 里的 value 如果是百分比属性(如暴击率),配置值可能是 2 (代表2%) // HeroAttrsComp.addBuff 会根据 BType 累加 // 为了安全,我们可以查一下 AttrsType,或者默认属性卡都是 VALUE (直接加数值) 或 RATIO (百分比) // 根据 AttrSet.ts,AttrCards 的 value 是直接数值。 // 而 HeroAttrsComp.recalculateSingleAttr 中,RATIO 类型的属性也是直接加 totalRatio // 所以这里关键是 buffConf.BType。 // 如果我们希望属性卡是"基础值加成"(VALUE) 还是 "百分比加成"(RATIO)? // 查看 AttrSet.ts: 2003 DEF value: 2 desc: "防御力 +2%" -> 看起来是百分比 // 2001 AP value: 10 desc: "攻击力 +10" -> 看起来是数值 // 所以需要根据 attrCard.attr 的类型来决定 BType,或者在 AttrCards 配置中增加 BType 字段 // 目前 AttrInfo 接口没有 BType。 // 我们可以引入 AttrsType 来判断默认类型,或者全部作为 VALUE (如果是百分比属性,VALUE也是加到数值上的) // 修正:HeroAttrsComp.recalculateSingleAttr 中: // isRatioAttr = AttrsType[attrIndex] === BType.RATIO // 如果是 RATIO 属性,totalValue + totalRatio // 如果是 VALUE 属性,totalValue * (1 + totalRatio/100) // 策略: // 如果是数值型属性(如HP, AP),属性卡通常是加固定值 -> BType.VALUE // 如果是百分比型属性(如暴击率),属性卡加的是点数 -> BType.VALUE (因为最终计算是 totalValue + totalRatio,对于百分比属性 totalValue 就是基础点数) // 等等,recalculateSingleAttr 对 RATIO 属性是 totalValue + totalRatio // 如果暴击率基础是 0,加 2%暴击率。 // 如果用 BType.VALUE, totalValue += 2。 结果 = 2 + 0 = 2。正确。 // 如果用 BType.RATIO, totalRatio += 2。 结果 = 0 + 2 = 2。也正确。 // 但是对于数值型属性(如AP): // 攻击力 +10。 // 用 BType.VALUE, totalValue += 10。 结果 = (Base+10) * (1+Ratio)。正确。 // 用 BType.RATIO, totalRatio += 10。 结果 = Base * (1+10/100) = Base * 1.1。这是加10%,不是加10点。 // 结论:AttrCards 中的 value 看起来都是"点数"或"绝对值",所以应该统一使用 BType.VALUE。 // 即使是 "防御力 +2%" (2003),如果 DEF 是 RATIO 类型(AttrSet.ts里定义为RATIO? 不,DEF通常是混合,但这里AttrsType定义DEF是RATIO? 让我们查一下) // 查 AttrsType: [Attrs.DEF]: BType.RATIO // 如果 DEF 是 RATIO,那 +2% 就是 +2 数值。 // 所以统一用 BType.VALUE 是安全的,代表"增加属性面板数值"。 BType: BType.VALUE, time: 0, chance: 1, }; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Adding Buff: Attr=${attrCard.attr}, Val=${attrCard.value}, Type=VALUE`); attrsComp.addBuff(buffConf); // addBuff 内部会自动调用 recalculateSingleAttr 和 updateHeroInfo const roleAfter = attrsComp.Attrs[attrCard.attr] || 0; mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] Attr After: Hero=${roleAfter} (Change: ${roleAfter - roleBefore})`); oops.gui.toast(attrCard.desc); } } else { console.warn(`[MissionCard] 未找到属性卡配置: UUID=${selectedData.uuid}`); } break; } } else { mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 主角实体无效,无法应用卡牌效果`); } // 记录已获取的卡牌 oops.message.dispatchEvent(GameEvent.UpdateMissionGet, { uuid: selectedData.uuid, icon: selectedData.icon, kind: selectedData.kind }); this.close(); }) .start(); } } /** 看广告关闭 Lock */ watchAdCloseLock() { // TODO: 此处接入 IAA 广告 SDK mLogger.log(this.debugMode, 'MissionCard', "播放激励视频广告..."); // 模拟广告播放成功回调 this.isLocked = false; this.isAdUnlocked = true; if (this.Lock) { this.Lock.active = false; oops.gui.toast("解锁成功"); } this.closeUnLock(); } coinCloseLock(){ let cost = smc.vmdata.mission_data.unlockCoin; if (smc.vmdata.gold >= cost) { // 扣除金币 if (smc.updateGold(-cost)) { this.isLocked = false; if (this.Lock) { this.Lock.active = false; } oops.gui.toast("解锁成功"); this.closeUnLock(); } else { oops.gui.toast("交易失败"); } } else { oops.gui.toast("金币不足"); } } showUnLock(){ if (this.unLock) { this.unLock.active = true; this.unLock.setScale(Vec3.ZERO); tween(this.unLock) .to(0.2, { scale: new Vec3(1, 1, 1) }, { easing: 'backOut' }) .start(); } } closeUnLock(){ if (this.unLock && this.unLock.active) { tween(this.unLock) .to(0.2, { scale: Vec3.ZERO }, { easing: 'backIn' }) .call(() => { this.unLock.active = false; }) .start(); } } /** 放弃选择 */ onGiveUp() { if (this.hasSelected) return; this.hasSelected = true; // 隐藏关闭按钮 if (this.btnClose) { this.btnClose.active = false; } const cards = [this.card1, this.card2, this.card3, this.card4]; let delayTime = 0.2; cards.forEach(card => { if (card && card.active) { tween(card).to(delayTime, { scale: Vec3.ZERO }).start(); } }); // 动画结束后关闭 this.scheduleOnce(() => { this.close(); }, delayTime); } /** * 关闭界面 */ close() { this.node.active = false; // 恢复游戏运行状态(取消暂停) smc.mission.stop_mon_action = false; // 关闭时隐藏按钮,避免下次打开其他类型时闪烁 if (this.btnClose) { this.btnClose.active = false; } // 关闭时隐藏 Lock 节点 if (this.Lock) { this.Lock.active = false; } // 关闭时隐藏 noStop 节点 if (this.noStop) { this.noStop.active = false; } // 恢复锁定状态(如果没有永久解锁) if (!this.isAdUnlocked) { this.isLocked = true; } this.checkQueue(); } /** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */ reset() { this.node.destroy(); } }