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.
515 lines
20 KiB
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));
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|