You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
10 KiB
276 lines
10 KiB
|
3 weeks ago
|
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<Vec3>): 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;
|
||
|
|
}
|