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.
AdLike2048/assets/mx/module/battle/core/BattleSession.ts

515 lines
20 KiB

3 weeks ago
import { BattlePhase } from './BattlePhase';
import { BattleFlowMode } from './BattleFlowMode';
import { BattleClock } from './BattleClock';
import { SeededRNG } from './SeededRNG';
import { BattleEventBus } from '../events/BattleEventBus';
import { BattleEventFactory } from '../events/BattleEvents';
import { BATTLE_EVENT_ALL } from '../events/BattleEventType';
import { World } from 'db://assets/mx/module/ecs';
import type { EntityId } from 'db://assets/mx/module/ecs';
import { SystemRegistry } from 'db://assets/mx/module/ecs';
import { GridMap } from '../battlefield/GridMap';
import { SpatialGrid } from '../battlefield/SpatialGrid';
import { MapTopology, type MapTopologySnapshot } from '../battlefield/MapTopology';
import { Pathfinder } from '../battlefield/Pathfinder';
import {
campAreaPolicyFromPreset,
resolveCampAreaPolicy,
type CampAreaPolicy,
type CampAreaPreset,
} from '../battlefield/CampAreaPolicy';
import type { IBattleBridge } from '../bridge/IBattleBridge';
import type { IConfigProvider } from '../config/IConfigProvider';
import type { LevelDef } from '../config/BattleConfigTypes';
import type { IBattleMode } from '../player/BattleMode';
import { SoloBattleMode } from '../player/BattleMode';
import type { PlayerSlot } from '../player/PlayerSlot';
import { AttributeSystem } from '../combat/AttributeSystem';
import { BuffSystem } from '../combat/BuffSystem';
import { TargetResolver } from '../combat/TargetResolver';
import { DamagePipeline } from '../combat/DamagePipeline';
import { EffectExecutor } from '../combat/EffectExecutor';
import { AbilitySystem } from '../combat/AbilitySystem';
import { PresentationPipeline } from '../combat/PresentationPipeline';
import { ProjectileSystem } from '../combat/ProjectileSystem';
import { LifecycleSystem } from '../combat/LifecycleSystem';
import { MovementSystem } from '../combat/MovementSystem';
import { ReachGoalBaseDamageSystem } from '../combat/ReachGoalBaseDamageSystem';
import { DeploySystem } from '../deploy/DeploySystem';
import { WaveSpawner } from '../unit/WaveSpawner';
import { registerBuiltinBattleEffects } from '../registry/registerBuiltin';
import type { BattleServices } from '../combat/BattleServices';
/**
* 战斗会话:组装根。
*
* 实现 BattleServices 接口,负责按依赖顺序构造各子系统并注入具体协作者
* (子系统之间不通过 session 互调,仅持有各自需要的协作者引用)。
* effect/condition 脚本扩展点通过 BattleServices 聚合访问各子系统。
*/
export class BattleSession implements BattleServices {
// —— 基础设施 ——
readonly levelId: string;
readonly levelDef: LevelDef;
readonly config: IConfigProvider;
readonly mode: IBattleMode;
readonly rng: SeededRNG;
readonly clock = new BattleClock();
readonly events = new BattleEventBus();
readonly world = new World();
readonly grid: GridMap;
readonly spatial: SpatialGrid;
readonly topology = new MapTopology();
readonly pathfinder: Pathfinder;
/** 阵营区域隔离策略(applyTopology 后按地图区域数解析) */
campPolicy: CampAreaPolicy = campAreaPolicyFromPreset('none');
readonly flowMode: BattleFlowMode;
/** 结波是否须清场(须击杀完场上敌人才结波) */
readonly waveClearRequiresKillAll: boolean;
players: PlayerSlot[] = [];
// —— 子系统(构造注入具体协作者) ——
readonly effects: EffectExecutor;
readonly attributes: AttributeSystem;
readonly targets: TargetResolver;
readonly abilities: AbilitySystem;
readonly lifecycle: LifecycleSystem;
readonly damage: DamagePipeline;
readonly buffs: BuffSystem;
readonly projectiles: ProjectileSystem;
readonly movement: MovementSystem;
readonly deploy: DeploySystem;
readonly spawner: WaveSpawner;
readonly presentation: PresentationPipeline;
/** System 注册与调度表(仅 tick 系统) */
readonly systems = new SystemRegistry();
private _bridge?: IBattleBridge;
private _waveAdvanceTimer = 0;
private _waveAdvancePending = false;
/** 当前战斗阶段 */
private _phase: BattlePhase = BattlePhase.PREP;
/** 子系统查询当前阶段(避免反向持有 session) */
private readonly _queryPhase = (): BattlePhase => this._phase;
/** pause 前阶段(resume 时还原;避免备战中暂停后误进 COMBAT) */
private _phaseBeforePause: BattlePhase | null = null;
/** 战斗是否已结束 */
ended = false;
/** 项目层:返回 true 时禁止该实体施法(魅惑等) */
castBlockPredicate?: (entityId: EntityId) => boolean;
/** 项目层:返回 true 时暂缓该实体 OnTick 计时(Boss 出生跳出等) */
holdAbilityTickPredicate?: (entityId: EntityId) => boolean;
/** 项目层:返回 true 时暂缓 kill,待死亡特效后再结算 */
tryDeferEntityKill?: (victim: EntityId, killer: EntityId, hpBeforeKill: number) => boolean;
/** 是否胜利 */
win = false;
/** 分帧 tick 切片数(性能优化) */
tickSliceCount = 1;
private _tickFrame = 0;
/** 已销毁实体组件 GC 周期(帧),到期调用 world.gc() 回收残留组件 */
private _gcInterval = 60;
private _gcCounter = 0;
/** 逻辑固定步长(秒)。≤0 表示用引擎可变 dt */
fixedStep = 1 / 30;
/** 固定步长单帧最大迭代次数(防止低帧螺旋追赶) */
maxFixedIterations = 5;
private _fixedAccumulator = 0;
/**
* @param options 会话创建参数
*/
constructor(options: {
levelId: string;
config: IConfigProvider;
mode?: IBattleMode;
seed?: number;
bridge?: IBattleBridge;
flowMode?: BattleFlowMode;
waveClearRequiresKillAll?: boolean;
}) {
registerBuiltinBattleEffects();
this.levelId = options.levelId;
this.config = options.config;
this.mode = options.mode ?? new SoloBattleMode();
this.flowMode = options.flowMode ?? BattleFlowMode.ClassicPrep;
this.waveClearRequiresKillAll = options.waveClearRequiresKillAll ?? true;
this.rng = new SeededRNG(options.seed);
const level = options.config.getLevel(options.levelId);
if (!level) {
throw new Error(`[BattleSession] level not found: ${options.levelId}`);
}
this.levelDef = level;
this.grid = new GridMap(level.mapRows, level.mapCols, level.cellSize);
this.spatial = new SpatialGrid(this.grid);
this.pathfinder = new Pathfinder(this.grid);
// 玩家槽(deploy/wave 依赖)
this.players = this.mode.createPlayers(this);
// —— 按依赖顺序构造子系统(this 作为 BattleServices 引用传给需要它的子系统) ——
this.effects = new EffectExecutor();
this.attributes = new AttributeSystem(this.world);
this.targets = new TargetResolver(this.world, this.attributes, this.spatial, this.grid);
this.abilities = new AbilitySystem(
this.world, this.config, this.attributes, this.targets,
this.effects, this.clock, this.events, this,
);
this.presentation = new PresentationPipeline(this.world, this.effects);
this.presentation.setLockHandler((caster, locked, meta) => {
this.abilities.setPresentationLocked(caster, locked, meta);
});
this.presentation.setComboFlushHandler((caster) => {
this.abilities.flushComboAfterPresentation(caster);
});
this.lifecycle = new LifecycleSystem(
this.world, this.grid, this.spatial, this.events, this.abilities,
);
this.lifecycle.setTryDeferKillPredicate((victim, killer, hpBeforeKill) => {
return this.tryDeferEntityKill?.(victim, killer, hpBeforeKill) ?? false;
});
this.buffs = new BuffSystem(this.world, this.config, this.effects, this);
this.damage = new DamagePipeline(
this.world, this.attributes, this.events, this.lifecycle, this.effects, this.grid,
);
this.projectiles = new ProjectileSystem(
this.world, this.grid, this.spatial, this.effects, this, this.events,
);
this.movement = new MovementSystem(this.world, this.grid, this.pathfinder, this.spatial, this.topology);
this.deploy = new DeploySystem(
this.world, this.config, this.grid, this.spatial,
this.abilities, this.events, this.players, this,
);
this.spawner = new WaveSpawner(
this.world, this.config, this.events, this.movement,
this.grid, this.topology, this,
);
// 注入运行时回调(避免子系统反向持有 session)
this.movement.setPhaseQuery(this._queryPhase);
this.spawner.setOnWaveCleared(() => this.onWaveCleared());
// 注册 tick 系统(按 priority 升序调度)
this.systems.register(this.attributes);
this.systems.register(this.buffs);
this.systems.register(this.abilities);
this.systems.register(this.projectiles);
this.systems.register(this.movement);
this.systems.register(new ReachGoalBaseDamageSystem(
this.world, this.players, this.events, this.lifecycle, this._queryPhase,
));
this.systems.register(this.spawner);
this._bridge = options.bridge;
if (this._bridge?.onBattleEvent) {
const bridge = this._bridge;
this.events.on(BATTLE_EVENT_ALL, (event) => {
bridge.onBattleEvent!(this, event);
});
}
}
// —— BattleServices:动态状态 getter ——
/** 当前战斗阶段 */
get phase(): BattlePhase {
return this._phase;
}
/** 快照用:PAUSE 时取暂停前相位 */
getSnapshotPhase(): BattlePhase {
if (this._phase === BattlePhase.PAUSE && this._phaseBeforePause != null) {
return this._phaseBeforePause;
}
return this._phase;
}
/** 连续战斗流程(常显部署面板、波次自动推进) */
get isContinuousFlow(): boolean {
return this.flowMode === BattleFlowMode.Continuous;
}
/** 是否处于波间/首波开怪等待(COMBAT 阶段、prepTime 倒计时中) */
get isWaveIntervalWaiting(): boolean {
return this._phase === BattlePhase.COMBAT && this._waveAdvancePending;
}
/** 连续模式:波间剩余秒数 */
get waveIntervalRemaining(): number {
return Math.max(0, this._waveAdvanceTimer);
}
/** 项目层桥接(可选) */
get bridge(): IBattleBridge | undefined {
return this._bridge;
}
/** @param bridge 运行时注入的表现桥接 */
setBridge(bridge: IBattleBridge | undefined): void {
this._bridge = bridge;
}
/** 本地玩家槽(索引 0) */
get localPlayer(): PlayerSlot {
return this.players[0];
}
/**
* 导入地图拓扑(项目层在 prefab 就绪后调用)
* @param snapshot 中性拓扑快照
* @param campPreset 可选阵营策略 preset(多区域地图默认 pathOnly)
*/
applyTopology(snapshot: MapTopologySnapshot, campPreset?: CampAreaPreset): void {
this.topology.apply(this.grid, snapshot);
this.campPolicy = resolveCampAreaPolicy(this.topology, campPreset);
// apply 会改攻击点/阵营区;须 invalidate,否则 early-out 可能沿用旧拓扑场
this.pathfinder.invalidate();
this.pathfinder.rebuild(this.topology);
}
/** 格可走状态变更后重建寻路并刷新敌人路径 */
notifyWalkabilityChanged(): void {
this.topology.rebuildPatrolLoop(this.grid);
this.pathfinder.invalidate();
this.pathfinder.rebuild(this.topology);
this.movement.refreshEnemyPaths();
}
/** @param phase 新阶段 */
setPhase(phase: BattlePhase): void {
const prev = this._phase;
this._phase = phase;
this.events.emit(BattleEventFactory.phaseChanged(phase, prev));
}
/** 进入备战阶段 */
startPrep(): void {
this.setPhase(BattlePhase.PREP);
}
/** 备战结束 / 连续模式开波 */
startCombat(): void {
if (this.ended) return;
if (!this.isContinuousFlow && this._phase !== BattlePhase.PREP) return;
if (!this.spawner.hasMoreWaves()) return;
if (this.spawner.isWaveCombatActive()) return;
if (this._waveAdvancePending) return;
if (this._phase !== BattlePhase.COMBAT) {
this.setPhase(BattlePhase.COMBAT);
}
this._tryStartWaveWithPrepDelay(this.spawner.waveIndex + 1);
}
/**
* 快照恢复:已在某波内但出怪队列丢失/未激活时,重建本波并切到 COMBAT(不 startNextWave 递增波次)。
*/
resumeCurrentWaveCombat(): boolean {
if (this.ended) return false;
if (!this.spawner.resumeCurrentWaveIfNeeded()) return false;
this._waveAdvancePending = false;
this._waveAdvanceTimer = 0;
if (this._phase !== BattlePhase.COMBAT) {
this.setPhase(BattlePhase.COMBAT);
}
return true;
}
/** 本波怪物全部击杀(出怪完毕且场上无敌人) */
onWaveCleared(): void {
if (this.ended) return;
const waveIndex = this.spawner.waveIndex;
this.events.emit(BattleEventFactory.waveEnd(waveIndex));
if (!this.spawner.hasMoreWaves()) {
if (this.localPlayer.wallHp <= 0) return;
this.endBattle(true, 'all_waves_cleared');
return;
}
if (this.isContinuousFlow) {
this._scheduleNextWave(waveIndex + 1);
return;
}
this.startPrep();
}
/** 按目标波 prepTime 延迟开波,或立即 startNextWave */
private _tryStartWaveWithPrepDelay(waveIndex: number): void {
const wave = this.config.getWave(this.levelId, waveIndex);
const delay = wave?.prepTime ?? 0;
if (delay > 0) {
this._waveAdvanceTimer = delay;
this._waveAdvancePending = true;
return;
}
this._waveAdvancePending = false;
this._waveAdvanceTimer = 0;
this.spawner.startNextWave();
}
/** 连续模式:按下一波 prepTime 延迟开波 */
private _scheduleNextWave(nextWaveIndex: number): void {
this._tryStartWaveWithPrepDelay(nextWaveIndex);
}
/**
* 销毁会话运行时状态(战斗结束、UI 关闭时调用)。
* 清空子系统缓存、事件待发队列,切断对配表的运行时引用,配合 world.gc 释放组件。
*/
dispose(): void {
this.abilities.clear();
this.presentation.clear();
this.events.flush();
this.clock.reset();
this._bridge = undefined;
// 清空玩家部署列表对实体的引用
for (const p of this.players) p.deployedUnits = [];
this.world.gc();
}
/** 暂停战斗(保留暂停前阶段,供 resume 还原;WAIT_REVIVE 本身已停表,不再叠成 PAUSE) */
pause(): void {
if (this.ended || this._phase === BattlePhase.END) return;
if (this._phase === BattlePhase.PAUSE || this._phase === BattlePhase.WAIT_REVIVE) return;
this._phaseBeforePause = this._phase;
this.setPhase(BattlePhase.PAUSE);
}
/** 从暂停恢复到暂停前阶段(PREP / COMBAT) */
resume(): void {
if (this.ended || this._phase === BattlePhase.END) return;
if (this._phase !== BattlePhase.PAUSE) return;
const next = this._phaseBeforePause ?? BattlePhase.COMBAT;
this._phaseBeforePause = null;
this.setPhase(next === BattlePhase.PAUSE ? BattlePhase.COMBAT : next);
}
/** 城墙归零进入等待复活(停表;仅 COMBAT 可进入) */
enterWaitRevive(): void {
if (this.ended || this._phase !== BattlePhase.COMBAT) return;
this._phaseBeforePause = null;
this.setPhase(BattlePhase.WAIT_REVIVE);
}
/** 复活成功后回到战斗 */
leaveWaitReviveToCombat(): void {
if (this._phase !== BattlePhase.WAIT_REVIVE) return;
this.setPhase(BattlePhase.COMBAT);
}
/**
* PauseGate 停表期间仍推进强制路径(芭蕉扇推回等),并同步表现。
* 不推进出怪、攻击、波次等其它系统。
*/
tickForcedPathsWhilePaused(dt: number): void {
if (this._phase !== BattlePhase.PAUSE || dt <= 0) return;
this.movement.tick(dt);
this._bridge?.onCombatTick?.(this, dt);
}
/** @param dt 帧间隔(秒) */
tick(dt: number): void {
if (
this._phase === BattlePhase.END
|| this._phase === BattlePhase.PAUSE
|| this._phase === BattlePhase.WAIT_REVIVE
) {
// 停表前一帧可能已 emit(如 BattleEnd)却因提前 return 未 flush;此处补派发
this.events.flush();
return;
}
// 逻辑固定步长:把引擎可变 dt 累加到累加器,按 fixedStep 切片推进子系统,
// 低帧率下逻辑行为不漂移(攻速 CD / buff 计时一致);高帧率下合并多帧减少开销。
// spiral-of-death 保护:单帧最多追 maxFixedIterations 步。
const step = this.fixedStep;
if (step > 0) {
this._fixedAccumulator += Math.min(dt, step * this.maxFixedIterations);
let iter = 0;
while (this._fixedAccumulator >= step && iter < this.maxFixedIterations) {
this._fixedAccumulator -= step;
this._tickOnce(step);
iter++;
const ph = this._phase as BattlePhase;
if (
ph === BattlePhase.END
|| ph === BattlePhase.PAUSE
|| ph === BattlePhase.WAIT_REVIVE
) {
this._presentCombat(dt);
this.events.flush();
return;
}
}
if (this._fixedAccumulator > step * this.maxFixedIterations) {
this._fixedAccumulator = 0;
}
} else {
this._tickOnce(dt);
}
this._presentCombat(dt);
this.events.flush();
}
/** 单次逻辑推进(固定步长或可变步长共用) */
private _tickOnce(dt: number): void {
this.clock.advance(dt);
this._tickFrame++;
// 波间/首波 prepTime 倒计时(COMBAT 阶段,到点开下一波)
if (this._waveAdvancePending && this._phase === BattlePhase.COMBAT) {
this._waveAdvanceTimer -= dt;
if (this._waveAdvanceTimer <= 0) {
this._waveAdvancePending = false;
this._waveAdvanceTimer = 0;
if (!this.spawner.isWaveCombatActive() && this.spawner.hasMoreWaves()) {
this.spawner.startNextWave();
}
}
}
const slice = this.tickSliceCount;
if (slice <= 1) {
this.systems.tick(dt);
} else {
const idx = (this._tickFrame - 1) % slice;
this.systems.tickSlice(dt, idx, slice);
}
// 周期回收已销毁实体的残留组件,避免长波次/长时间战斗内存爬升
if (++this._gcCounter >= this._gcInterval) {
this._gcCounter = 0;
this.world.gc();
}
}
/** COMBAT 表现同步:每个渲染帧最多一次,不随固定步长追赶重复 */
private _presentCombat(dt: number): void {
if (this._phase === BattlePhase.COMBAT) {
this._bridge?.onCombatTick?.(this, dt);
}
}
/**
* @param win 是否胜利
* @param reason 结束原因
*/
endBattle(win: boolean, reason?: string): void {
if (this.ended) return;
this.ended = true;
this.win = win;
this.setPhase(BattlePhase.END);
this.events.emit(BattleEventFactory.battleEnd(win, reason));
}
}