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/combat/PresentationPipeline.ts

210 lines
7.6 KiB

3 weeks ago
import type { World, EntityId } from 'db://assets/mx/module/ecs';
import type { EffectSpec, ConditionSpec } from '../config/BattleConfigTypes';
import type { AbilityContext } from './AbilityContext';
import type { EffectExecutor } from './EffectExecutor';
import { ConditionRegistry } from '../registry/EffectRegistry';
import { createMembershipSet, membershipAdd, membershipForEach, type MembershipSet } from 'db://assets/mx/tools/MembershipSet';
/** 表现层 deferred 的逻辑 timeline 切片 */
export interface DeferredTimelineSlice {
/** 切片序号(多段命中时按序;与 applyNextEffect 顺序一致) */
index?: number;
/** 原 timeline 时刻(毫秒,供调试/排序) */
at: number;
effects: EffectSpec[];
conditions?: ConditionSpec[];
}
interface PendingPresentation {
ctx: AbilityContext;
slices: DeferredTimelineSlice[];
nextIndex: number;
state: 'pending' | 'finished' | 'cancelled';
}
/** 施法锁变化:releaseLock 只解冻 OnTick;finish 才触发 interval=0 连发 */
export interface PresentationLockChangeMeta {
flushCombo?: boolean;
}
/** 表现驱动 deferred 管线:hit()/finish() 推进逻辑 timeline */
export class PresentationPipeline {
private readonly _byCast = new Map<number, PendingPresentation>();
private readonly _casterCasts = new Map<EntityId, MembershipSet<number>>();
private _onLockChange: ((
caster: EntityId,
locked: boolean,
meta?: PresentationLockChangeMeta,
) => void) | null = null;
/** 表现 finish 后触发 base_para 连发(与施法锁回调分离,避免 releaseLock 竞态丢 flush) */
private _onComboFlush: ((caster: EntityId) => void) | null = null;
constructor(
private readonly _world: World,
private readonly _effects: EffectExecutor,
) {}
/** 施法锁回调(AbilitySystem 注册,表现未 finish 前冻结可打断 OnTick) */
setLockHandler(handler: (
caster: EntityId,
locked: boolean,
meta?: PresentationLockChangeMeta,
) => void): void {
this._onLockChange = handler;
}
/** 表现 finish 后触发 para_6 / atk_before 连发调度 */
setComboFlushHandler(handler: (caster: EntityId) => void): void {
this._onComboFlush = handler;
}
register(castInstanceId: number, ctx: AbilityContext, slices: DeferredTimelineSlice[]): void {
this._byCast.set(castInstanceId, {
ctx,
slices,
nextIndex: 0,
state: 'pending',
});
let set = this._casterCasts.get(ctx.caster);
if (!set) {
set = createMembershipSet();
this._casterCasts.set(ctx.caster, set);
}
membershipAdd(set, castInstanceId);
this._onLockChange?.(ctx.caster, true);
}
getPending(castInstanceId: number): Readonly<{
ctx: AbilityContext;
slices: readonly DeferredTimelineSlice[];
nextIndex: number;
}> | undefined {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return undefined;
return p;
}
updateTargets(castInstanceId: number, targets: EntityId[]): void {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return;
p.ctx.targets = [...targets];
}
/**
* 结算下一段 deferred 战斗效果(伤害、弹道等)。
* 只有显式 hit 才会造成伤害;可多次调用推进多段。
*/
hit(castInstanceId: number): boolean {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return false;
if (!p.ctx || !this._world.isAlive(p.ctx.caster)) {
this.cancel(castInstanceId);
return false;
}
if (p.nextIndex >= p.slices.length) {
return false;
}
const sliceIndex = p.nextIndex;
const ok = this._runSlice(p, sliceIndex);
return ok;
}
/** @deprecated 使用 hit */
applyNextEffect(castInstanceId: number): boolean {
return this.hit(castInstanceId);
}
/**
* 仅释放施法锁,保留 pending 供后续 hit()。
* 长表现大招(如飞龙)在飞行开始即解锁,带间隔连发由定时链调度,不在此 flush。
*/
releaseLock(castInstanceId: number): void {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return;
if (!p.ctx) return;
this._removeCastLock(castInstanceId, p.ctx.caster, false);
}
/**
* 结束表现:丢弃未 hit 的剩余切片并解锁施法,不造成伤害。
* 碰撞类技能可只 finish、不 hit。
*/
finish(castInstanceId: number): void {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return;
this._complete(castInstanceId, p);
}
/** @deprecated 使用 finish(不再自动刷剩余伤害) */
completePresentation(castInstanceId: number): void {
this.finish(castInstanceId);
}
cancel(castInstanceId: number): void {
const p = this._byCast.get(castInstanceId);
if (!p || p.state !== 'pending') return;
p.state = 'cancelled';
this._removeCast(castInstanceId, p.ctx?.caster ?? 0, false);
}
cancelByCaster(caster: EntityId): void {
const set = this._casterCasts.get(caster);
if (!set) return;
const ids: number[] = [];
membershipForEach(set, id => ids.push(id));
for (const id of ids) this.cancel(id);
}
clear(): void {
this._byCast.clear();
this._casterCasts.clear();
}
private _runSlice(p: PendingPresentation, index: number): boolean {
if (index < p.nextIndex) return false;
const slice = p.slices[index];
if (slice.conditions?.length) {
for (const c of slice.conditions) {
if (!ConditionRegistry.evaluate(p.ctx.services, p.ctx, c.type, c.params ?? {})) {
p.nextIndex = index + 1;
return false;
}
}
}
this._effects.executeAll(p.ctx, slice.effects);
p.nextIndex = index + 1;
return true;
}
private _complete(castInstanceId: number, p: PendingPresentation): void {
p.state = 'finished';
this._byCast.delete(castInstanceId);
const caster = p.ctx?.caster ?? 0;
const set = this._casterCasts.get(caster);
set?.delete(castInstanceId);
if (!set?.size) {
this._casterCasts.delete(caster);
}
// flush 可能同步 register 下一段连发:须在 flush 之后重算锁,
// 否则仍用「flush 前 stillLocked=false」调 _onLockChange 会清掉新施法锁,
// OnTick 再叠一层 cast → beginPresentationCast 序号覆盖 → 旧 pending 永不 finish → 永久停攻
this._onComboFlush?.(caster);
const after = this._casterCasts.get(caster);
const stillLocked = after != null && after.size > 0;
this._onLockChange?.(caster, stillLocked, { flushCombo: false });
}
private _removeCast(castInstanceId: number, caster: EntityId, flushCombo: boolean): void {
this._byCast.delete(castInstanceId);
this._removeCastLock(castInstanceId, caster, flushCombo);
}
private _removeCastLock(castInstanceId: number, caster: EntityId, flushCombo: boolean): void {
const set = this._casterCasts.get(caster);
set?.delete(castInstanceId);
if (!set?.size) {
this._casterCasts.delete(caster);
this._onLockChange?.(caster, false, { flushCombo });
}
}
}