refactor: 重构英雄养成与卡牌系统,移除旧合成机制

1.  移除三合一英雄合成与链式合成逻辑,统一使用升级卡进行英雄等级提升
2.  重构卡池系统:移除卡池等级机制,所有英雄卡牌统一为LV1
3.  重构升级逻辑:改为按UUID精确升级场上英雄,动态生成对应升级卡
4.  更新配置常量:拆分并重构成长倍率、英雄上限等战斗配置
5.  简化抽卡逻辑:不再按卡池等级分发卡牌,改为动态混合基础卡与升级卡
6.  清理废弃代码:移除卡池升级相关的UI、逻辑与配置
This commit is contained in:
pan
2026-07-20 17:26:50 +08:00
parent f0952ef82c
commit 15ab2f7b0f
10 changed files with 729 additions and 1059 deletions

View File

@@ -764,14 +764,28 @@ export class CardComp extends CCComp {
if (this.info_node) this.info_node.active = false;
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
// 动态升级卡:显示对应英雄名 + "升级"后缀 + 目标等级
if (this.card_type === CardType.SpecialUpgrade && this.cardData.target_hero_uuid) {
const targetHero = HeroInfo[this.cardData.target_hero_uuid];
const targetLv = Math.max(1, Math.floor(this.cardData.hero_lv ?? 1));
this.setLabel(this.name_node, `${targetHero?.name || ""} 升级`);
if (this.info_node) {
this.info_node.active = true;
this.setLabel(this.info_node, `升至 Lv.${targetLv}`);
}
} else {
// 特殊卡(升级 / 刷新):显示卡名 + 品质后缀 + 描述
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
if (this.info_node) {
this.info_node.active = true;
this.setLabel(this.info_node, specialCard?.info || "");
}
}
this.ap_node.active = false;
this.hp_node.active = false;
}

View File

