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.
228 lines
8.6 KiB
228 lines
8.6 KiB
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
import {
|
|
BattleEventFactory,
|
|
BattleEventType,
|
|
BattleTeamId,
|
|
HealthComp,
|
|
MovementComp,
|
|
TeamComp,
|
|
isBajiaoBlowRetreating,
|
|
isYanSoulClaimed,
|
|
type BattleEvent,
|
|
} from 'db://assets/mx/module/battle';
|
|
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
|
|
import { MonsterType } from '../../manager/MonsterDataManager';
|
|
import {
|
|
resolveCarriedBossInterruptConfig,
|
|
rollCarriedBossInterrupt,
|
|
type BattleBuffBossInterruptAdjust,
|
|
} from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
|
|
import { isMonsterAbilityId } from '../config/weapon1/Weapon1BattleIds';
|
|
import { isMechanismUnit } from '../mechanism/MechanismServices';
|
|
import { schedulePresentation } from '../skill/BattlePresentationClock';
|
|
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
|
|
import { getMonsterSkillHost } from './MonsterSkillHost';
|
|
import type { BattleGameBossInterruptFxPresenter } from '../../ui/BattleGame/BattleGameBossInterruptFxPresenter';
|
|
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
|
|
|
|
/** 打断 Boss 施法伤害类型 */
|
|
const BOSS_INTERRUPT_DAMAGE_TYPE = 'boss_interrupt';
|
|
/** 无施法者占位 */
|
|
const BOSS_INTERRUPT_CASTER = 0 as EntityId;
|
|
/** 飘字展示后再击杀(逻辑秒) */
|
|
const BOSS_INTERRUPT_KILL_DELAY_SEC = 0.4;
|
|
|
|
/** 单玩家打断配置 */
|
|
type PlayerInterruptCfg = {
|
|
/** 防守方 playerId */
|
|
playerId: number;
|
|
/** 汇总配置 */
|
|
config: BattleBuffBossInterruptAdjust;
|
|
};
|
|
|
|
/**
|
|
* 被动 functiontype=21:敌方 Boss 释放技能前按概率打断并造成最大生命比例伤害。
|
|
* 本机/AI 各读 CarryStore;每次 Boss 技能 AbilityCast 独立判定。
|
|
*/
|
|
export class BattleBuffBossInterruptHost {
|
|
private _battle: ProjectBattleHandle | null = null;
|
|
private _presenter: BattleGameBossInterruptFxPresenter | null = null;
|
|
private readonly _cfgs: PlayerInterruptCfg[] = [];
|
|
private _onAbilityCastBound = false;
|
|
/** 延迟击杀调度世代(unbind 时递增以作废) */
|
|
private _killScheduleGen = 0;
|
|
|
|
/** 绑定本局与碎法帝钟表现层 */
|
|
bind(battle: ProjectBattleHandle, presenter: BattleGameBossInterruptFxPresenter | null): void {
|
|
this.unbind();
|
|
this._battle = battle;
|
|
this._presenter = presenter;
|
|
this._rebuildConfigs();
|
|
if (this._cfgs.length <= 0) return;
|
|
battle.session.events.on(BattleEventType.AbilityCast, this._onAbilityCast);
|
|
this._onAbilityCastBound = true;
|
|
}
|
|
|
|
/** AI 挂载后按 CarryStore 重扫(保留表现层) */
|
|
refreshCarry(): void {
|
|
const battle = this._battle;
|
|
if (!battle) return;
|
|
const presenter = this._presenter;
|
|
this.bind(battle, presenter);
|
|
}
|
|
|
|
/** 解除绑定 */
|
|
unbind(): void {
|
|
const battle = this._battle;
|
|
if (battle && this._onAbilityCastBound) {
|
|
battle.session.events.off(BattleEventType.AbilityCast, this._onAbilityCast);
|
|
}
|
|
this._onAbilityCastBound = false;
|
|
this._battle = null;
|
|
this._presenter = null;
|
|
this._cfgs.length = 0;
|
|
this._killScheduleGen++;
|
|
}
|
|
|
|
/** 重建各方打断配置 */
|
|
private _rebuildConfigs(): void {
|
|
this._cfgs.length = 0;
|
|
this._tryPush(0);
|
|
this._tryPush(AI_PLAYER_ID);
|
|
}
|
|
|
|
/**
|
|
* 若该玩家携带有效打断则记入。
|
|
* @param playerId 槽位
|
|
*/
|
|
private _tryPush(playerId: number): void {
|
|
const adjust = resolveCarriedBossInterruptConfig(BattleBuffCarryStore.getPassives(playerId));
|
|
if (!adjust.active || adjust.probWan <= 0 || adjust.dmgWan <= 0) return;
|
|
this._cfgs.push({ playerId, config: adjust });
|
|
}
|
|
|
|
/** AbilityCast:Boss 怪物技能起手时判定打断 */
|
|
private readonly _onAbilityCast = (event: BattleEvent): void => {
|
|
if (event.type !== BattleEventType.AbilityCast) return;
|
|
const battle = this._battle;
|
|
if (!battle || this._cfgs.length <= 0) return;
|
|
|
|
const ctx = event.ctx;
|
|
if (!ctx) return;
|
|
const caster = ctx.caster;
|
|
if (!(caster > 0)) return;
|
|
if (!isMonsterAbilityId(ctx.abilityId)) return;
|
|
|
|
const ownerId = this._bossTargetPlayerId(battle, caster);
|
|
if (ownerId < 0) return;
|
|
let cfg: BattleBuffBossInterruptAdjust | null = null;
|
|
for (let i = 0; i < this._cfgs.length; i++) {
|
|
if (this._cfgs[i].playerId === ownerId) {
|
|
cfg = this._cfgs[i].config;
|
|
break;
|
|
}
|
|
}
|
|
if (!cfg) return;
|
|
if (!rollCarriedBossInterrupt(BattleBuffCarryStore.getPassives(ownerId))) return;
|
|
|
|
this._presenter?.playOnBoss(caster);
|
|
this._interruptCast(battle, caster);
|
|
this._applyDamage(battle, caster, ownerId, cfg.dmgWan);
|
|
};
|
|
|
|
/**
|
|
* 取消 deferred 表现与 Host 待播,解锁施法锁。
|
|
* @param battle 战斗句柄
|
|
* @param caster Boss 实体
|
|
*/
|
|
private _interruptCast(battle: ProjectBattleHandle, caster: EntityId): void {
|
|
battle.session.presentation.cancelByCaster(caster);
|
|
battle.session.abilities.setPresentationLocked(caster, false);
|
|
getMonsterSkillHost().releasePassiveReviveHold(caster);
|
|
}
|
|
|
|
/**
|
|
* 按最大生命万分比掉血并触发飘字;致死则延迟击杀。
|
|
* @param battle 战斗句柄
|
|
* @param entityId Boss
|
|
* @param ownerPlayerId 防守方
|
|
* @param dmgWan 伤害万分比
|
|
*/
|
|
private _applyDamage(battle: ProjectBattleHandle, entityId: EntityId, ownerPlayerId: number, dmgWan: number): void {
|
|
const session = battle.session;
|
|
const world = session.world;
|
|
if (!world.isAlive(entityId)) return;
|
|
if (this._bossTargetPlayerId(battle, entityId) !== ownerPlayerId) return;
|
|
|
|
const hp = world.getComponent(entityId, HealthComp);
|
|
if (!hp || hp.maxHp <= 0 || !(hp.hp > 0)) return;
|
|
const move = world.getComponent(entityId, MovementComp);
|
|
if (move?.spawnProtected || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
|
|
|
|
const wan = Math.max(0, Math.floor(dmgWan));
|
|
if (wan <= 0) return;
|
|
const amount = Math.max(1, Math.floor(hp.maxHp * wan / 10000));
|
|
const hpBefore = hp.hp;
|
|
|
|
session.damage.apply(BOSS_INTERRUPT_CASTER, entityId, amount, BOSS_INTERRUPT_DAMAGE_TYPE, undefined, false);
|
|
session.events.flush();
|
|
session.events.emit(
|
|
BattleEventFactory.hitAfter(
|
|
BOSS_INTERRUPT_CASTER,
|
|
entityId,
|
|
amount,
|
|
false,
|
|
undefined,
|
|
BOSS_INTERRUPT_DAMAGE_TYPE,
|
|
),
|
|
true,
|
|
);
|
|
|
|
if (hp.hp <= 0) {
|
|
this._scheduleKill(entityId, hpBefore);
|
|
}
|
|
}
|
|
|
|
/** 飘字后再击杀,避免产桃/死亡特效抢在伤害数字前 */
|
|
private _scheduleKill(entityId: EntityId, hpBefore: number): void {
|
|
const battle = this._battle;
|
|
if (!battle) return;
|
|
const scheduleGen = this._killScheduleGen;
|
|
schedulePresentation(BOSS_INTERRUPT_KILL_DELAY_SEC, () => {
|
|
if (scheduleGen !== this._killScheduleGen || !this._battle) return;
|
|
const session = this._battle.session;
|
|
const world = session.world;
|
|
if (!world.isAlive(entityId)) return;
|
|
const hpNow = world.getComponent(entityId, HealthComp);
|
|
if (!hpNow || hpNow.hp > 0) return;
|
|
session.lifecycle.kill(entityId, BOSS_INTERRUPT_CASTER, hpBefore);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Boss 进攻目标玩家;非 Boss 或不可打断返回 -1。
|
|
* @param battle 战斗
|
|
* @param entityId 实体
|
|
*/
|
|
private _bossTargetPlayerId(battle: ProjectBattleHandle, entityId: EntityId): number {
|
|
const session = battle.session;
|
|
const world = session.world;
|
|
if (!world.isAlive(entityId)) return -1;
|
|
if (isMechanismUnit(session, entityId)) return -1;
|
|
const team = world.getComponent(entityId, TeamComp);
|
|
if (!team || !BattleTeamId.isEnemyTeam(team.team)) return -1;
|
|
const ownerId = Math.max(0, Math.floor(Number(team.ownerId) || 0));
|
|
const row = battle.resolveMonsterRow(entityId);
|
|
if (!row) return -1;
|
|
if (Math.floor(Number(row.type)) !== MonsterType.Boss) return -1;
|
|
return ownerId;
|
|
}
|
|
}
|
|
|
|
let _host: BattleBuffBossInterruptHost | null = null;
|
|
|
|
/** 全局打断 Boss 施法被动 Host(单局复用) */
|
|
export function getBattleBuffBossInterruptHost(): BattleBuffBossInterruptHost {
|
|
if (!_host) _host = new BattleBuffBossInterruptHost();
|
|
return _host;
|
|
}
|
|
|