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)); } }