refactor(hero): 重构英雄死亡与复活机制

1. 统一英雄真实死亡流程:死亡时快照数据到全局列表,动画结束后统一销毁实体
2. 移除回合自动复活逻辑,改为由复活系统主动读取快照重新召唤
3. 修复近战追敌可能脱离战场的问题,增加推进距离上限
4. 重构死亡计数与全员存活检测逻辑,基于死亡快照列表而非实体状态
This commit is contained in:
pan
2026-08-12 16:15:33 +08:00
parent ab6a3b7824
commit 3060ee7df7
6 changed files with 79 additions and 42 deletions

View File

@@ -38,6 +38,23 @@ export interface CloudData {
openid: string; openid: string;
data: GameDate; data: GameDate;
} }
/**
* 死亡英雄快照:英雄真实死亡时记录,供后续复活机制(重新召唤)消费。
* 仅记录静态/成长数据不含战斗内临时状态buff、CD 等)。
*/
export interface DeadHeroRecord {
/** 英雄模板 uuid决定重新召唤哪个英雄 */
uuid: number;
/** 死亡时的英雄等级(复活后继承) */
lv: number;
/** 卡牌等级(驱动 bg_node 颜色) */
card_lv: number;
/** 死亡时占用的站位索引(复活时优先归位,-1 表示未分配) */
posIndex: number;
/** 死亡时的回合数(用于统计/按回合清理) */
wave: number;
}
/** 游戏模块 */ /** 游戏模块 */
@ecs.register('SingletonModule') @ecs.register('SingletonModule')
export class SingletonModuleComp extends ecs.Comp { export class SingletonModuleComp extends ecs.Comp {
@@ -60,6 +77,8 @@ export class SingletonModuleComp extends ecs.Comp {
stop_mon_action: false, stop_mon_action: false,
heroGrid: [-1, -1, -1, -1, -1, -1], heroGrid: [-1, -1, -1, -1, -1, -1],
monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1], monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
/** 本局真实死亡的英雄快照列表,供后续复活机制消费;整局结束(MissionEnd)时清空 */
dead_heroes: [] as DeadHeroRecord[],
}; };
data: any = { data: any = {
openid: '', openid: '',

View File

@@ -220,10 +220,11 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
// 检查死亡 // 检查死亡
if (TAttrsComp.hp <= 0) { if (TAttrsComp.hp <= 0) {
// 先触发死亡技能(如亡语),不管后续是否复活都应触发 // 先触发死亡技能(如亡语),不管后续是否续命都应触发
this.triggerDeadSkills(target); this.triggerDeadSkills(target);
// 复活机制:如果玩家属性内的复活属性值>=1 则执行复活,原地50%血量复活 // 濒死回血revive 系):不是真正复活,而是血量归零瞬间消耗 1 次续命次数,
// 按比例回血继续战斗。次数每回合重置(见 MissionComp.healAllHeroes
let canRevive = false; let canRevive = false;
let maxReviveCount = 0; let maxReviveCount = 0;
let reviveHpPercent = 50; // 默认恢复50% let reviveHpPercent = 50; // 默认恢复50%
@@ -338,16 +339,16 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
/** /**
* 处理角色死亡 * 处理角色死亡
* *
* 死亡处理流程: * 死亡处理流程:
* 1. 标记死亡状态is_dead = true * 1. 标记死亡状态is_dead = true
* 2. 触发死亡事件onDeath * 2. 英雄阵营:快照死亡状态到 smc.mission.dead_heroes供后续复活机制重新召唤
* 3. 记录调试信息(如启用调试模式 * 3. 触发死亡事件onDeath
* *
* @param entity 死亡的实体 * @param entity 死亡的实体
* *
* @important 死亡状态一旦设置,该实体将不再处理新的伤害事件 * @important 死亡状态一旦设置,该实体将不再处理新的伤害事件
* 这确保了死亡逻辑的单一性和一致性 * 英雄为真实死亡:视图动画结束后由 HeroViewComp.realDead 销毁实体
*/ */
private doDead(entity: ecs.Entity): void { private doDead(entity: ecs.Entity): void {
const TAttrsComp = entity.get(HeroAttrsComp); const TAttrsComp = entity.get(HeroAttrsComp);
@@ -355,6 +356,11 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
TAttrsComp.is_dead = true; TAttrsComp.is_dead = true;
// 英雄真实死亡:记录快照,供后续复活机制(重新召唤)消费
if (TAttrsComp.fac === FacSet.HERO) {
this.recordDeadHero(TAttrsComp);
}
// 触发死亡事件 // 触发死亡事件
this.onDeath(entity); this.onDeath(entity);
@@ -363,6 +369,24 @@ export class HeroAtkSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
} }
} }
/**
* 快照死亡英雄的静态/成长数据到 smc.mission.dead_heroes。
* 仅记录重新召唤所需字段不含战斗内临时状态buff、技能 CD 等)。
*
* @param attrs 死亡英雄的属性组件
*/
private recordDeadHero(attrs: HeroAttrsComp): void {
smc.mission.dead_heroes.push({
uuid: attrs.hero_uuid,
lv: attrs.lv,
card_lv: attrs.card_lv,
posIndex: attrs.posIndex,
wave: Math.max(1, smc.vmdata?.mission_data?.level ?? 1),
});
mLogger.log(this.debugMode, 'HeroAtkSystem',
` 死亡英雄已记录: ${attrs.hero_name}(uuid=${attrs.hero_uuid}) lv=${attrs.lv} pos=${attrs.posIndex} wave=${smc.vmdata?.mission_data?.level}, 当前共 ${smc.mission.dead_heroes.length}`);
}
/** /**

View File

@@ -516,17 +516,12 @@ export class HeroViewComp extends CCComp {
let isHero = this.model.fac === FacSet.HERO; let isHero = this.model.fac === FacSet.HERO;
let dirX = isHero ? -800 : 800; let dirX = isHero ? -800 : 800;
// 死亡往后飞出屏幕动画 // 死亡往后飞出屏幕动画,结束后统一销毁实体
// 英雄为真实死亡:快照已在 doDead 阶段写入 smc.mission.dead_heroes此处直接销毁
tween(this.node) tween(this.node)
.by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" }) .by(0.5, { position: v3(dirX, 700, 0) }, { easing: "quadOut" })
.call(() => { .call(() => {
if (isHero) { this.ent.destroy();
// 将英雄移到玩家看不到的墓地,留待下回合上场
this.node.setPosition(v3(-2000, -2000, 0));
} else {
// 动画结束后销毁怪物实体
this.ent.destroy();
}
}) })
.start(); .start();
} }

View File

@@ -223,7 +223,8 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
// 未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下 // 未贴近:向怪物方向推进(攻击范围内也继续移动),于接触距离处停下
const dir = enemyX > selfX ? 1 : -1; const dir = enemyX > selfX ? 1 : -1;
move.direction = dir; move.direction = dir;
const stopX = enemyX - dir * MoveSystem.MELEE_ENGAGE_X; // 终止点取接触距离与推进上限的较小者,防止越过 300 继续追击
const stopX = Math.min(enemyX - dir * MoveSystem.MELEE_ENGAGE_X, MoveSystem.HERO_ADVANCE_LIMIT_X);
const speed = model.speed / 3; const speed = model.speed / 3;
this.moveEntity(view, dir, speed, stopX); this.moveEntity(view, dir, speed, stopX);
model.is_atking = false; model.is_atking = false;
@@ -290,6 +291,8 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private static readonly FORMATION_STEP_X = 60; private static readonly FORMATION_STEP_X = 60;
/** 近战贴近怪物的固定接触距离:小于攻击距离 dis使英雄进攻击范围后仍继续贴近到此间距才停 */ /** 近战贴近怪物的固定接触距离:小于攻击距离 dis使英雄进攻击范围后仍继续贴近到此间距才停 */
private static readonly MELEE_ENGAGE_X = 60; private static readonly MELEE_ENGAGE_X = 60;
/** 英雄向敌人推进的最远终止点 X防止近战追敌时无限右移脱离战场 */
private static readonly HERO_ADVANCE_LIMIT_X = 300;
/** /**
* 计算某个槽位的目标 X。 * 计算某个槽位的目标 X。

View File

@@ -560,17 +560,15 @@ export class MissionComp extends CCComp {
let allAlive = true; let allAlive = true;
let hasHero = false; let hasHero = false;
let heroDeathCount = 0;
ecs.query(this.heroAttrsMatcher).forEach(entity => { ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp); const attrs = entity.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.HERO) { if (attrs && attrs.fac === FacSet.HERO) {
hasHero = true; hasHero = true;
if (attrs.is_dead) {
allAlive = false;
heroDeathCount++;
}
} }
}); });
// 英雄为真实死亡(实体已销毁),本回合死亡数取死亡记录长度
const heroDeathCount = smc.mission.dead_heroes.length;
if (heroDeathCount > 0) allAlive = false;
// 【动态难度调节】根据本回合战况自动放水 / 加压 // 【动态难度调节】根据本回合战况自动放水 / 加压
// 清场加速节省的时间还原进口径,避免"打得好"被节奏加速+强度加压双重惩罚 // 清场加速节省的时间还原进口径,避免"打得好"被节奏加速+强度加压双重惩罚
@@ -978,15 +976,14 @@ export class MissionComp extends CCComp {
/** 检测场上英雄是否全员存活(清屏 Perfect 判定用,独立于评分统计逻辑) */ /** 检测场上英雄是否全员存活(清屏 Perfect 判定用,独立于评分统计逻辑) */
private checkAllHeroAlive(): boolean { private checkAllHeroAlive(): boolean {
let hasHero = false; let hasHero = false;
let allAlive = true;
ecs.query(this.heroAttrsMatcher).forEach(entity => { ecs.query(this.heroAttrsMatcher).forEach(entity => {
const attrs = entity.get(HeroAttrsComp); const attrs = entity.get(HeroAttrsComp);
if (attrs && attrs.fac === FacSet.HERO) { if (attrs && attrs.fac === FacSet.HERO) {
hasHero = true; hasHero = true;
if (attrs.is_dead) allAlive = false;
} }
}); });
return hasHero && allAlive; // 英雄为真实死亡(实体已销毁),有死亡记录即非全员存活
return hasHero && smc.mission.dead_heroes.length === 0;
} }
/** /**

View File

@@ -15,6 +15,11 @@
* 旧英雄移动合并销毁,新英雄以 **两者最高等级 + 1** 落地(封顶 HERO_MAX_LV * 旧英雄移动合并销毁,新英雄以 **两者最高等级 + 1** 落地(封顶 HERO_MAX_LV
* 等级已达上限时拒绝召唤。英雄也可通过升级卡SpecialUpgrade升级。 * 等级已达上限时拒绝召唤。英雄也可通过升级卡SpecialUpgrade升级。
* *
* 死亡机制:
* 英雄为真实死亡HeroAtkSystem.doDead → 快照至 smc.mission.dead_heroes → 实体销毁),
* 本组件不做回合自动复活;复活由后续复活机制读取 dead_heroes 重新召唤。
* revive 系技能为"濒死回血"(血量归零时消耗次数回血续命),不触发真死。
*
* 依赖: * 依赖:
* - Herohero/Hero.ts—— 英雄 ECS 实体类 * - Herohero/Hero.ts—— 英雄 ECS 实体类
* - HeroAttrsComp —— 英雄属性组件 * - HeroAttrsComp —— 英雄属性组件
@@ -102,34 +107,28 @@ export class MissionHeroComp extends CCComp {
// ======================== 事件处理 ======================== // ======================== 事件处理 ========================
/** 关卡结束时清理全部英雄 ECS 实体 */ /** 关卡结束时清理全部存活英雄 ECS 实体,并清空本局死亡英雄记录 */
clear_heros() { clear_heros() {
const heroes = this.getAllHeroes(); const heroes = this.getAllHeroes();
for (let i = 0; i < heroes.length; i++) { for (let i = 0; i < heroes.length; i++) {
heroes[i].destroy(); heroes[i].destroy();
} }
// 整局结束,死亡记录失效(复活机制仅限局内)
smc.mission.dead_heroes = [];
} }
/** 战斗准备阶段:重置出战英雄计数,恢复满血重新登场 */ /**
* 战斗准备阶段:刷新存活英雄计数与血条。
*
* 说明:英雄为真实死亡(实体已销毁,快照存于 smc.mission.dead_heroes
* 本阶段不再做回合自动复活;复活由后续复活机制(重新召唤)显式触发。
*/
fight_ready() { fight_ready() {
const heroes = this.getAllHeroes(); const heroes = this.getAllHeroes();
smc.vmdata.mission_data.hero_num = heroes.length; smc.vmdata.mission_data.hero_num = heroes.length;
for (let i = 0; i < heroes.length; i++) { for (let i = 0; i < heroes.length; i++) {
const hero = heroes[i]; const model = heroes[i].get(HeroAttrsComp);
const model = hero.get(HeroAttrsComp); if (model) {
const view = hero.get(HeroViewComp);
if (model && view) {
if (model.is_dead) {
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;
if (posIndex >= 0) smc.mission.heroGrid[posIndex] = hero.eid;
hero.playDropAnim(spawnPos, landingPos.y);
}
model.dirty_hp = true; model.dirty_hp = true;
} }
} }