feat(game): 调整英雄最大数量与合成升级规则

1. 将英雄最大数量从3提升至10
2. 重构英雄等级上限逻辑,改为从配置项读取
3. 新增同uuid英雄二合一合成升级机制,等级封顶为配置上限
4. 优化召唤校验逻辑,支持同uuid英雄合成并提示等级上限
This commit is contained in:
pan
2026-08-12 15:10:29 +08:00
parent ccce8a11ea
commit db98b60737
3 changed files with 92 additions and 20 deletions

View File

@@ -29,7 +29,7 @@ export enum FightSet {
CRIT_DAMAGE = 50,//暴击伤害
MORE_RC = 10,//更多次数 广告获取的次数
HEARTPOS = -320,//基地位置
HERO_MAX_NUM = 3,//英雄最大数量
HERO_MAX_NUM = 10,//英雄最大数量
/** 英雄可升级到的最高等级(通过升级卡提升) */
HERO_MAX_LV = 6,
/** 英雄技能等级上限(无限升级下技能 5 级封顶,超出等级只长属性) */

View File

@@ -76,8 +76,8 @@ export class Hero extends ecs.Entity {
load(pos: Vec3 = Vec3.ZERO, scale: number = 1, uuid: number = 1001, dropToY: number = pos.y, hero_lv: number = 1, card_lv: number = 1, posIndex: number = -1) {
// 英雄始终朝右,表现缩放固定为正向
scale = 1
// 英雄等级在当前规则下上限为 3避免超配表范围
if (hero_lv > 3) hero_lv = 3
// 英雄等级上限以配置为准,合成可升到 HERO_MAX_LV
if (hero_lv > FightSet.HERO_MAX_LV) hero_lv = FightSet.HERO_MAX_LV
// 英雄尺寸随等级做轻量放大,强化成长反馈
let size = 1.15
// 根据配置路径加载英雄预制体

View File

@@ -8,11 +8,12 @@
*
* 关键设计:
* - summon_queue + processSummonQueue() 确保召唤请求 **串行处理**。
* - handleSingleSummon() 负责生成英雄;英雄升级由升级卡系统MissionCardComp独立处理
* - handleSingleSummon() 负责生成英雄;同 uuid 重复召唤走二合一合成升级
*
* 历史
* 旧版本曾包含"三合一合成 + 链式合成"机制,已移除。
* 英雄等级提升现在仅通过升级卡SpecialUpgrade实现。
* 合成规则
* 场上存在同 uuid 英雄时再次召唤触发二合一:
* 英雄移动合并销毁,新英雄以 **两者最高等级 + 1** 落地(封顶 HERO_MAX_LV
* 等级已达上限时拒绝召唤。英雄也可通过升级卡SpecialUpgrade升级。
*
* 依赖:
* - Herohero/Hero.ts—— 英雄 ECS 实体类
@@ -142,7 +143,8 @@ export class MissionHeroComp extends CCComp {
/**
* 召唤请求入口:
* 从事件参数中提取 uuid / hero_lv / card_lv放入串行队列。
* 防御:已召唤的英雄 uuid 拒绝重复召唤(只能通过升级卡升级)。
* 二合一:同 uuid 已召唤且等级未达上限时允许入队,由 handleSingleSummon 执行合成;
* 同 uuid 且等级已达上限时拒绝(只能通过升级卡继续提升之外无法再合成)。
*
* @param event 事件名
* @param args { uuid, hero_lv, card_lv }
@@ -153,10 +155,14 @@ export class MissionHeroComp extends CCComp {
const hero_lv = Math.max(1, Number(payload?.hero_lv ?? 1));
const card_lv = Math.max(1, Number(payload?.card_lv ?? 1));
// 防御:场上已有同 uuid 的存活英雄时,拒绝重复召唤
// 二合一前置校验:同 uuid 已存在且合成后等级会超上限时,直接拒绝
if (this.isHeroAlreadySummoned(uuid)) {
oops.gui.toast(`该英雄已召唤,只能通过升级卡升级`);
return;
const existing = this.findHeroByUuid(uuid);
const existingLv = existing ? (existing.get(HeroAttrsComp)?.lv ?? 1) : 1;
if (Math.max(existingLv, hero_lv) + 1 > FightSet.HERO_MAX_LV) {
oops.gui.toast(`该英雄已达等级上限,无法继续合成`);
return;
}
}
this.summon_queue.push({ uuid, hero_lv, card_lv });
@@ -169,14 +175,23 @@ export class MissionHeroComp extends CCComp {
* @returns true 表示场上已有该英雄,不可重复召唤
*/
private isHeroAlreadySummoned(uuid: number): boolean {
let exists = false;
return this.findHeroByUuid(uuid) !== null;
}
/**
* 查找场上指定 uuid 的存活英雄实体。
* @param uuid 英雄模板 uuid
* @returns 命中的 Hero 实体,未命中返回 null
*/
private findHeroByUuid(uuid: number): Hero | null {
let found: Hero | null = null;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
if (exists) return;
if (found) return;
const model = entity.get(HeroAttrsComp);
if (!model || model.fac !== FacSet.HERO || model.is_dead) return;
if (model.hero_uuid === uuid) exists = true;
if (model.hero_uuid === uuid) found = entity as Hero;
});
return exists;
return found;
}
// ======================== 英雄生成 ========================
@@ -220,12 +235,27 @@ export class MissionHeroComp extends CCComp {
* @returns 创建的 Hero 实体
*/
private addHero(uuid: number = 1001, hero_lv: number = 1, card_lv: number = 1) {
return this.spawnHeroAt(uuid, hero_lv, card_lv, -1);
}
/**
* 在指定站位生成英雄:
* - posIndex < 0 时自动分配空位。
* - 计算出生点(空中)和落点(地面),播放掉落动画。
*
* @param uuid 英雄 UUID
* @param hero_lv 英雄等级
* @param card_lv 卡牌等级(驱动 bg_node 颜色)
* @param posIndex 指定站位索引,-1 表示自动分配
* @returns 创建的 Hero 实体
*/
private spawnHeroAt(uuid: number, hero_lv: number, card_lv: number, posIndex: number) {
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const posIndex = this.pickPositionIndexForHero();
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
const finalPosIndex = posIndex >= 0 ? posIndex : this.pickPositionIndexForHero();
const landingPos = MissionHeroComp.HERO_POSITIONS[finalPosIndex];
let spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, posIndex);
hero.load(spawnPos, scale, uuid, landingPos.y, hero_lv, card_lv, finalPosIndex);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
@@ -275,14 +305,56 @@ export class MissionHeroComp extends CCComp {
}
/**
* 处理单次召唤:仅生成英雄,不再触发合成。
* 处理单次召唤:
* - 场上无同 uuid 英雄:直接生成新英雄。
* - 场上已有同 uuid 英雄:触发二合一,旧英雄合并销毁后生成等级 +1 的新英雄。
*
* @param uuid 英雄 UUID
* @param hero_lv 英雄等级
* @param card_lv 卡牌等级
*/
private async handleSingleSummon(uuid: number, hero_lv: number, card_lv: number = 1) {
this.addHero(uuid, hero_lv, card_lv);
const existing = this.findHeroByUuid(uuid);
if (!existing) {
this.addHero(uuid, hero_lv, card_lv);
return;
}
const oldModel = existing.get(HeroAttrsComp);
const oldLv = oldModel?.lv ?? 1;
// 合成后等级 = 两者最高等级 + 1封顶 HERO_MAX_LV
const mergedLv = Math.min(FightSet.HERO_MAX_LV, Math.max(oldLv, hero_lv) + 1);
// 继承旧英雄站位,避免新英雄重新抢位导致阵型跳动
const posIndex = oldModel?.posIndex ?? -1;
await this.mergeHeroAndRespawn(existing, uuid, mergedLv, card_lv, posIndex);
}
/**
* 二合一合成流程:
* 1. 旧英雄播放移动合并动画并销毁(释放站位与实体)。
* 2. 在原站位生成 mergedLv 级新英雄并播放入场动画。
*
* @param oldHero 场上已有的同 uuid 英雄
* @param uuid 英雄 UUID
* @param mergedLv 合成后的英雄等级
* @param card_lv 卡牌等级
* @param posIndex 继承的站位索引(-1 表示重新分配)
*/
private mergeHeroAndRespawn(oldHero: Hero, uuid: number, mergedLv: number, card_lv: number, posIndex: number): Promise<void> {
return new Promise<void>((resolve) => {
// 以旧英雄节点位置作为合并目标点,保证动画收拢到原站位
const oldView = oldHero.get(HeroViewComp);
const birthPos = oldView?.node?.isValid
? oldView.node.getPosition()
: (posIndex >= 0 ? MissionHeroComp.HERO_POSITIONS[posIndex] : MissionHeroComp.HERO_POSITIONS[1]);
oldHero.mergeToBirthAndDestroy(birthPos, () => {
// 旧实体销毁后 posIndex 已释放,直接按原站位生成新英雄
this.spawnHeroAt(uuid, mergedLv, card_lv, posIndex);
resolve();
});
});
}