@@ -246,12 +246,18 @@ export class CardLiteComp extends CCComp {
this.setLabel(this.name_node, `${spSuffix}${skillCard?.name || skill?.name || ""}${spSuffix}`);
} else {
if (this.lvl_node) this.lvl_node.node.active = false;
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
// 动态升级卡:显示对应英雄名 + "升级"后缀
if (this.card_type === CardType.SpecialUpgrade && this.cardData.target_hero_uuid) {
const targetHero = HeroInfo[this.cardData.target_hero_uuid];
this.setLabel(this.name_node, `${targetHero?.name || ""} 升级`);
} else {
const specialCard = this.card_type === CardType.SpecialUpgrade
? SpecialUpgradeCardList[this.card_uuid]
: SpecialRefreshCardList[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}${specialCard?.name || ""}${spSuffix}`);
}
}
if (this.cost_node) {

View File

@@ -3,41 +3,43 @@
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
*
* 职责:
* 1. **卡牌分发管理** —— 从卡池抽取 4 张卡,分发到 4 个 CardComp 槽位。
* 2. **卡池升级** —— 消耗金币提升卡池等级poolLv解锁更高稀有度的卡牌。
* 3. **金币费用管理** —— 抽卡费用refreshCost、升级费用CardsUpSet
* 波次折扣CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE的计算与扣除。
* 4. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许
* 再召唤英雄(含合成后腾位的特殊判断 canUseHeroCardByMerge
* 5. **场上英雄信息面板HInfoComp 列表)同步** ——
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CardComp 槽位。
* 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡"
* 场上无英雄时仅抽取英雄卡和刷新卡。
* 2. **金币费用管理** —— 抽卡费用refreshCost扣除。
* 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。
* 4. **场上英雄信息面板HInfoComp 列表)同步** ——
* 英雄上场时实例化面板,死亡时移除,定时刷新属性显示。
* 6. **特殊卡执行** —— 处理英雄升级卡SpecialUpgrade和英雄刷新卡SpecialRefresh
* 7. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画
* 5. **特殊卡执行** —— 处理英雄升级卡SpecialUpgrade,按 UUID 精确升级)和
* 英雄刷新卡SpecialRefresh
* 6. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
*
* 关键设计:
* - 4 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[]
* - 3 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[]
* 之后所有分发、清空操作均通过此数组进行。
* - buildDrawCards() 保证每次抽取 4 张,不足时循环补齐。
* - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。
* - 英雄上限校验onUseHeroCard采用 guard/cancel 模式:
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
* 本组件可通过 guard.cancel=true 阻止使用。
* - ensureHeroInfoPanel() 建立 EID → HInfoComp 的 Map 映射,
* 支持英雄合成升级后面板热更新。
*
* 历史:
* 旧版本曾包含"卡池等级poolLv"机制和"三合一合成腾位"判断,已全部移除:
* - 卡牌不再分级,所有英雄卡统一 lv1。
* - 英雄升级仅通过升级卡SpecialUpgrade触发按 UUID 精确升级场上对应英雄。
*
* 依赖:
* - CardComp —— 单卡槽位
* - HInfoComp —— 英雄信息面板
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
* - HeroAttrsComp —— 英雄属性(合成校验 / 升级)
* - MissionHeroComp —— 获取合成规则needCount / maxLv
* - HeroAttrsComp —— 英雄属性(升级)
* - smc.vmdata.mission_data —— 局内数据coin / hero_num / hero_max_num
*/
import { mLogger } from "../common/Logger";
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtlas, Tween, tween, Vec3, Widget } from "cc";
import { _decorator, Label, Node, NodeEventType, Tween, tween, Vec3 } 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 { GameEvent } from "../common/config/GameEvent";
import { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE, CardConfig, CardType, CardsUpSet, drawCardsByRule, getCardsByLv, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardComp } from "./CardComp";
import { SCardComp } from "./SCardComp";
import { oops } from "db://oops-framework/core/Oops";
@@ -45,11 +47,8 @@ import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet, CARD_POOL_UPGRADE_WAVES, SKILL_CARD_WAVES } from "../common/config/GameSet";
import { MoveComp } from "../hero/MoveComp";
import { MissionHeroComp } from "./MissionHeroComp";
import { FacSet, FightSet, SKILL_CARD_WAVES } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
import { MissionComp } from "./MissionComp";
import { UIID } from "../common/config/GameUIConfig";
const { ccclass, property } = _decorator;
@@ -58,7 +57,7 @@ const { ccclass, property } = _decorator;
/**
* MissionCardComp —— 卡牌系统核心控制器
*
* 管理 4 个卡牌槽位的抽卡分发、卡池升级、金币费用、
* 管理 3 个卡牌槽位的抽卡分发、金币费用、
* 英雄上限校验、场上英雄信息面板同步以及特殊卡执行。
*/
@ccclass('MissionCardComp')
@@ -99,13 +98,13 @@ export class MissionCardComp extends CCComp {
/** 抽卡(刷新)按钮节点 */
@property(Node)
cards_chou: Node = null!
/** 卡池升级按钮节点 */
/** 卡池升级按钮节点(已废弃,保留节点引用防止 editor 报错) */
@property(Node)
cards_up: Node = null!
/** 金币显示节点(含 icon + num 子节点) */
@property(Node)
coins_node: Node = null!
/** 卡池等级显示节点 */
/** 卡池等级显示节点(已废弃,保留节点引用) */
@property(Node)
pool_lv_node: Node = null!
/** 英雄数量显示节点(含 icon + num 子节点) */
@@ -135,12 +134,10 @@ export class MissionCardComp extends CCComp {
// ======================== 运行时状态 ========================
/** 个槽位对应的 CardComp 控制器缓存(有序数组) */
/** 个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 技能卡槽控制器缓存 */
private skillCardComps: SCardComp[] = [];
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
private poolLv: number = CARD_POOL_INIT_LEVEL;
/** 是否已缓存卡牌面板基准缩放 */
private hasCachedCardsBaseScale: boolean = false;
/** 卡牌面板基准缩放(从场景读取) */
@@ -157,20 +154,17 @@ export class MissionCardComp extends CCComp {
/**
* 组件加载:
* 1. 绑定生命周期事件和按钮交互事件。
* 2. 缓存 4 个 CardComp 子控制器引用。
* 2. 缓存 3 个 CardComp 子控制器引用。
* 3. 计算并设置槽位水平布局。
* 4. 初始化卡牌面板缩放参数。
* 5. 触发首次任务开始流程。
*/
onLoad() {
this.bindEvents();
this.cacheCardComps();
this.layoutCardSlots();
this.initCardsPanelPos();
// this.onMissionStart(); // 移除 onLoad 自动触发,改为事件驱动
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
slots: this.cardComps.length,
poolLv: this.poolLv
});
}
@@ -193,15 +187,13 @@ export class MissionCardComp extends CCComp {
/**
* 任务开始:
* 1. 进入准备阶段(展开卡牌面板)。
* 2. 重置卡池等级为初始值
* 3. 初始化局内数据(金币、英雄数量上限)
* 4. 清空旧英雄信息面板和卡牌槽位
* 5. 重置按钮状态和 UI 显示
* 6. 执行首次抽卡并分发到 4 个槽位。
* 2. 初始化局内数据(金币、英雄数量上限)
* 3. 清空旧英雄信息面板和卡牌槽位
* 4. 重置按钮状态和 UI 显示
* 5. 执行首次抽卡并分发到 3 个槽位
*/
onMissionStart() {
this.enterPreparePhase();
this.poolLv = CARD_POOL_INIT_LEVEL;
const missionData = this.getMissionData();
if (missionData) {
missionData.coin = Math.max(0, Math.floor(missionData.coin ?? 0));
@@ -217,11 +209,7 @@ export class MissionCardComp extends CCComp {
this.layoutCardSlots();
this.clearAllCards();
// if (this.cards_up) {
// this.cards_up.active = true;
// }
this.resetButtonScale(this.cards_chou);
// this.resetButtonScale(this.cards_up);
this.updateCoinAndCostUI();
this.updateHeroNumUI(false, false);
if (this.node && this.node.isValid) {
@@ -235,12 +223,10 @@ export class MissionCardComp extends CCComp {
this.showSkillCardPopup();
}
mLogger.log(this.debugMode, "MissionCardComp", "mission start", {
poolLv: this.poolLv
});
mLogger.log(this.debugMode, "MissionCardComp", "mission start");
}
/** 任务结束:清空 4 槽 + 英雄面板并隐藏整个节点 */
/** 任务结束:清空 3 槽 + 英雄面板并隐藏整个节点 */
onMissionEnd() {
this.clearAllCards();
if (this.node && this.node.isValid) {
@@ -271,7 +257,7 @@ export class MissionCardComp extends CCComp {
* 绑定所有事件监听:
* - 节点级事件MissionStart / MissionEnd / NewWave / FightStart
* - 全局消息CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard
* - 按钮触控抽卡cards_chou、升级cards_up
* - 按钮触控抽卡cards_chou
*/
private bindEvents() {
/** 生命周期事件(节点级) */
@@ -288,10 +274,9 @@ export class MissionCardComp extends CCComp {
oops.message.on(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.on(GameEvent.CardPoolUpgrade, this.onCardPoolUpgrade, this);
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
/** 按钮触控事件:抽卡与卡池升级 */
/** 按钮触控事件:抽卡 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
@@ -303,9 +288,6 @@ 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.cards_up?.on(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
// this.cards_up?.on(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
// this.cards_up?.on(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
}
// ======================== 事件回调 ========================
@@ -333,47 +315,17 @@ export class MissionCardComp extends CCComp {
}
}
/**
* 接收卡池升级事件:
* - 更新卡池等级
* - 更新UI显示
*/
private onCardPoolUpgrade(event: string, args: any) {
const targetLv = args?.targetLv;
if (!targetLv) return;
if (targetLv > CARD_POOL_MAX_LEVEL) {
this.poolLv = CARD_POOL_MAX_LEVEL;
} else {
this.poolLv = targetLv;
}
mLogger.log(this.debugMode, "MissionCardComp", "onCardPoolUpgrade", {
targetLv,
poolLv: this.poolLv
});
// 提示卡池升级
this.showSmallTip("pool_upgrade");
// 更新UI
this.updatePoolLvUI();
}
private onShowSmallTip(event: string, args: any) {
const type = args as string;
this.showSmallTip(type as any);
}
public showSmallTip(type: "refresh_coin" | "pool_upgrade" | "buy_coin" | "hero_full") {
public showSmallTip(type: "refresh_coin" | "buy_coin" | "hero_full") {
let targetNode: Node | null = null;
switch (type) {
case "refresh_coin":
targetNode = this.cards_chou;
break;
case "pool_upgrade":
targetNode = this.pool_lv_node;
break;
case "buy_coin":
targetNode = this.coins_node;
break;
@@ -481,14 +433,6 @@ export class MissionCardComp extends CCComp {
this.skill_card_node.active = false;
}
// 首次完成技能选取后 关闭guide2打开guide3
// 之前这里有个逻辑漏洞:玩家如果在弹出 guide2 之前就已经因为手快点掉了 guide2
// smc.finish_guides 里就会有 2。
// 但其实这里的本意是:只要发生了选取技能,并且 guide3 还没弹过,就弹 guide3。
// 如果我们把它包在 `if (!smc.finish_guides.includes(2))` 里,
// 当玩家点击 guide2 把它关掉时finish_guides 存入了 2
// 再点技能卡触发这个方法,外层 if 就会进不去guide3 就永远弹不出来了!
// 修复:独立判断 guide2 的关闭 和 guide3 的开启
if (!smc.finish_guides.includes(2)) {
smc.finish_guides.push(2);
@@ -500,7 +444,6 @@ export class MissionCardComp extends CCComp {
}
// 驻场技能可能影响刷新费用(如"刷新优惠"),延迟到下一帧刷新费用 UI
// 确保 MissSkillsComp 已创建 SkillBoxComp 并注册驻场效果
this.scheduleOnce(() => this.updateCoinAndCostUI(), 0);
}
@@ -513,7 +456,6 @@ export class MissionCardComp extends CCComp {
oops.message.off(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.off(GameEvent.CardPoolUpgrade, this.onCardPoolUpgrade, this);
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
@@ -530,9 +472,6 @@ export class MissionCardComp extends CCComp {
this.skill_ad_refresh.off(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh.off(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
}
// this.cards_up?.off(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
// this.cards_up?.off(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
// this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
}
/**
@@ -575,28 +514,18 @@ export class MissionCardComp extends CCComp {
/**
* 使用英雄卡的 guard 校验(由 CardComp 通过 UseHeroCard 事件调用):
* - 当前英雄数 < 上限 → 允许使用。
* - 已满但新卡可触发合成(腾位) → 允许使用
* - 已满且不可合成 → 阻止使用cancel=true弹 toast。
* - 已满 → 阻止使用cancel=true弹 toast
*
* 注意:英雄不再支持合成腾位,满员时一律阻止。
*/
private onUseHeroCard(event: string, args: any) {
const payload = args ?? event;
if (!payload) return;
// 战斗阶段也允许召唤英雄(无需额外费用),仅校验英雄数量上限
const current = this.getAliveHeroCount();
this.syncMissionHeroData(current);
const heroMax = this.getMissionHeroMaxNum();
if (current >= heroMax) {
const heroUuid = Number(payload?.uuid ?? 0);
const heroLv = Math.max(1, Math.floor(Number(payload?.hero_lv ?? 1)));
const cardLv = Math.max(1, Math.floor(Number(payload?.pool_lv ?? 1)));
// 检查是否可以通过合成腾出位置
if (this.canUseHeroCardByMerge(heroUuid, heroLv)) {
payload.cancel = false;
payload.reason = "";
return;
}
payload.cancel = true;
payload.reason = "hero_limit";
this.showSmallTip("hero_full");
@@ -604,53 +533,11 @@ export class MissionCardComp extends CCComp {
}
}
/**
* 判断新召唤的英雄是否能通过合成腾位:
* 场上同 UUID 同等级数量 + 1新卡自身>= 合成所需数量 → 可以合成。
*/
private canUseHeroCardByMerge(heroUuid: number, heroLv: number): boolean {
if (!heroUuid) return false;
const mergeRule = this.getMergeRule();
if (heroLv >= mergeRule.maxLv) return false;
const sameCount = this.countAliveHeroesByUuidAndLv(heroUuid, heroLv);
return sameCount + 1 >= mergeRule.needCount;
}
/** 统计场上同 UUID 同等级的存活英雄数量 */
private countAliveHeroesByUuidAndLv(heroUuid: number, heroLv: number): number {
let count = 0;
const actors = this.queryAliveHeroActors();
for (let i = 0; i < actors.length; i++) {
const model = actors[i].model;
if (!model) continue;
if (model.hero_uuid !== heroUuid) continue;
if (model.lv !== heroLv) continue;
count += 1;
}
return count;
}
/**
* 从 MissionHeroComp 实时读取合成规则。
* 通过 ECS 查询获取,避免硬编码与 MissionHeroComp 不一致。
* @returns { needCount: 合成所需数量, maxLv: 最大合成等级 }
*/
private getMergeRule(): { needCount: number, maxLv: number } {
let needCount = FightSet.MERGE_NEED ? FightSet.MERGE_NEED : 2
let maxLv = Math.max(1, Math.floor(FightSet.MERGE_MAX ?? 3));
ecs.query(ecs.allOf(MissionHeroComp)).forEach((entity: ecs.Entity) => {
const comp = entity.get(MissionHeroComp);
if (!comp) return;
needCount = comp.merge_need_count === 2 ? 2 : 3;
maxLv = Math.max(1, Math.floor(comp.merge_max_lv ?? 3));
});
return { needCount, maxLv };
}
/**
* 使用特殊卡事件回调:
* - SpecialUpgrade随机选一个指定等级的英雄升级到目标等级
* - SpecialRefresh按英雄类型 / 指定等级重新抽取英雄
* - SpecialUpgrade按卡牌携带的 target_hero_uuid 精确升级场上对应英雄
* target_hero_uuid 缺失时回退为随机升级一个可升级的英雄。
* - SpecialRefresh按英雄类型重新抽取英雄卡。
*/
private onUseSpecialCard(event: string, args: any) {
const payload = args ?? event;
@@ -659,14 +546,17 @@ export class MissionCardComp extends CCComp {
if (!uuid) return;
let success = false;
if (type === CardType.SpecialUpgrade) {
const card = SpecialUpgradeCardList[uuid];
if (!card) return;
success = this.tryUpgradeOneHero(card.currentLv, card.targetLv);
if (!success) oops.gui.toast(`场上没有可从${card.currentLv}级升到${card.targetLv}级的英雄`);
const template = SpecialUpgradeCardList[uuid];
if (!template) return;
const targetHeroUuid = Number(payload?.target_hero_uuid ?? 0);
success = this.tryUpgradeHeroByUuid(targetHeroUuid);
if (!success) {
oops.gui.toast(`场上没有可升级的英雄`);
}
} else if (type === CardType.SpecialRefresh) {
const card = SpecialRefreshCardList[uuid];
if (!card) return;
success = this.tryRefreshHeroCardsByEffect(card.refreshHeroType, card.refreshLv);
success = this.tryRefreshHeroCardsByEffect(card.refreshHeroType);
if (!success) oops.gui.toast("当前卡池无符合条件的英雄卡");
}
mLogger.log(this.debugMode, "MissionCardComp", "use special card", {
@@ -731,20 +621,8 @@ export class MissionCardComp extends CCComp {
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
// /** 升级按钮按下反馈 */
// private onUpgradeTouchStart() {
// this.playButtonPressAnim(this.cards_up);
// }
// /** 升级按钮释放 → 执行升级逻辑 */
// private onUpgradeTouchEnd() {
// this.playButtonClickAnim(this.cards_up, () => this.onClickUpgrade());
// }
// /** 升级按钮取消 → 恢复缩放 */
// private onUpgradeTouchCancel() {
// this.playButtonResetAnim(this.cards_up);
// }
/** 将个卡槽节点映射为 CardComp形成固定顺序控制数组 */
/** 将个卡槽节点映射为 CardComp形成固定顺序控制数组 */
private cacheCardComps() {
if (this.card4) {
this.card4.active = false;
@@ -760,16 +638,15 @@ export class MissionCardComp extends CCComp {
.filter((comp): comp is SCardComp => !!comp);
}
// ======================== 核心业务:抽卡 & 升级 ========================
// ======================== 核心业务:抽卡 ========================
/**
* 抽卡按钮核心逻辑:
* 1. 检查金币是否足够 → 不够则 toast 提示。
* 2. 扣除费用、播放金币动画。
* 3. 重新布局槽位 → 从卡池构建 4 张卡 → 分发到槽位。
* 3. 重新布局槽位 → 从卡池构建 3 张卡 → 分发到槽位。
*/
private onClickDraw() {
// 战斗阶段和倒计时阶段均允许刷新抽卡
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
@@ -783,7 +660,6 @@ export class MissionCardComp extends CCComp {
}
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
poolLv: this.poolLv,
cost,
leftCoin: MissionEconomy.getCoin()
});
@@ -792,35 +668,6 @@ export class MissionCardComp extends CCComp {
this.dispatchCardsToSlots(cards);
}
// /** 升级按钮:仅提升卡池等级,卡槽是否更新由下一次抽卡触发 */
// private onClickUpgrade() {
// if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
// mLogger.log(this.debugMode, "MissionCardComp", "pool already max", this.poolLv);
// return;
// }
// const cost = this.getUpgradeCost(this.poolLv);
// const currentCoin = this.getMissionCoin();
// if (currentCoin < cost) {
// oops.gui.toast(`金币不足,升级需要${cost}`);
// this.updateCoinAndCostUI();
// mLogger.log(this.debugMode, "MissionCardComp", "pool upgrade coin not enough", {
// poolLv: this.poolLv,
// currentCoin,
// cost
// });
// return;
// }
// this.setMissionCoin(currentCoin - cost);
// this.poolLv += 1;
// this.playCoinChangeAnim(false);
// this.updateCoinAndCostUI();
// mLogger.log(this.debugMode, "MissionCardComp", "pool level up", {
// poolLv: this.poolLv,
// cost,
// leftCoin: this.getMissionCoin()
// });
// }
// ======================== 阶段切换 ========================
/** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */
@@ -853,7 +700,7 @@ export class MissionCardComp extends CCComp {
private enterBattlePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
// 战斗阶段允许抽卡nobg 按"金币是否足够"判断,而非强制置灰
// 战斗阶段允许抽卡nobg 按"金币是否足够"判断
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
@@ -862,30 +709,140 @@ export class MissionCardComp extends CCComp {
}
}
/** 构建本次抽卡结果保证最终可分发3条数据 */
/**
* 构建本次抽卡结果,保证最终可分发 3 条数据。
*
* 抽卡池构成:
* - 动态升级卡:扫描场上存活英雄,每个 UUID 至多生成一张升级卡
* (已达到 HERO_MAX_LV 的英雄不出卡)。同 UUID 只出一张,避免重复。
* - 基础英雄卡:所有英雄的 lv1 卡。
* - 刷新功能卡SpecialRefresh 卡。
*
* 三类卡按 weight 权重混合抽 3 张,且升级卡之间 unique 去重。
*
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,
* 不再抽取基础英雄卡和刷新卡,只抽取场上已有英雄的升级卡
* (若全部已满级则降级回混合池,避免卡池为空)。
*/
private buildDrawCards(): CardConfig[] {
const targetType = [CardType.Hero, CardType.SpecialRefresh];
const cards = getCardsByLv(this.poolLv, targetType);
const upgradeCards = this.buildHeroUpgradeCards();
const aliveHeroCount = this.getAliveHeroCount();
const heroMax = this.getMissionHeroMaxNum();
/** 正常情况下直接取前3 */
if (cards.length >= 3) return cards.slice(0, 3);
/** 兜底当返回不足3张时循环补齐保证分发不缺位 */
const filled = [...cards];
// 英雄已满员且有可升级的英雄 → 只出升级卡
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 refreshCards: CardConfig[] = Object.values(SpecialRefreshCardList);
const mixedPool: CardConfig[] = [...upgradeCards, ...heroCards, ...refreshCards];
if (mixedPool.length === 0) return [];
const picked = this.pickMixedCards(mixedPool, 3, upgradeCards);
if (picked.length >= 3) return picked.slice(0, 3);
/** 兜底:不足 3 张时循环补齐 */
const filled = [...picked];
while (filled.length < 3) {
const fallback = getCardsByLv(this.poolLv, targetType);
if (fallback.length === 0) break;
filled.push(fallback[filled.length % fallback.length]);
if (mixedPool.length === 0) break;
filled.push(mixedPool[filled.length % mixedPool.length]);
}
return filled;
}
/**
* 从混合池中按权重抽取 n 张卡。
* 升级卡之间强制 unique同一个 target_hero_uuid 只能出现一次)。
*/
private pickMixedCards(pool: CardConfig[], count: number, upgradeCards: CardConfig[]): CardConfig[] {
if (pool.length === 0 || count <= 0) return [];
const selected: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>();
while (selected.length < count) {
const available = pool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_uuid) {
return !usedUpgradeTargets.has(c.target_hero_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_uuid) {
usedUpgradeTargets.add(pick.target_hero_uuid);
}
}
return selected;
}
/** 单次按权重抽取一张卡 */
private 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 至多生成一张升级卡。
* - 已达 HERO_MAX_LV 的英雄不出卡。
* - 同 UUID 多个英雄只生成一张(取等级最低的那个作为升级目标)。
* - cost = 模板 cost + (当前等级 - 1) * BASE_COST等级越高升级越贵。
*/
private buildHeroUpgradeCards(): CardConfig[] {
const template = SpecialUpgradeCardList[7001];
if (!template) return [];
const actors = this.queryAliveHeroActors();
if (actors.length === 0) return [];
// 同 UUID 取等级最低的英雄
const heroMap = new Map<number, { uuid: number, lv: number }>();
for (const actor of actors) {
const uuid = actor.model.hero_uuid;
const lv = actor.model.lv;
const existing = heroMap.get(uuid);
if (!existing || lv < existing.lv) {
heroMap.set(uuid, { uuid, lv });
}
}
const result: CardConfig[] = [];
heroMap.forEach(({ uuid, lv }) => {
if (lv >= FightSet.HERO_MAX_LV) return; // 已达上限不再出升级卡
const dynamicCost = template.cost + (lv - 1) * FightSet.BASE_COST;
result.push({
...template,
cost: dynamicCost,
weight: template.weight,
target_hero_uuid: uuid,
hero_lv: lv + 1,
});
});
return result;
}
private buildSkillDrawCards(): CardConfig[] {
const targetType = CardType.Skill;
const currentWave = this.getCurrentWave();
// 使用明确规则的 drawCardsByRule指定只要 3 张技能卡,并且过滤对应 wave
const cards = drawCardsByRule(this.poolLv, {
const cards = drawCardsByRule(1, {
count: 3,
type: targetType,
type: CardType.Skill,
wave: currentWave,
unique: true // 保证技能牌不重复
});
@@ -893,9 +850,9 @@ export class MissionCardComp extends CCComp {
if (cards.length >= 3) return cards.slice(0, 3);
const filled = [...cards];
while (filled.length < 3) {
const fallback = drawCardsByRule(this.poolLv, {
const fallback = drawCardsByRule(1, {
count: 3,
type: targetType,
type: CardType.Skill,
wave: currentWave,
unique: true
});
@@ -912,12 +869,11 @@ export class MissionCardComp extends CCComp {
return filled;
}
private tryRefreshHeroCards(heroType?: HType, targetPoolLv?: number): boolean {
const cards = drawCardsByRule(this.poolLv, {
private tryRefreshHeroCards(heroType?: HType): boolean {
const cards = drawCardsByRule(1, {
count: 3,
type: CardType.Hero,
heroType,
targetPoolLv
});
if (cards.length <= 0) return false;
this.layoutCardSlots();
@@ -925,10 +881,9 @@ export class MissionCardComp extends CCComp {
return true;
}
private tryRefreshHeroCardsByEffect(refreshHeroType: SpecialRefreshHeroType, refreshLv: number): boolean {
private tryRefreshHeroCardsByEffect(refreshHeroType: SpecialRefreshHeroType): boolean {
const heroType = this.resolveRefreshHeroType(refreshHeroType);
const targetPoolLv = refreshLv > 0 ? refreshLv : undefined;
return this.tryRefreshHeroCards(heroType, targetPoolLv);
return this.tryRefreshHeroCards(heroType);
}
private resolveRefreshHeroType(refreshHeroType: SpecialRefreshHeroType): HType | undefined {
@@ -937,7 +892,7 @@ export class MissionCardComp extends CCComp {
return undefined;
}
/** 全量分发给4槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
/** 全量分发给 3 槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
private dispatchCardsToSlots(cards: CardConfig[]) {
if (!this.cardComps) return;
for (let i = 0; i < this.cardComps.length; i++) {
@@ -952,7 +907,7 @@ export class MissionCardComp extends CCComp {
}
}
/** 系统清空4槽(用于任务切换) */
/** 系统清空 3 槽(用于任务切换) */
private clearAllCards() {
if (!this.cardComps) return;
this.cardComps.forEach(comp => {
@@ -1001,77 +956,10 @@ export class MissionCardComp extends CCComp {
Tween.stopAllByTarget(node);
node.setScale(this.buttonNormalScale, this.buttonNormalScale, 1);
}
private canUpPool() {
if (this.poolLv >= CARD_POOL_MAX_LEVEL) return false;
const currentCoin = MissionEconomy.getCoin();
return currentCoin >= this.getUpgradeCost(this.poolLv);
}
private canDrawCards() {
return MissionEconomy.getCoin() >= MissionEconomy.getRefreshCost(this.refreshCost);
}
/** 更新升级按钮上的等级文案,反馈当前卡池层级 */
private updatePoolLvUI() {
if (this.pool_lv_node) {
this.pool_lv_node.active = true;
const lv = Math.max(CARD_POOL_INIT_LEVEL, Math.min(CARD_POOL_MAX_LEVEL, Math.floor(this.poolLv)));
const lvNode = this.pool_lv_node.getChildByName("lv");
if (lvNode) {
const label = lvNode.getComponent(Label);
if (label) {
label.string = `lv.${lv}`;
}
}
const nextNode = this.pool_lv_node.getChildByName("next");
if (nextNode) {
const nextLabel = nextNode.getComponent(Label);
if (nextLabel) {
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
nextLabel.string = `已满级`;
} else {
// 优先取 MissionComp 运行时配置,缺失时回退到全局常量
let upgradeWaves: number[] = CARD_POOL_UPGRADE_WAVES;
ecs.query(ecs.allOf(MissionComp)).forEach((entity) => {
const mission = entity.get(MissionComp);
if (mission && mission.cardPoolUpgradeWaves && mission.cardPoolUpgradeWaves.length > 0) {
upgradeWaves = mission.cardPoolUpgradeWaves;
}
});
// 已完成的升级次数 = 当前等级 - 初始等级
// 例poolLv=2INIT=1→ 已升 1 次 → 下一升级对应 upgradeWaves[1]
const upgradedCount = Math.max(0, Math.floor(this.poolLv) - CARD_POOL_INIT_LEVEL);
const currentWave = this.getCurrentWave();
if (upgradedCount >= upgradeWaves.length) {
// 配置已耗尽但等级未到上限(配置缺陷)
nextLabel.string = `已满级`;
} else {
const nextWave = upgradeWaves[upgradedCount];
if (nextWave > currentWave) {
const remain = nextWave - currentWave;
nextLabel.string = `${remain} 回合后升级`;
} else if (nextWave === currentWave) {
// 当前波次正好是升级波次(事件可能即将触发或刚刚触发)
nextLabel.string = `本回合升级`;
} else {
// nextWave < currentWave异常状态升级事件未按时触发
nextLabel.string = `即将升级`;
}
}
}
}
}
const peak = 1.2
this.playHeroNumNodePop(this.pool_lv_node, peak);
}
mLogger.log(this.debugMode, "MissionCardComp", "pool lv ui update", {
poolLv: this.poolLv,
cost: this.getUpgradeCost(this.poolLv)
});
}
private updateDrawCostUI() {
// 战斗阶段也允许抽卡nobg 统一按"金币是否足够"判断
@@ -1101,7 +989,6 @@ export class MissionCardComp extends CCComp {
}
private updateCoinAndCostUI() {
this.updatePoolLvUI();
this.updateDrawCostUI();
}
@@ -1116,13 +1003,6 @@ export class MissionCardComp extends CCComp {
this.playHeroNumNodePop(num, peak);
}
private getUpgradeCost(lv: number): number {
const baseCost = Math.max(0, Math.floor(CardsUpSet[lv] ?? 0));
const completedWave = Math.max(0, this.getCurrentWave() - 1);
const discount = Math.max(0, Math.floor(CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE)) * completedWave;
return Math.max(0, baseCost - discount);
}
public setHeroMaxCount(max: number) {
const missionData = this.getMissionData();
if (!missionData) return;
@@ -1171,28 +1051,42 @@ export class MissionCardComp extends CCComp {
return actors;
}
private tryUpgradeOneHero(currentLv: number, targetLv: number): boolean {
const fromLv = Math.max(1, Math.floor(currentLv));
const toLv = Math.max(1, Math.floor(targetLv));
if (toLv <= fromLv) return false;
const candidates = this.queryAliveHeroActors().filter(item => item.model.lv === fromLv);
/**
* 按英雄 UUID 精确升级一个存活英雄。
* 若同 UUID 多个英雄,取等级最低且未达上限的;若都达上限则失败。
*
* @param heroUuid 要升级的英雄 UUID
* @returns true = 升级成功
*/
private tryUpgradeHeroByUuid(heroUuid: number): boolean {
if (!heroUuid) return false;
const candidates = this.queryAliveHeroActors().filter(item =>
item.model.hero_uuid === heroUuid && item.model.lv < FightSet.HERO_MAX_LV
);
if (candidates.length === 0) return false;
const target = candidates[Math.floor(Math.random() * candidates.length)];
this.applyHeroLevel(target.model, toLv);
// 取等级最低的(先升级低等级英雄)
candidates.sort((a, b) => a.model.lv - b.model.lv);
const target = candidates[0];
const nextLv = Math.min(FightSet.HERO_MAX_LV, target.model.lv + 1);
this.applyHeroLevel(target.model, nextLv);
if (target.view) {
target.view.playBuff("buff_lvup");
}
return true;
}
/**
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
*/
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
const hero = HeroInfo[model.hero_uuid];
if (!hero) return;
const nextLv = Math.max(1, Math.min(3, Math.floor(targetLv)));
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
const hpRate = model.hp_max > 0 ? model.hp / model.hp_max : 1;
model.lv = nextLv;
model.ap = hero.ap * nextLv;
model.hp_max = hero.hp * nextLv;
model.ap = hero.ap * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
model.hp_max = hero.hp * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
model.hp = Math.max(1, Math.floor(model.hp_max * Math.max(0, Math.min(1, hpRate))));
model.skills = {};
for (const key in hero.skills) {
@@ -1266,17 +1160,12 @@ export class MissionCardComp extends CCComp {
return Math.max(0, Math.floor(missionData?.hero_num ?? 0));
}
private getCurrentWave(): number {
const missionData = this.getMissionData();
return Math.max(1, Math.floor(missionData?.level ?? 1));
}
private getMissionHeroMaxNum(): number {
return FightSet.HERO_MAX_NUM
}
@@ -1290,7 +1179,6 @@ export class MissionCardComp extends CCComp {
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.resetButtonScale(this.cards_chou);
// this.resetButtonScale(this.cards_up);
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.cardComps = [] as any;

View File

@@ -39,7 +39,7 @@ import { HeroViewComp } from "../hero/HeroViewComp";
import { SkillTriggerHelper } from "../hero/SkillTriggerHelper";
import { UIID } from "../common/config/GameUIConfig";
import { SkillView } from "../skill/SkillView";
import { FacSet, FightSet, CARD_POOL_UPGRADE_WAVES } from "../common/config/GameSet";
import { FacSet, FightSet } from "../common/config/GameSet";
import { HeroInfo } from "../common/config/heroSet";
import { mLogger } from "../common/Logger";
import { Monster } from "../hero/Mon";
@@ -86,8 +86,6 @@ export class MissionComp extends CCComp {
private maxMonsterCount: number = 80;
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
private resumeMonsterCount: number = 45;
/** 卡池升级波次配置(默认值来自 GameSet.CARD_POOL_UPGRADE_WAVES保持全局统一 */
public cardPoolUpgradeWaves: number[] = CARD_POOL_UPGRADE_WAVES;
// ======================== 编辑器绑定节点 ========================
@@ -829,22 +827,6 @@ export class MissionComp extends CCComp {
this.lastTimeSecond = -1;
this.clearTime = 0;
this.update_time();
// 检查并推送卡池升级事件
this.checkCardPoolUpgrade(wave);
}
/** 检查是否达到卡池升级波次,并推送升级事件 */
private checkCardPoolUpgrade(wave: number) {
if (!this.cardPoolUpgradeWaves || this.cardPoolUpgradeWaves.length === 0) return;
const upgradeIndex = this.cardPoolUpgradeWaves.indexOf(wave);
if (upgradeIndex !== -1) {
// 根据配置的索引,计算目标等级(初始等级 + index + 1
// 例如 index=0对应等级为2index=1对应等级为3
const targetLv = upgradeIndex + 2;
oops.message.dispatchEvent(GameEvent.CardPoolUpgrade, { wave, targetLv });
mLogger.log(this.debugMode, 'MissionComp', "card pool upgrade event pushed", { wave, targetLv });
}
}
// ======================== 怪物数量管理 ========================

View File

@@ -1,29 +1,24 @@
/**
* @file MissionHeroComp.ts
* @description 英雄召唤与合成管理组件(逻辑层 + 视图层)
* @description 英雄召唤管理组件(逻辑层 + 视图层)
*
* 职责:
* 1. 处理 **英雄召唤**:接收 CallHero 事件 → 通过串行队列执行召唤。
* 2. 处理 **英雄合成**:检测同 UUID 同等级英雄是否达到合成条件 →
* 执行合成动画 → 销毁素材 → 生成高一级英雄。
* 3. 支持 **链式合成**:合成完成后自动检测更高等级是否也满足合成条件。
* 4. 管理英雄的出生点和掉落动画。
* 2. 管理英雄的出生点和掉落动画。
*
* 关键设计:
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**
* 避免同帧并发导致合成判断错误
* - handleSingleSummon() 在每次召唤后检测是否触发合成。
* - mergeGroupHeroes() 执行完整合成流程
* 聚合属性 → 向出生点汇聚动画 → 爆点特效 → 生成高级英雄
* - merge_need_count 控制合成所需数量2 合 1 或 3 合 1
* - merge_max_lv 控制合成上限等级。
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**
* - handleSingleSummon() 仅负责生成英雄英雄升级由升级卡系统MissionCardComp独立处理
*
* 历史
* 旧版本曾包含"三合一合成 + 链式合成"机制,已移除
* 英雄等级提升现在仅通过升级卡SpecialUpgrade实现
*
* 依赖:
* - Herohero/Hero.ts—— 英雄 ECS 实体类
* - HeroAttrsComp —— 英雄属性组件
* - HeroInfo / HeroPos / HTypeheroSet—— 英雄静态配置
* - FightSet —— 战斗常量MERGE_NEED / MERGE_MAX
* - oneCom —— 一次性特效组件(控制爆点特效生命周期)
* - FightSet —— 战斗常量
*/
import { _decorator, instantiate, Prefab, v3, Vec3, BoxCollider2D } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
@@ -36,17 +31,16 @@ import { HeroInfo, HeroPos, HType } from "../common/config/heroSet";
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { FacSet, FightSet, BoxSet } from "../common/config/GameSet";
import { oneCom } from "../skill/oncend";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FieldSkillSet, FieldSkillType } from "../common/config/SkillSet";
import { MoveComp } from "../hero/MoveComp";
const { ccclass } = _decorator;
/**
* MissionHeroComp —— 英雄召唤与合成管理器
* MissionHeroComp —— 英雄召唤管理器
*
* 管理英雄的召唤请求队列出生动画和合成系统
* 合成支持 2 合 1 或 3 合 1且可链式合成至上限等级
* 管理英雄的召唤请求队列出生动画。
* 英雄升级由 MissionCardComp 的升级卡系统统一处理
*/
@ccclass('MissionHeroComp')
@ecs.register('MissionHeroComp', false)
@@ -76,15 +70,9 @@ export class MissionHeroComp extends CCComp {
current_hero_uuid:number=0
/** 当前英雄数量缓存 */
current_hero_num:number=-1
/** 合成规则需要几个同级英雄才能合成2 或 3 */
merge_need_count:number=FightSet.MERGE_NEED
/** 允许合成的最高等级(合成产物不超过此等级) */
merge_max_lv:number=FightSet.MERGE_MAX
/** 是否正在执行一次合成流程(防止并发) */
is_merging:boolean=false
/** 是否正在消费召唤队列(防止并发) */
is_processing_queue:boolean=false
/** 召唤请求队列:保证召唤与合成按顺序串行执行 */
/** 召唤请求队列:保证召唤按顺序串行执行 */
summon_queue:{ uuid: number; hero_lv: number; pool_lv: number }[]=[]
/** 预留英雄列表 */
heros:any=[]
@@ -134,8 +122,7 @@ export class MissionHeroComp extends CCComp {
view.alive();
const posIndex = this.pickPositionIndexForHero([hero.eid]);
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
// 不再直接设置位置,而是播放下落入场动画
// 计算出出生点(空中)
// 计算出生点(空中)
const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
view.node.setPosition(spawnPos);
model.posIndex = posIndex;
@@ -172,7 +159,7 @@ export class MissionHeroComp extends CCComp {
/**
* 动态分配英雄上场的位置
* @param excludeEids 排除计算的实体ID数组避免复活或合成时把自己算成占据的位置)
* @param excludeEids 排除计算的实体ID数组避免复活时把自己算成占据的位置
*/
private pickPositionIndexForHero(excludeEids: number[] = []): number {
const heroes = this.getAllHeroes().filter(h => {
@@ -205,11 +192,10 @@ export class MissionHeroComp extends CCComp {
*
* @param uuid 英雄 UUID
* @param hero_lv 英雄等级
* @param pool_lv 卡池等级
* @param pool_lv 卡池等级(历史遗留,新机制下不再使用)
* @returns 创建的 Hero 实体
*/
private addHero(uuid:number=1001,hero_lv:number=1, pool_lv:number=1) {
console.log("addHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const posIndex = this.pickPositionIndexForHero();
@@ -229,56 +215,6 @@ export class MissionHeroComp extends CCComp {
return hero;
}
/**
* 生成合成后的高级英雄,并覆盖为聚合后的属性。
*
* @param uuid 英雄 UUID
* @param hero_lv 合成后等级
* @param pool_lv 卡池等级
* @param ap 聚合后攻击力
* @param hp_max 聚合后最大生命值
* @param targetPos 指定生成位置
* @returns 实际生成的英雄等级
*/
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPosIndex?: number, targetPos?: Vec3): number {
console.log("addMergedHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
let posIndex = targetPosIndex;
let landingPos = targetPos;
if (posIndex === undefined || posIndex < 0 || !landingPos) {
posIndex = this.pickPositionIndexForHero();
landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
}
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
if (model) {
model.ap = Math.max(0, ap);
model.hp_max = Math.max(1, hp_max);
model.hp = model.hp_max;
model.dirty_hp = true;
// 获取视图组件触发升级特效(包含描边更新)
const view = hero.get(HeroViewComp);
if (view && typeof view['lv_up'] === 'function') {
view['lv_up']();
}
oops.message.dispatchEvent(GameEvent.MasterCalled, {
eid: hero.eid,
model: model
});
return model.lv;
}
return hero_lv;
}
// ======================== 英雄查询 ========================
/** 获取当前全部友方英雄 ECS 实体列表(包括存活和墓地) */
@@ -293,62 +229,6 @@ export class MissionHeroComp extends CCComp {
return heroes;
}
/**
* 从存活英雄中挑选可参与本次合成的英雄组。
*
* @param aliveHeroes 存活英雄列表
* @param uuid 目标英雄 UUID
* @param hero_lv 目标等级
* @param needCount 合成需要数量
* @returns 匹配的英雄数组(长度 = needCount 或不足)
*/
private pickMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number, needCount: number = 3): Hero[] {
const mergeHeroes: Hero[] = [];
for (let i = 0; i < aliveHeroes.length; i++) {
const model = aliveHeroes[i].get(HeroAttrsComp);
if (!model) continue;
if (model.hero_uuid !== uuid) continue;
if (model.lv !== hero_lv) continue;
mergeHeroes.push(aliveHeroes[i]);
if (mergeHeroes.length === needCount) break;
}
return mergeHeroes;
}
/** 统计满足同 UUID 同等级的可合成英雄数量 */
private countMergeHeroes(aliveHeroes: Hero[], uuid: number, hero_lv: number): number {
let count = 0;
for (let i = 0; i < aliveHeroes.length; i++) {
const model = aliveHeroes[i].get(HeroAttrsComp);
if (!model) continue;
if (model.hero_uuid !== uuid) continue;
if (model.lv !== hero_lv) continue;
count += 1;
}
return count;
}
// ======================== 合成规则 ========================
/**
* 读取合成所需数量(仅支持 2 或 3
* 由 FightSet.MERGE_NEED 配置。
*/
private getMergeNeedCount(): number {
return this.merge_need_count === 2 ? 2 : 3;
}
/**
* 判断该等级是否还能继续向上合成。
* @param hero_lv 当前等级
* @returns true = 可以合成(未达上限)
*/
private canMergeLevel(hero_lv: number): boolean {
return hero_lv < Math.max(1, this.merge_max_lv);
}
// ======================== 召唤队列 ========================
/**
@@ -371,159 +251,16 @@ export class MissionHeroComp extends CCComp {
}
/**
* 处理单次召唤:
* 1. 生成英雄。
* 2. 检测是否满足合成条件。
* 3. 满足则执行合成 + 链式合成。
* 处理单次召唤:仅生成英雄,不再触发合成。
*
* @param uuid 英雄 UUID
* @param hero_lv 英雄等级
* @param pool_lv 卡池等级
* @param pool_lv 卡池等级(历史遗留)
*/
private async handleSingleSummon(uuid: number, hero_lv: number, pool_lv: number = 1) {
this.addHero(uuid, hero_lv, pool_lv);
if (!this.canMergeLevel(hero_lv)) return;
const needCount = this.getMergeNeedCount();
const aliveHeroes = this.getAllHeroes();
const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, hero_lv, needCount);
if (mergeHeroes.length !== needCount) return;
this.is_merging = true;
try {
const mergedLv = await this.mergeGroupHeroes(mergeHeroes, uuid, hero_lv, pool_lv);
await this.tryChainMerge(uuid, mergedLv, pool_lv);
} finally {
this.is_merging = false;
}
}
// ======================== 合成动画 ========================
/**
* 将一组合成素材英雄向出生点汇聚并销毁。
* 所有素材动画完成后 Promise resolve。
*
* @param mergeHeroes 合成素材英雄数组
* @param spawnPos 汇聚目标位置
*/
private mergeDestroyAtBirth(mergeHeroes: Hero[], spawnPos: Vec3): Promise<void> {
return new Promise((resolve) => {
let doneCount = 0;
const total = mergeHeroes.length;
if (total <= 0) {
resolve();
return;
}
const onDone = () => {
doneCount += 1;
if (doneCount >= total) {
resolve();
}
};
for (let i = 0; i < mergeHeroes.length; i++) {
mergeHeroes[i].mergeToBirthAndDestroy(spawnPos, onDone);
}
});
}
/**
* 播放合成爆点特效(使用 oneCom 控制生命周期)。
* 延迟 0.4 秒后 resolve。
*
* @param worldPos 特效播放位置
*/
private playMergeBoomFx(worldPos: Vec3): Promise<void> {
return new Promise((resolve) => {
const scene = smc.map?.MapView?.scene;
const layer = scene?.entityLayer?.node;
if (!layer || !layer.isValid) {
resolve();
return;
}
const prefab: Prefab = oops.res.get("game/skill/end/dead", Prefab)!;
if (!prefab) {
resolve();
return;
}
const fx = instantiate(prefab);
if (!fx || !fx.isValid) {
resolve();
return;
}
fx.parent = layer;
fx.setPosition(worldPos);
fx.getComponent(oneCom) || fx.addComponent(oneCom);
this.scheduleOnce(() => resolve(), 0.4);
});
}
/**
* 执行一次完整合成流程:
* 1. 聚合素材的 AP 和 HP。
* 2. 将素材向出生点汇聚并销毁。
* 3. 播放爆点特效。
* 4. 生成高一级英雄(属性为聚合值)。
*
* @param mergeHeroes 合成素材
* @param uuid 英雄 UUID
* @param hero_lv 素材等级
* @param pool_lv 卡池等级
* @returns 合成产物的实际等级
*/
private async mergeGroupHeroes(mergeHeroes: Hero[], uuid: number, hero_lv: number, pool_lv: number): Promise<number> {
// 聚合属性
let sumAp = 0;
let sumHpMax = 0;
const mergeEids = [];
for (let i = 0; i < mergeHeroes.length; i++) {
const model = mergeHeroes[i].get(HeroAttrsComp);
mergeEids.push(mergeHeroes[i].eid);
if (!model) continue;
sumAp += model.ap;
sumHpMax += model.hp_max;
}
const posIndex = this.pickPositionIndexForHero(mergeEids);
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
// 汇聚 → 特效 → 生成
await this.mergeDestroyAtBirth(mergeHeroes, spawnPos);
await this.playMergeBoomFx(spawnPos);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, posIndex, landingPos);
}
/**
* 链式合成:合成完成后继续检测更高等级是否也满足条件。
* 最多循环 20 次作为安全上限。
*
* @param uuid 英雄 UUID
* @param startLv 起始检测等级
* @param pool_lv 卡池等级
*/
private async tryChainMerge(uuid: number, startLv: number, pool_lv: number) {
let checkLv = Math.max(1, startLv);
const needCount = this.getMergeNeedCount();
let guard = 0;
while (guard < 20) {
guard += 1;
if (!this.canMergeLevel(checkLv)) {
break;
}
const aliveHeroes = this.getAllHeroes();
const sameCount = this.countMergeHeroes(aliveHeroes, uuid, checkLv);
if (sameCount < needCount) {
break;
}
const mergeHeroes = this.pickMergeHeroes(aliveHeroes, uuid, checkLv, needCount);
if (mergeHeroes.length < needCount) {
break;
}
checkLv = await this.mergeGroupHeroes(mergeHeroes, uuid, checkLv, pool_lv);
}
}
/** ECS 组件移除时触发(当前不销毁节点,保留引用) */
reset() {