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