BattleSessionHookLifecycleProcessor.kt
package io.github.lishangbu.avalon.game.battle.engine.core.session
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleFlowPhaseProcessor
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
/**
* 会话层生命周期 Hook 调度器。
*
* 设计意图:
* - 把“不是 move pipeline 自身阶段的一组生命周期 hook”统一收敛到一个小组件里;
* - 避免把 `BattleSession`、`BattleSessionActionExecutionSupport`、
* `BattleSessionReplacementResolver` 等多个编排类继续堆成一个大文件;
* - 让 `before_turn / switch_in / switch_out / faint` 这些生命周期点都走同一条
* attached-effect 扫描链,从而复用当前 battle-engine 已有的 rule、condition、
* mutation interceptor 与 mutation apply 体系。
*
* 当前约定:
* - 这些生命周期 hook 都通过 `processAttachedEffects(...)` 触发,而不是构造一个伪 move;
* - `on_faint` 需要保证同一次倒下只派发一次,因此这里会在 `UnitState.sessionState` 中写入
* 一个内部工作位;
* - 如果某个单位在后续流程中被重新拉回可战斗状态,该内部标记会在下一次扫描时自动清理,
* 以便后续再次倒下时仍然能正确触发 `on_faint`。
*/
internal class BattleSessionHookLifecycleProcessor(
private val session: BattleSession,
private val battleFlowPhaseProcessor: BattleFlowPhaseProcessor,
) {
private val attachedHookDispatcher: BattleSessionAttachedHookDispatcher =
BattleSessionAttachedHookDispatcher(battleFlowPhaseProcessor)
private val faintHookStateManager: BattleSessionFaintHookStateManager = BattleSessionFaintHookStateManager()
/**
* 在回合正式执行 action 之前,对当前全部 active 单位派发一次 `on_before_turn`。
*
* 这样当前 active 槽位上的 ability / item / status / weather / terrain
* 都有机会在本回合开始前做统一修正。
*/
fun processBeforeTurn(snapshot: BattleRuntimeSnapshot = session.currentSnapshot): BattleRuntimeSnapshot =
attachedHookDispatcher.processHookForUnitIds(
snapshot = snapshot,
unitIds = activeUnitIds(snapshot),
hookName = StandardHookNames.ON_BEFORE_TURN.value,
)
/**
* 对指定单位派发一次 `on_switch_out`。
*
* 这个 hook 只在“主动离场”的编排点调用:
* - 玩家提交 `switch`
* - 强制换人导致活着的 active 单位被替下
*
* 对于已经倒下的单位,当前不把它视为正常的 switch-out 生命周期。
*/
fun processSwitchOut(
unitId: String,
snapshot: BattleRuntimeSnapshot = session.currentSnapshot,
): BattleRuntimeSnapshot = attachedHookDispatcher.processHookForUnit(snapshot, unitId, StandardHookNames.ON_SWITCH_OUT.value)
/**
* 对指定单位派发一次 `on_switch_in`。
*
* 这个 hook 会在单位已经进入 `activeUnitIds` 之后触发,
* 这样 attached effect 在读取当前 snapshot 时能看到“它已经在场”这一事实。
*/
fun processSwitchIn(
unitId: String,
snapshot: BattleRuntimeSnapshot = session.currentSnapshot,
): BattleRuntimeSnapshot = attachedHookDispatcher.processHookForUnit(snapshot, unitId, StandardHookNames.ON_SWITCH_IN.value)
/**
* 扫描当前快照里全部“尚未派发过 faint hook 的倒下单位”,并逐个触发 `on_faint`。
*
* 这里使用循环而不是一次性快照列表,原因是:
* - `on_faint` 自己也可能继续产生 mutation;
* - 这些 mutation 又可能让新的单位在同一轮流程中倒下;
* - 因此需要在每一轮 hook 写回后重新读取当前 snapshot,直到没有新的未处理倒下单位。
*/
fun processFaintHooks(snapshot: BattleRuntimeSnapshot = session.currentSnapshot): BattleRuntimeSnapshot {
var currentSnapshot = faintHookStateManager.clearRecoveredFaintFlags(snapshot)
while (true) {
val faintedUnitId = faintHookStateManager.firstUnhandledFaintedUnitId(currentSnapshot) ?: return currentSnapshot
currentSnapshot = attachedHookDispatcher.processHookForUnit(currentSnapshot, faintedUnitId, StandardHookNames.ON_FAINT.value)
val updatedUnit = currentSnapshot.units[faintedUnitId]
currentSnapshot =
if ((updatedUnit?.currentHp ?: 0) <= 0) {
faintHookStateManager.markFaintHookProcessed(currentSnapshot, faintedUnitId)
} else {
faintHookStateManager.clearFaintHookProcessed(currentSnapshot, faintedUnitId)
}
}
}
/**
* 返回当前 battle 中全部 active 单位,保持 side 顺序与 active 槽位顺序稳定。
*/
private fun activeUnitIds(snapshot: BattleRuntimeSnapshot): List<String> =
snapshot.sides.values
.flatMap { side -> side.activeUnitIds }
.distinct()
}