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.
474 lines
18 KiB
474 lines
18 KiB
|
3 weeks ago
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
||
|
|
import {
|
||
|
|
BattlePhase,
|
||
|
|
BattleTeamId,
|
||
|
|
BattleEventFactory,
|
||
|
|
HealthComp,
|
||
|
|
MovementComp,
|
||
|
|
TeamComp,
|
||
|
|
TransformComp,
|
||
|
|
UnitMetaComp,
|
||
|
|
isBajiaoBlowRetreating,
|
||
|
|
isYanSoulClaimed,
|
||
|
|
type BattleSession,
|
||
|
|
type Cell,
|
||
|
|
} from 'db://assets/mx/module/battle';
|
||
|
|
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
|
||
|
|
import { MonsterType } from '../../manager/MonsterDataManager';
|
||
|
|
import {
|
||
|
|
resolveCarriedBaseThunderConfig,
|
||
|
|
type BattleBuffBaseThunderAdjust,
|
||
|
|
} from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
|
||
|
|
import { schedulePresentation } from '../skill/BattlePresentationClock';
|
||
|
|
import { isMechanismUnit } from '../mechanism/MechanismServices';
|
||
|
|
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
|
||
|
|
import type { BattleGameBaseThunderPresenter } from '../../ui/BattleGame/BattleGameBaseThunderPresenter';
|
||
|
|
import { AI_PLAYER_ID } from '../ai/runtime/AiCampArea';
|
||
|
|
|
||
|
|
/** 九霄神雷伤害类型 */
|
||
|
|
const BASE_THUNDER_DAMAGE_TYPE = 'base_thunder';
|
||
|
|
/** 无施法者占位 */
|
||
|
|
const BASE_THUNDER_CASTER = 0 as EntityId;
|
||
|
|
/** 飘字展示后再击杀(逻辑秒) */
|
||
|
|
const BASE_THUNDER_KILL_DELAY_SEC = 0.4;
|
||
|
|
/** 与 Pathfinder 正交一步代价一致,用于距离场→格数 */
|
||
|
|
const PATH_ORTH_STEP = 1000;
|
||
|
|
|
||
|
|
/** 沿进路排序的目标条目 */
|
||
|
|
type ThunderTargetEntry = {
|
||
|
|
/** 受击实体 */
|
||
|
|
entityId: EntityId;
|
||
|
|
/** 在生效格序列中的索引(越小越靠前) */
|
||
|
|
cellOrder: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 单玩家九霄神雷状态 */
|
||
|
|
type PlayerThunderState = {
|
||
|
|
/** 防守方 playerId */
|
||
|
|
playerId: number;
|
||
|
|
/** 汇总配置 */
|
||
|
|
config: BattleBuffBaseThunderAdjust;
|
||
|
|
/** 本局已触发次数 */
|
||
|
|
usedTriggers: number;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 被动 functiontype=20:进攻各方军营的敌军接近终点时降雷。
|
||
|
|
* 本机/AI 各读 CarryStore,次数独立。
|
||
|
|
*/
|
||
|
|
export class BattleBuffBaseThunderHost {
|
||
|
|
private _battle: ProjectBattleHandle | null = null;
|
||
|
|
private _presenter: BattleGameBaseThunderPresenter | null = null;
|
||
|
|
private readonly _states: PlayerThunderState[] = [];
|
||
|
|
/** 当前一轮落雷进行中 */
|
||
|
|
private _striking = false;
|
||
|
|
/** 延迟击杀调度世代(unbind 时递增以作废) */
|
||
|
|
private _killScheduleGen = 0;
|
||
|
|
|
||
|
|
/** 绑定本局与表现层 */
|
||
|
|
bind(battle: ProjectBattleHandle, presenter: BattleGameBaseThunderPresenter | null): void {
|
||
|
|
const sameBattle = this._battle === battle;
|
||
|
|
if (!sameBattle) {
|
||
|
|
this.unbind();
|
||
|
|
}
|
||
|
|
this._battle = battle;
|
||
|
|
this._presenter = presenter;
|
||
|
|
this._rebuildStates(sameBattle);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* AI 挂载后按 CarryStore 重扫(保留本局已触发次数)。
|
||
|
|
* @param battle 可选;开战时 Host 可能尚未 bind,传入则一并绑定
|
||
|
|
*/
|
||
|
|
refreshCarry(battle?: ProjectBattleHandle): void {
|
||
|
|
const b = battle ?? this._battle;
|
||
|
|
if (!b) {
|
||
|
|
console.log('[BaseThunder] refreshCarry 时尚未 bind');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._battle = b;
|
||
|
|
this._rebuildStates(true);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 该玩家九霄神雷单局次数是否已用尽。
|
||
|
|
* @param playerId 防守方
|
||
|
|
*/
|
||
|
|
isTriggersExhausted(playerId: number): boolean {
|
||
|
|
for (let i = 0; i < this._states.length; i++) {
|
||
|
|
const state = this._states[i];
|
||
|
|
if (state.playerId !== playerId) continue;
|
||
|
|
return state.usedTriggers >= state.config.maxTriggers;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 解除绑定 */
|
||
|
|
unbind(): void {
|
||
|
|
this._battle = null;
|
||
|
|
this._presenter = null;
|
||
|
|
this._states.length = 0;
|
||
|
|
this._striking = false;
|
||
|
|
this._killScheduleGen++;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 重建各方神雷状态。
|
||
|
|
* @param keepUsed 是否保留已触发次数
|
||
|
|
*/
|
||
|
|
private _rebuildStates(keepUsed = false): void {
|
||
|
|
const prev: PlayerThunderState[] = [];
|
||
|
|
if (keepUsed) {
|
||
|
|
for (let i = 0; i < this._states.length; i++) {
|
||
|
|
prev.push(this._states[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._states.length = 0;
|
||
|
|
this._tryPush(0, prev);
|
||
|
|
this._tryPush(AI_PLAYER_ID, prev);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 若该玩家携带有效九霄神雷则记入。
|
||
|
|
* @param playerId 槽位
|
||
|
|
* @param prev 重建前状态(用于保留次数)
|
||
|
|
*/
|
||
|
|
private _tryPush(playerId: number, prev: readonly PlayerThunderState[] = []): void {
|
||
|
|
const passives = BattleBuffCarryStore.getPassives(playerId);
|
||
|
|
const adjust = resolveCarriedBaseThunderConfig(passives);
|
||
|
|
if (!adjust.active || adjust.maxTriggers <= 0 || adjust.triggerCells <= 0 || adjust.effectCells <= 0) {
|
||
|
|
if (adjust.active) {
|
||
|
|
console.log('[BaseThunder] 九霄神雷配置无效,未启用', playerId, adjust);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (adjust.smallDmgWan <= 0 || adjust.bossDmgWan <= 0) {
|
||
|
|
console.log('[BaseThunder] 九霄神雷伤害比例无效', playerId, adjust);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let usedTriggers = 0;
|
||
|
|
for (let i = 0; i < prev.length; i++) {
|
||
|
|
if (prev[i].playerId === playerId) {
|
||
|
|
usedTriggers = prev[i].usedTriggers;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this._states.push({ playerId, config: adjust, usedTriggers });
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 帧更新:扫描触发条件 */
|
||
|
|
tick(_dt: number): void {
|
||
|
|
if (this._striking || !this._battle || this._states.length <= 0) return;
|
||
|
|
if (this._battle.session.phase !== BattlePhase.COMBAT) return;
|
||
|
|
|
||
|
|
for (let i = 0; i < this._states.length; i++) {
|
||
|
|
const state = this._states[i];
|
||
|
|
if (state.usedTriggers >= state.config.maxTriggers) continue;
|
||
|
|
const anchor = this._findTriggerAnchor(state.playerId, state.config.triggerCells);
|
||
|
|
if (anchor == null) continue;
|
||
|
|
|
||
|
|
const effectCells = this._collectEffectCells(anchor, state.config.effectCells);
|
||
|
|
if (effectCells.length <= 0) continue;
|
||
|
|
|
||
|
|
const targets = this._collectTargets(state.playerId, effectCells);
|
||
|
|
if (targets.length <= 0) continue;
|
||
|
|
|
||
|
|
state.usedTriggers++;
|
||
|
|
this._striking = true;
|
||
|
|
const config = state.config;
|
||
|
|
const presenter = this._presenter;
|
||
|
|
if (presenter) {
|
||
|
|
presenter.playStrike(
|
||
|
|
targets,
|
||
|
|
(entityId) => this._applyDamage(state.playerId, config, entityId),
|
||
|
|
() => {
|
||
|
|
this._striking = false;
|
||
|
|
},
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
for (let t = 0; t < targets.length; t++) {
|
||
|
|
this._applyDamage(state.playerId, config, targets[t]);
|
||
|
|
}
|
||
|
|
this._striking = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 找最靠近终点、且处于触发距离内的进攻指定玩家的小怪/Boss/召唤怪。
|
||
|
|
* @param ownerPlayerId 防守方
|
||
|
|
* @param triggerCells 触发距离
|
||
|
|
*/
|
||
|
|
private _findTriggerAnchor(ownerPlayerId: number, triggerCells: number): EntityId | null {
|
||
|
|
const battle = this._battle;
|
||
|
|
if (!battle) return null;
|
||
|
|
const session = battle.session;
|
||
|
|
const thresholdCells = Math.max(1, Math.floor(triggerCells));
|
||
|
|
let bestId: EntityId | null = null;
|
||
|
|
let bestRemaining = Number.POSITIVE_INFINITY;
|
||
|
|
|
||
|
|
session.world.forEachAlive((entityId) => {
|
||
|
|
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
|
||
|
|
const move = session.world.getComponent(entityId, MovementComp);
|
||
|
|
if (!move || move.spawnProtected || move.presentLocked || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
|
||
|
|
const remainingCells = this._getRemainingCellsToAttackTarget(session, entityId);
|
||
|
|
if (!Number.isFinite(remainingCells) || remainingCells > thresholdCells) return;
|
||
|
|
if (remainingCells < bestRemaining) {
|
||
|
|
bestRemaining = remainingCells;
|
||
|
|
bestId = entityId;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return bestId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 实体沿进路至攻击终点的剩余格数。
|
||
|
|
* 取「未走完路径步数」与「距离场格数」的较小值,避免 path 过长或重寻路导致漏触发。
|
||
|
|
*/
|
||
|
|
private _getRemainingCellsToAttackTarget(session: BattleSession, entityId: EntityId): number {
|
||
|
|
const world = session.world;
|
||
|
|
const move = world.getComponent(entityId, MovementComp);
|
||
|
|
const tf = world.getComponent(entityId, TransformComp);
|
||
|
|
if (!move || !tf) return Number.POSITIVE_INFINITY;
|
||
|
|
if (move.reachedGoal) return 0;
|
||
|
|
|
||
|
|
let best = Number.POSITIVE_INFINITY;
|
||
|
|
const pathSteps = move.path.length - move.pathIndex;
|
||
|
|
if (pathSteps > 0) best = pathSteps;
|
||
|
|
|
||
|
|
const campAreaId = this._resolveEntityCampAreaId(session, entityId);
|
||
|
|
const cost = session.pathfinder.getPathCost({ row: tf.row, col: tf.col }, campAreaId);
|
||
|
|
if (Number.isFinite(cost) && cost >= 0) {
|
||
|
|
const costCells = Math.max(0, Math.ceil(cost / PATH_ORTH_STEP));
|
||
|
|
if (costCells < best) best = costCells;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Number.isFinite(best)) return best;
|
||
|
|
|
||
|
|
const forward = this._buildPathToAttackTarget(session, entityId);
|
||
|
|
if (forward.length <= 0) return 0;
|
||
|
|
return forward.length;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 从实体当前格起,沿进路至攻击目标的路径格(不含起点) */
|
||
|
|
private _buildPathToAttackTarget(session: BattleSession, entityId: EntityId): Cell[] {
|
||
|
|
const world = session.world;
|
||
|
|
const tf = world.getComponent(entityId, TransformComp);
|
||
|
|
if (!tf) return [];
|
||
|
|
|
||
|
|
const move = world.getComponent(entityId, MovementComp);
|
||
|
|
const out: Cell[] = [];
|
||
|
|
if (move && move.pathIndex < move.path.length) {
|
||
|
|
for (let i = move.pathIndex; i < move.path.length; i++) {
|
||
|
|
const c = move.path[i];
|
||
|
|
const last = out.length > 0 ? out[out.length - 1] : tf;
|
||
|
|
if (last.row === c.row && last.col === c.col) continue;
|
||
|
|
out.push({ row: c.row, col: c.col });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (out.length > 0) return out;
|
||
|
|
|
||
|
|
const campAreaId = this._resolveEntityCampAreaId(session, entityId);
|
||
|
|
const from = { row: tf.row, col: tf.col };
|
||
|
|
const target = session.pathfinder.shortestTargetCell(from, campAreaId);
|
||
|
|
if (!target) return [];
|
||
|
|
const fullPath = session.pathfinder.buildPathToTarget(from, target, campAreaId);
|
||
|
|
if (fullPath.length <= 1) return [];
|
||
|
|
for (let i = 1; i < fullPath.length; i++) {
|
||
|
|
const c = fullPath[i];
|
||
|
|
const last = out.length > 0 ? out[out.length - 1] : from;
|
||
|
|
if (last.row === c.row && last.col === c.col) continue;
|
||
|
|
out.push({ row: c.row, col: c.col });
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 实体出怪口所属阵营区域(与 MovementSystem.assignPath 一致) */
|
||
|
|
private _resolveEntityCampAreaId(session: BattleSession, entityId: EntityId): number | undefined {
|
||
|
|
const meta = session.world.getComponent(entityId, UnitMetaComp);
|
||
|
|
const port = meta?.spawnPoint ?? 0;
|
||
|
|
if (port < 1) {
|
||
|
|
console.log('[BaseThunder] 实体缺少有效 spawnPoint', entityId);
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
const area = session.topology.getSpawnPortCampArea(port);
|
||
|
|
if (area === undefined) {
|
||
|
|
console.log('[BaseThunder] 出怪口缺少阵营区域', port, entityId);
|
||
|
|
}
|
||
|
|
return area;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 实体出怪口格(与 MovementSystem 一致) */
|
||
|
|
private _resolveEntitySpawnCell(session: BattleSession, entityId: EntityId): Cell | null {
|
||
|
|
const meta = session.world.getComponent(entityId, UnitMetaComp);
|
||
|
|
const port = meta?.spawnPoint ?? 0;
|
||
|
|
if (port < 1) {
|
||
|
|
console.log('[BaseThunder] 实体缺少有效 spawnPoint', entityId);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const spawn = session.topology.getSpawnCell(port);
|
||
|
|
if (!spawn) {
|
||
|
|
console.log('[BaseThunder] 出怪口格缺失', port, entityId);
|
||
|
|
}
|
||
|
|
return spawn ?? null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从锚点格起沿进路向出怪口后退收集生效格(含锚点格,共 effectCells 格)。
|
||
|
|
* @param anchorId 锚点
|
||
|
|
* @param effectCells 生效格数
|
||
|
|
*/
|
||
|
|
private _collectEffectCells(anchorId: EntityId, effectCells: number): Cell[] {
|
||
|
|
const battle = this._battle;
|
||
|
|
const count = Math.max(1, Math.floor(effectCells));
|
||
|
|
if (!battle) return [];
|
||
|
|
const session = battle.session;
|
||
|
|
const world = session.world;
|
||
|
|
const tf = world.getComponent(anchorId, TransformComp);
|
||
|
|
if (!tf) return [];
|
||
|
|
|
||
|
|
const from = { row: tf.row, col: tf.col };
|
||
|
|
const meta = world.getComponent(anchorId, UnitMetaComp);
|
||
|
|
const port = meta?.spawnPoint ?? 0;
|
||
|
|
const spawn = this._resolveEntitySpawnCell(session, anchorId);
|
||
|
|
if (!spawn || port < 1) return [{ ...from }];
|
||
|
|
|
||
|
|
const retreatSteps = count - 1;
|
||
|
|
const path = session.pathfinder.buildRetreatPath(from, retreatSteps, spawn, port);
|
||
|
|
if (path.length <= 0) return [{ ...from }];
|
||
|
|
if (path.length <= count) return path;
|
||
|
|
return path.slice(0, count);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 收集生效格上的小怪/Boss;排序为锚点格(近终点)优先,再沿后退方向。
|
||
|
|
* @param ownerPlayerId 防守方
|
||
|
|
* @param effectCells 生效格
|
||
|
|
*/
|
||
|
|
private _collectTargets(ownerPlayerId: number, effectCells: Cell[]): EntityId[] {
|
||
|
|
const battle = this._battle;
|
||
|
|
if (!battle) return [];
|
||
|
|
const session = battle.session;
|
||
|
|
const entries: ThunderTargetEntry[] = [];
|
||
|
|
|
||
|
|
session.world.forEachAlive((entityId) => {
|
||
|
|
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
|
||
|
|
const tf = session.world.getComponent(entityId, TransformComp);
|
||
|
|
const move = session.world.getComponent(entityId, MovementComp);
|
||
|
|
let cellOrder = -1;
|
||
|
|
if (move) {
|
||
|
|
const cell = session.grid.worldToCell(move.worldX, move.worldY);
|
||
|
|
cellOrder = this._indexOfCell(effectCells, cell.row, cell.col);
|
||
|
|
}
|
||
|
|
if (cellOrder < 0 && tf) {
|
||
|
|
cellOrder = this._indexOfCell(effectCells, tf.row, tf.col);
|
||
|
|
}
|
||
|
|
if (cellOrder < 0) return;
|
||
|
|
entries.push({ entityId, cellOrder });
|
||
|
|
});
|
||
|
|
|
||
|
|
entries.sort((a, b) => {
|
||
|
|
if (a.cellOrder !== b.cellOrder) return a.cellOrder - b.cellOrder;
|
||
|
|
return a.entityId - b.entityId;
|
||
|
|
});
|
||
|
|
|
||
|
|
const out: EntityId[] = [];
|
||
|
|
for (let i = 0; i < entries.length; i++) {
|
||
|
|
out.push(entries[i].entityId);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 格在序列中的索引;-1 表示不在序列内 */
|
||
|
|
private _indexOfCell(cells: Cell[], row: number, col: number): number {
|
||
|
|
for (let i = 0; i < cells.length; i++) {
|
||
|
|
if (cells[i].row === row && cells[i].col === col) return i;
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 是否可被九霄神雷选中的进攻指定玩家的小怪/Boss/召唤怪。
|
||
|
|
* @param battle 战斗
|
||
|
|
* @param ownerPlayerId 防守方
|
||
|
|
* @param entityId 实体
|
||
|
|
*/
|
||
|
|
private _isThunderCandidate(battle: ProjectBattleHandle, ownerPlayerId: number, entityId: EntityId): boolean {
|
||
|
|
const session = battle.session;
|
||
|
|
const world = session.world;
|
||
|
|
if (!world.isAlive(entityId)) return false;
|
||
|
|
if (isMechanismUnit(session, entityId)) return false;
|
||
|
|
const team = world.getComponent(entityId, TeamComp);
|
||
|
|
if (!team || !BattleTeamId.isEnemyTeam(team.team)) return false;
|
||
|
|
if (Math.max(0, Math.floor(Number(team.ownerId) || 0)) !== ownerPlayerId) return false;
|
||
|
|
const row = battle.resolveMonsterRow(entityId);
|
||
|
|
if (!row) return false;
|
||
|
|
const type = Math.floor(Number(row.type));
|
||
|
|
return type === MonsterType.SmallMonster
|
||
|
|
|| type === MonsterType.Boss
|
||
|
|
|| type === MonsterType.SummonMonster;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 对单目标结算伤害(走正常掉血/击杀流程)。
|
||
|
|
* @param ownerPlayerId 防守方
|
||
|
|
* @param config 配置
|
||
|
|
* @param entityId 目标
|
||
|
|
*/
|
||
|
|
private _applyDamage(ownerPlayerId: number, config: BattleBuffBaseThunderAdjust, entityId: EntityId): void {
|
||
|
|
const battle = this._battle;
|
||
|
|
if (!battle || !config) return;
|
||
|
|
const session = battle.session;
|
||
|
|
const world = session.world;
|
||
|
|
if (!world.isAlive(entityId)) return;
|
||
|
|
if (!this._isThunderCandidate(battle, ownerPlayerId, entityId)) return;
|
||
|
|
|
||
|
|
const hp = world.getComponent(entityId, HealthComp);
|
||
|
|
if (!hp || hp.maxHp <= 0) return;
|
||
|
|
const move = world.getComponent(entityId, MovementComp);
|
||
|
|
if (move?.spawnProtected || move?.presentLocked || isYanSoulClaimed(move) || isBajiaoBlowRetreating(move)) return;
|
||
|
|
|
||
|
|
const monsterRow = battle.resolveMonsterRow(entityId);
|
||
|
|
const isBoss = monsterRow != null && Math.floor(Number(monsterRow.type)) === MonsterType.Boss;
|
||
|
|
const wan = isBoss ? config.bossDmgWan : config.smallDmgWan;
|
||
|
|
const amount = Math.max(1, Math.floor(hp.maxHp * wan / 10000));
|
||
|
|
const hpBefore = hp.hp;
|
||
|
|
|
||
|
|
session.damage.apply(BASE_THUNDER_CASTER, entityId, amount, BASE_THUNDER_DAMAGE_TYPE, undefined, false);
|
||
|
|
session.events.flush();
|
||
|
|
session.events.emit(
|
||
|
|
BattleEventFactory.hitAfter(BASE_THUNDER_CASTER, entityId, amount, false, undefined, BASE_THUNDER_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(BASE_THUNDER_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, BASE_THUNDER_CASTER, hpBefore);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let _host: BattleBuffBaseThunderHost | null = null;
|
||
|
|
|
||
|
|
/** 全局九霄神雷被动 Host(单局复用) */
|
||
|
|
export function getBattleBuffBaseThunderHost(): BattleBuffBaseThunderHost {
|
||
|
|
if (!_host) _host = new BattleBuffBaseThunderHost();
|
||
|
|
return _host;
|
||
|
|
}
|