import type { Node, Vec3 } from 'cc'; import type { EntityId } from 'db://assets/mx/module/ecs'; import { HealthComp, MovementComp, TagComp, } from 'db://assets/mx/module/battle'; import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore'; import { BATTLE_BUFF_PASSIVE_REVIVED_TAG, resolveCarriedReviveProbWan, rollCarriedPassiveRevive, } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil'; import { resolveMarchOwnerPlayerId } from '../../manager/battleBuff/BattleBuffSideResolve'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; import { getMonsterSkillHost } from './MonsterSkillHost'; import { HitEffectResolver } from '../view/HitEffectResolver'; import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea'; /** 还魂丹暂缓击杀条目 */ type PendingReviveEntry = { /** 待判定实体 */ entityId: EntityId; /** 击杀者 */ killer: EntityId; /** 致死前血量(判定失败真死时传给 lifecycle) */ reviveHp: number; /** 行军怪归属玩家 */ ownerPlayerId: number; }; /** * 被动 functiontype=14 还魂丹:携带方「出征兵」死亡后按概率原地复活。 * 出征兵 = 进攻对面基地的行军怪,不是己侧场上单位。 */ export class BattleBuffUnitReviveHost { private _battle: ProjectBattleHandle | null = null; private _reviveActive = false; private _deathFxParent: Node | null = null; private readonly _deathFx = new HitEffectResolver(); private readonly _pending: PendingReviveEntry[] = []; /** 已激活还魂的 playerId */ private readonly _activePlayerIds: number[] = []; /** 已播死亡特效、待真死时跳过 UIBattleGame 重复播放 */ private readonly _deathFxPlayedIds: EntityId[] = []; /** 还魂判定失败后须立即真死,不再暂缓 */ private readonly _forceKillOnce: EntityId[] = []; /** 绑定本局;注册暂缓 kill */ bind(battle: ProjectBattleHandle, deathFxParent: Node | null): void { this.unbind(); this._battle = battle; this._deathFxParent = deathFxParent?.isValid ? deathFxParent : null; this._rebuildActivePlayers(); battle.session.tryDeferEntityKill = this._tryDeferEntityKill; } /** AI 挂载后按 CarryStore 重扫激活玩家(保留死亡特效父节点) */ refreshCarry(): void { if (!this._battle) return; this._rebuildActivePlayers(); } /** 解除绑定并作废进行中的还魂判定 */ unbind(): void { const battle = this._battle; if (battle && battle.session.tryDeferEntityKill === this._tryDeferEntityKill) { battle.session.tryDeferEntityKill = undefined; } this._pending.length = 0; this._deathFxPlayedIds.length = 0; this._forceKillOnce.length = 0; this._activePlayerIds.length = 0; this._battle = null; this._deathFxParent = null; this._reviveActive = false; } /** 是否还魂丹已复活过(再次死亡无桃、不再判定) */ isPassiveRevivedEntity(entityId: EntityId): boolean { const battle = this._battle; if (!battle || !battle.session.world.isAlive(entityId)) return false; return this._hasTag(battle, entityId, BATTLE_BUFF_PASSIVE_REVIVED_TAG); } /** EntityKilled 时是否跳过重复死亡特效 */ shouldSkipDeathFxOnKill(victim: EntityId): boolean { for (let i = 0; i < this._deathFxPlayedIds.length; i++) { if (this._deathFxPlayedIds[i] === victim) { this._deathFxPlayedIds.splice(i, 1); return true; } } return false; } /** 按 CarryStore 重建激活玩家列表 */ private _rebuildActivePlayers(): void { this._activePlayerIds.length = 0; this._tryPushActive(0); this._tryPushActive(AI_PLAYER_ID); this._reviveActive = this._activePlayerIds.length > 0; } /** * 若该玩家携带有效还魂则记入激活列表。 * @param playerId 槽位 */ private _tryPushActive(playerId: number): void { const adjust = resolveCarriedReviveProbWan(BattleBuffCarryStore.getPassives(playerId)); if (!adjust.active || adjust.probWan <= 0) return; this._activePlayerIds.push(playerId); } private _tryDeferEntityKill = (victim: EntityId, killer: EntityId, hpBeforeKill: number): boolean => { const forceIdx = this._forceKillOnce.indexOf(victim); if (forceIdx >= 0) { this._forceKillOnce.splice(forceIdx, 1); return false; } const battle = this._battle; if (!battle || !this._reviveActive) return false; const ownerPlayerId = resolveMarchOwnerPlayerId(battle.session, victim); if (ownerPlayerId < 0 || this._activePlayerIds.indexOf(ownerPlayerId) < 0) return false; if (this._hasTag(battle, victim, BATTLE_BUFF_PASSIVE_REVIVED_TAG)) return false; if (this._findPending(victim)) return true; const reviveHp = Math.max(0, Math.floor(hpBeforeKill)); const presenter = battle.getMonsterPresenter(); const worldPos = presenter?.holdForPassiveRevive(victim) ?? null; if (!worldPos) { console.log('[BattleBuffUnitRevive] 还魂丹暂缓击杀无坐标', victim); return false; } const entry: PendingReviveEntry = { entityId: victim, killer, reviveHp, ownerPlayerId }; this._pending.push(entry); this._deathFxPlayedIds.push(victim); this._playDeathFxThenResolve(entry, worldPos); return true; }; private _playDeathFxThenResolve(entry: PendingReviveEntry, worldPos: Readonly): void { let settled = false; const settle = (): void => { if (settled) return; settled = true; this._resolvePending(entry); }; const parent = this._deathFxParent; if (!parent?.isValid) { console.log('[BattleBuffUnitRevive] 死亡特效父节点无效,直接判定还魂'); settle(); return; } this._deathFx.playMonsterDeathAt(parent, worldPos, settle); // 特效加载/播完失败时仍须收尾,否则 holdVisual 永不释放、尸体堆在场上 const battle = this._battle; if (battle) { battle.session.clock.schedule(2.5, settle); } else { settle(); } } private _resolvePending(entry: PendingReviveEntry): void { if (!this._findPending(entry.entityId)) return; if (rollCarriedPassiveRevive(BattleBuffCarryStore.getPassives(entry.ownerPlayerId))) { this._applyRevive(entry); return; } this._finalizeKill(entry); } private _applyRevive(entry: PendingReviveEntry): void { const battle = this._battle; if (!battle) return; this._removePending(entry.entityId); const world = battle.session.world; if (!world.isAlive(entry.entityId)) return; const health = world.getComponent(entry.entityId, HealthComp); if (!health) { console.log('[BattleBuffUnitRevive] 还魂成功但无 HealthComp', entry.entityId); this._finalizeKill(entry); return; } health.hp = health.maxHp; this._addTag(battle, entry.entityId, BATTLE_BUFF_PASSIVE_REVIVED_TAG); // 复活成功不会走 EntityKilled,必须在此清掉 skip 标记, // 否则该怪再次真死时 UIBattleGame 会误跳过死亡特效 this._removeDeathFxPlayed(entry.entityId); getMonsterSkillHost().releasePassiveReviveHold(entry.entityId); const presenter = battle.getMonsterPresenter(); presenter?.releasePassiveRevive(entry.entityId, health.hp, health.maxHp); } private _removeDeathFxPlayed(entityId: EntityId): void { for (let i = this._deathFxPlayedIds.length - 1; i >= 0; i--) { if (this._deathFxPlayedIds[i] === entityId) { this._deathFxPlayedIds.splice(i, 1); } } } private _finalizeKill(entry: PendingReviveEntry): void { const battle = this._battle; if (!battle) return; this._removePending(entry.entityId); const presenter = battle.getMonsterPresenter(); const world = battle.session.world; if (!world.isAlive(entry.entityId)) { presenter?.releaseVisual(entry.entityId); return; } const move = world.getComponent(entry.entityId, MovementComp); if (move) { move.spawnProtected = false; move.presentLocked = false; } this._forceKillOnce.push(entry.entityId); battle.session.lifecycle.kill(entry.entityId, entry.killer, entry.reviveHp); // holdForPassiveRevive 仍占用节点:EntityKilled 只会进 pendingRecycle,须 release 才能 putNode presenter?.releaseVisual(entry.entityId); getMonsterSkillHost().releasePassiveReviveHold(entry.entityId); } private _findPending(entityId: EntityId): PendingReviveEntry | null { for (let i = 0; i < this._pending.length; i++) { if (this._pending[i].entityId === entityId) return this._pending[i]; } return null; } private _removePending(entityId: EntityId): void { for (let i = this._pending.length - 1; i >= 0; i--) { if (this._pending[i].entityId === entityId) { this._pending.splice(i, 1); } } } private _hasTag(battle: ProjectBattleHandle, entityId: EntityId, tag: string): boolean { const tags = battle.session.world.getComponent(entityId, TagComp)?.tags; if (!tags) return false; return tags.indexOf(tag) >= 0; } private _addTag(battle: ProjectBattleHandle, entityId: EntityId, tag: string): void { const world = battle.session.world; let tagComp = world.getComponent(entityId, TagComp); if (!tagComp) { tagComp = new TagComp([]); world.addComponent(entityId, tagComp); } if (tagComp.tags.indexOf(tag) < 0) { tagComp.tags.push(tag); } } } let _host: BattleBuffUnitReviveHost | null = null; /** 全局还魂丹被动 Host(单局复用) */ export function getBattleBuffUnitReviveHost(): BattleBuffUnitReviveHost { if (!_host) _host = new BattleBuffUnitReviveHost(); return _host; }