From d135aa9905ade9ab8d8f59e4fdf18401f3a4897e Mon Sep 17 00:00:00 2001 From: panFD Date: Sun, 26 Jul 2026 14:44:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor(heroCard):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=8B=B1=E9=9B=84=E5=8D=A1=E7=89=8C=E7=B3=BB=E7=BB=9F=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=8EUI=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重写抽卡逻辑,移除升级卡机制,改为从基础英雄池按权重抽取不重复英雄 2. 优化英雄卡入场、购买、刷新动画,新增按钮触摸反馈效果 3. 调整mission.prefab与cHero.prefab的UI元素位置与布局参数 4. 简化卡牌布局代码,交由prefab的Widget组件自动约束 --- assets/resources/gui/element/cHero.prefab | 10 +- assets/resources/gui/element/mission.prefab | 4 +- assets/script/game/map/CHeroComp.ts | 184 ++++++++++++++++++- assets/script/game/map/MissionCardComp.ts | 192 +++----------------- 4 files changed, 213 insertions(+), 177 deletions(-) diff --git a/assets/resources/gui/element/cHero.prefab b/assets/resources/gui/element/cHero.prefab index 6a9d0e7b..c6423178 100644 --- a/assets/resources/gui/element/cHero.prefab +++ b/assets/resources/gui/element/cHero.prefab @@ -2244,7 +2244,7 @@ "value": { "__type__": "cc.Vec3", "x": 0, - "y": -95.715, + "y": -110.847, "z": 0 } }, @@ -2523,14 +2523,14 @@ "node": { "__id__": 1 }, - "_enabled": true, + "_enabled": false, "__prefab": { "__id__": 150 }, - "_alignFlags": 40, + "_alignFlags": 0, "_target": null, - "_left": 245, - "_right": 245, + "_left": 310, + "_right": 310, "_top": 0, "_bottom": 0, "_horizontalCenter": 0, diff --git a/assets/resources/gui/element/mission.prefab b/assets/resources/gui/element/mission.prefab index 308bafad..023f916b 100644 --- a/assets/resources/gui/element/mission.prefab +++ b/assets/resources/gui/element/mission.prefab @@ -1456,7 +1456,7 @@ "_lpos": { "__type__": "cc.Vec3", "x": 306.634, - "y": 451.513, + "y": 651.253, "z": 0 }, "_lrot": { @@ -1975,7 +1975,7 @@ "_left": -8.317000000000007, "_right": -6.6340000000000146, "_top": 89.63, - "_bottom": 396.513, + "_bottom": 596.253, "_horizontalCenter": 0, "_verticalCenter": 0, "_isAbsLeft": true, diff --git a/assets/script/game/map/CHeroComp.ts b/assets/script/game/map/CHeroComp.ts index b651c9f7..39cb2594 100644 --- a/assets/script/game/map/CHeroComp.ts +++ b/assets/script/game/map/CHeroComp.ts @@ -16,7 +16,7 @@ * - 英雄卡效果事件为 GameEvent.CallHero,而非 UseSkillCard。 */ import { mLogger } from "../common/Logger"; -import { _decorator, Node, Sprite, Label, NodeEventType, Vec3 } from "cc"; +import { _decorator, Node, Sprite, Label, NodeEventType, Vec3, Tween, tween, UIOpacity } 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"; @@ -88,6 +88,8 @@ export class CHeroComp extends CCComp { private fixedBaseZ: number = 0; /** 静止位置(由 MissionCardComp 布局决定) */ private restPosition: Vec3 = new Vec3(); + /** 透明度组件(用于淡入淡出) */ + private opacityComp: UIOpacity | null = null; // ======================== 生命周期 ======================== @@ -106,7 +108,12 @@ export class CHeroComp extends CCComp { 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,此处绑定就绪后重刷一次 @@ -118,7 +125,9 @@ export class CHeroComp extends CCComp { 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); } } @@ -127,6 +136,10 @@ export class CHeroComp extends CCComp { /** * 初始化英雄卡数据并渲染 UI * + * 若槽位已有旧卡且处于显示状态,先播放旧卡"逝去"动画(缩小淡出), + * 动画结束后再切换为新卡数据并播放入场动画。 + * 首次赋值(node 未激活)直接渲染,无逝去动画。 + * * @param data 卡牌配置,传 null 时清空显示 */ applyCardData(data: CardConfig | null): void { @@ -142,6 +155,36 @@ export class CHeroComp extends CCComp { 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) { @@ -154,6 +197,12 @@ export class CHeroComp extends CCComp { /** 清空 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]; @@ -252,6 +301,122 @@ export class CHeroComp extends CCComp { costLabel.string = `${this.cardData.cost ?? 0}`; } } + + // 入场动画:从静止位置下方升起 + 淡入 + this.playEnterAnim(); + } + + // ======================== 动画效果 ======================== + + /** 入场动画时长(秒) */ + 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(); } /** @@ -298,15 +463,19 @@ export class CHeroComp extends CCComp { /** * 购买点击处理: - * 1. 英雄卡先通过 UseHeroCard 事件做数量上限校验(guard 模式,可被取消)。 - * 2. 扣除金币(失败则提示并中止)。 - * 3. 按卡牌类型分发效果:英雄卡 → CallHero 召唤英雄;特殊卡 → UseSpecialCard。 - * 4. 派发 CardUsed 通知 MissionCardComp 刷新英雄卡池。 - * 5. 标记已购买,隐藏购买按钮。 + * 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 阻止使用 @@ -353,6 +522,9 @@ export class CHeroComp extends CCComp { break; } + // 播放购买动画(缩小淡出),刷新卡池由 CardUsed 事件触发 + this.playPurchaseAnim(); + // 标记已购买 this.isPurchased = true; if (this.buy_node) { diff --git a/assets/script/game/map/MissionCardComp.ts b/assets/script/game/map/MissionCardComp.ts index 334293e0..212f7626 100644 --- a/assets/script/game/map/MissionCardComp.ts +++ b/assets/script/game/map/MissionCardComp.ts @@ -3,9 +3,9 @@ * @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层) * * 职责: - * 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CHeroComp 槽位。 - * 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡"; - * 场上无英雄时仅抽取英雄卡和刷新卡。 + * 1. **卡牌分发管理** —— 从卡池抽取 3 张英雄卡,分发到 3 个 CHeroComp 槽位。 + * 抽卡规则:从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复; + * 场上已召唤的英雄不再刷出。 * 2. **金币费用管理** —— 抽卡费用(refreshCost)的扣除。 * 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。 * 4. **场上英雄信息面板(HInfoComp 列表)同步** —— @@ -17,7 +17,7 @@ * 关键设计: * - 3 个 CHeroComp 由 cardPrefab 动态实例化生成,通过 cacheCardComps() 映射为有序数组 cardComps[], * 之后所有分发、清空操作均通过此数组进行。 - * - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。 + * - buildDrawCards() 从基础英雄卡池按权重抽取 3 张,不足时循环补齐。 * - 英雄上限校验(onUseHeroCard)采用 guard/cancel 模式: * CHeroComp 发出 UseHeroCard 事件并传入 guard 对象, * 本组件可通过 guard.cancel=true 阻止使用。 @@ -25,7 +25,7 @@ * 历史: * 旧版本曾包含"卡池等级(poolLv)"机制和"三合一合成腾位"判断,已全部移除: * - 卡牌不再分级,所有英雄卡统一 lv1。 - * - 英雄升级仅通过升级卡(SpecialUpgrade)触发,按 UUID 精确升级场上对应英雄。 + * - 升级卡机制已移除,英雄卡池只出普通英雄卡;英雄升级仅通过升级卡(SpecialUpgrade)触发。 * * 依赖: * - CHeroComp —— 单卡槽位 @@ -39,7 +39,7 @@ import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, Tween, twe import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { GameEvent } from "../common/config/GameEvent"; -import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet"; +import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType } from "../common/config/CardSet"; import { drawSkillCards } from "../common/config/SCardSet"; import { drawEquipCards } from "../common/config/EquipSet"; import { drawItemCards } from "../common/config/ICardSet"; @@ -70,8 +70,6 @@ const { ccclass, property } = _decorator; export class MissionCardComp extends CCComp { /** 是否启用调试日志 */ private debugMode: boolean = false; - /** 卡牌槽位宽度(像素),用于水平等距布局 */ - private readonly cardWidth: number = 175; /** 按钮正常缩放 */ private readonly buttonNormalScale: number = 1; /** 按钮按下缩放 */ @@ -217,14 +215,6 @@ export class MissionCardComp extends CCComp { private cardsShowScale: Vec3 = new Vec3(1, 1, 1); /** 卡牌面板收起态缩放(scale=0 隐藏) */ private cardsHideScale: Vec3 = new Vec3(0, 0, 1); - /** 卡牌原始定位点 */ - private cardsPos = [-220, 0, 220] - /** - * 必出升级卡的轮询索引:每次刷新按场上可升级英雄 UUID 升序轮询选 1 张。 - * 每次 buildDrawCards 取模更新,保证多次刷新时升级卡依次切换。 - */ - private upgradeRotationIndex: number = 0; - /** 卡牌面板显示位置 Y 坐标 */ private readonly cardPanelShowY: number = 0; /** 卡牌面板隐藏位置 Y 坐标(向下 700) */ @@ -801,8 +791,6 @@ export class MissionCardComp extends CCComp { if (!uuid) return; let success = false; if (type === CardType.SpecialUpgrade) { - const template = SpecialUpgradeCardList[uuid]; - if (!template) return; const targetHeroEid = Number(payload?.target_hero_eid ?? 0); success = this.tryUpgradeHeroByEid(targetHeroEid); if (!success) { @@ -1337,123 +1325,42 @@ export class MissionCardComp extends CCComp { } /** - * 构建本次抽卡结果,保证最终可分发 3 条数据。 + * 构建本次抽卡结果,返回 3 张英雄卡。 * - * 抽卡规则: - * 1. **必出规则**:当场上有可升级英雄时,每次刷新必出 1 张升级卡,并放在返回数组首位(对应卡池最左侧槽位)。 - * 多次刷新时,按场上英雄 target_hero_eid 升序循环切换升级卡(upgradeRotationIndex)。 - * 2. 其余 2 张从混合池(剩余升级卡 + 基础英雄卡)按权重抽取。 + * 规则: + * 1. 从基础英雄卡池按权重抽取,同一次抽卡内英雄不重复。 + * 2. 过滤掉场上已召唤的英雄(已召唤的英雄不再刷出,仅通过升级卡升级)。 + * 3. 不足 3 张时循环补齐(允许重复)。 * - * 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,全部出升级卡(若全部已满级则降级回混合池)。 + * 注:升级卡机制已移除,英雄卡池只出普通英雄卡。 */ private buildDrawCards(): CardConfig[] { - const upgradeCards = this.buildHeroUpgradeCards(); - const aliveHeroCount = this.getAliveHeroCount(); - const heroMax = this.getMissionHeroMaxNum(); - - // 英雄已满员且有可升级的英雄 → 只出升级卡 - if (aliveHeroCount >= heroMax && upgradeCards.length > 0) { - const picked = this.pickMixedCards(upgradeCards, 3, upgradeCards); - if (picked.length >= 3) return picked.slice(0, 3); - /** 兜底:不足 3 张时循环补齐 */ - const filled = [...picked]; - while (filled.length < 3) { - filled.push(upgradeCards[filled.length % upgradeCards.length]); - } - return filled; - } - const heroCards = CardPoolList.filter(c => c.type === CardType.Hero); - // 过滤掉场上已召唤英雄的普通英雄卡:已召唤的英雄只能通过升级卡升级,不再刷出普通卡 const aliveHeroUuids = this.getAliveHeroUuids(); const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid)); - // 英雄池只刷英雄卡(含动态升级卡 + 未召唤英雄的普通卡) - const mixedPool: CardConfig[] = [...upgradeCards, ...availableHeroCards]; - if (mixedPool.length === 0) return []; + if (availableHeroCards.length === 0) return []; const result: CardConfig[] = []; - const usedUpgradeTargets = new Set(); - const usedHeroUuids = new Set(); + const usedUuids = new Set(); - // 必出 1 张升级卡,按 UUID 升序轮询,放在返回数组首位(对应最左侧卡槽) - if (upgradeCards.length > 0) { - const idx = this.upgradeRotationIndex % upgradeCards.length; - this.upgradeRotationIndex = (this.upgradeRotationIndex + 1) % upgradeCards.length; - const mandatory = upgradeCards[idx]; - result.push(mandatory); - usedUpgradeTargets.add(mandatory.target_hero_eid ?? 0); - } - - // 剩余张数从混合池抽取,排除已选升级卡 target_hero_eid 和已选英雄卡 uuid - const remainingCount = 3 - result.length; - if (remainingCount > 0) { - const remainingPool = mixedPool.filter(c => { - if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) { - return !usedUpgradeTargets.has(c.target_hero_eid); - } - if (c.type === CardType.Hero) { - return !usedHeroUuids.has(c.uuid); - } - return true; - }); - const rest = this.pickMixedCards(remainingPool, remainingCount, upgradeCards); - rest.forEach(c => { - result.push(c); - if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) { - usedUpgradeTargets.add(c.target_hero_eid); - } - if (c.type === CardType.Hero) { - usedHeroUuids.add(c.uuid); - } - }); - } - - // 兜底:不足 3 张时从未使用的英雄卡中补齐,避免出现重复英雄 + // 按权重抽取,同一次抽卡内英雄不重复 while (result.length < 3) { - const fillCard = availableHeroCards.find(c => !usedHeroUuids.has(c.uuid)); - if (!fillCard) break; - result.push(fillCard); - usedHeroUuids.add(fillCard.uuid); + const pool = availableHeroCards.filter(c => !usedUuids.has(c.uuid)); + if (pool.length === 0) break; + const pick = this.weightedPick(pool); + if (!pick) break; + result.push(pick); + usedUuids.add(pick.uuid); + } + + // 兜底:不足 3 张时循环补齐(允许重复) + while (result.length < 3) { + result.push(availableHeroCards[result.length % availableHeroCards.length]); } return result.slice(0, 3); } - /** - * 从混合池中按权重抽取 n 张卡。 - * 升级卡之间强制 unique(同一个 target_hero_eid 只能出现一次)。 - * 英雄卡之间强制 unique(同一个 uuid 只能出现一次),避免同一次发牌出现重复英雄。 - */ - private pickMixedCards(pool: CardConfig[], count: number, upgradeCards: CardConfig[]): CardConfig[] { - if (pool.length === 0 || count <= 0) return []; - const selected: CardConfig[] = []; - const usedUpgradeTargets = new Set(); - const usedHeroUuids = new Set(); - - while (selected.length < count) { - const available = pool.filter(c => { - if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) { - return !usedUpgradeTargets.has(c.target_hero_eid); - } - if (c.type === CardType.Hero) { - return !usedHeroUuids.has(c.uuid); - } - return true; - }); - if (available.length === 0) break; - const pick = this.weightedPick(available); - if (!pick) break; - selected.push(pick); - if (pick.type === CardType.SpecialUpgrade && pick.target_hero_eid) { - usedUpgradeTargets.add(pick.target_hero_eid); - } - if (pick.type === CardType.Hero) { - usedHeroUuids.add(pick.uuid); - } - } - return selected; - } - /** 单次按权重抽取一张卡 */ private weightedPick(cards: CardConfig[]): CardConfig | null { if (cards.length === 0) return null; @@ -1466,38 +1373,6 @@ export class MissionCardComp extends CCComp { return cards[cards.length - 1]; } - /** - * 扫描场上存活英雄,为每个英雄实体生成一张升级卡(按 eid 绑定)。 - * - 已达 HERO_MAX_LV 的英雄不出卡。 - * - 同 eid 只生成一张(eid 本就唯一,不会重复)。 - * - cost = 模板 cost + (当前等级 - 1) * BASE_COST,等级越高升级越贵。 - */ - private buildHeroUpgradeCards(): CardConfig[] { - const template = SpecialUpgradeCardList[7001]; - if (!template) return []; - - const actors = this.queryAliveHeroActors(); - if (actors.length === 0) return []; - - const result: CardConfig[] = []; - for (const actor of actors) { - const eid = actor.eid; - const lv = actor.model.lv; - if (lv >= FightSet.HERO_MAX_LV) continue; // 已达上限不再出升级卡 - const dynamicCost = template.cost + (lv - 1) * FightSet.BASE_COST; - result.push({ - ...template, - cost: dynamicCost, - weight: template.weight, - target_hero_eid: eid, - hero_lv: lv + 1, - }); - } - // 按 target_hero_eid 升序排序,保证返回顺序稳定(便于 buildDrawCards 按顺序轮询) - result.sort((a, b) => (a.target_hero_eid ?? 0) - (b.target_hero_eid ?? 0)); - return result; - } - /** * 构建技能卡抽卡结果,返回 3 张。 * @@ -1571,18 +1446,7 @@ export class MissionCardComp extends CCComp { } private layoutCardSlots() { - if (!this.cardComps) return; - const count = this.cardComps.length; - if (count === 0) return; - for (let i = 0; i < count; i++) { - if (this.cardComps[i]) { - this.cardComps[i].setSlotPosition(this.cardsPos[i]); - } - } - mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", { - count, - cardWidth: this.cardWidth - }); + // 布局由 cHero.prefab 根节点 Widget 约束控制,代码不再覆盖位置 } private playButtonPressAnim(node: Node | null) { @@ -1727,7 +1591,7 @@ export class MissionCardComp extends CCComp { /** * 获取场上所有存活英雄的 hero_uuid 集合。 - * 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出,只能通过升级卡升级。 + * 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出。 */ private getAliveHeroUuids(): Set { const uuids = new Set();