BattleSessionLifecycleCoordinator.kt

package io.github.lishangbu.avalon.game.battle.engine.core.session

import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot

/**
 * 管理回合末推进和手动替补提交流程。
 *
 * 它负责会话生命周期中两类“跨多步状态变更”的流程:
 * - 回合结束后的 residual、替补和 turn 递增
 * - 玩家对待处理替补请求提交具体 replacement choice
 */
internal class BattleSessionLifecycleCoordinator(
    private val session: BattleSession,
    private val sideConditionDurationManager: BattleSideConditionDurationManager = BattleSideConditionDurationManager(),
    private val fieldEffectDurationManager: BattleFieldEffectDurationManager = BattleFieldEffectDurationManager(),
    private val unitEffectDurationManager: BattleUnitEffectDurationManager = BattleUnitEffectDurationManager(),
) {
    /**
     * 提交一个手动替补选择。
     *
     * @param sideId 需要替补的 side
     * @param incomingUnitId 被选中的替补单位
     * @return 更新后的 battle snapshot
     */
    fun submitReplacementChoice(
        sideId: String,
        incomingUnitId: String,
    ): BattleRuntimeSnapshot {
        // 手动替补只允许发生在当前回合已经被打断、明确等待 replacement 的阶段。
        session.ensureAwaitingReplacement()
        val request =
            requireNotNull(session.replacementRequests.firstOrNull { candidate -> candidate.sideId == sideId }) {
                "No replacement request found for side '$sideId'."
            }
        require(incomingUnitId in request.candidateUnitIds) {
            "Unit '$incomingUnitId' is not a valid replacement candidate for side '$sideId'."
        }
        val side = requireNotNull(session.currentSnapshot.sides[sideId]) { "Side '$sideId' was not found." }
        val outgoingUnitId = request.outgoingUnitIds.firstOrNull()
        if (outgoingUnitId != null) {
            val outgoingUnit = session.currentSnapshot.units[outgoingUnitId]
            if ((outgoingUnit?.currentHp ?: 0) > 0) {
                session.currentSnapshot = session.processSwitchOut(outgoingUnitId, session.currentSnapshot)
            }
            session.currentSnapshot = BattleSessionSwitchBoostStateSupport.clearOutgoingBoosts(session.currentSnapshot, outgoingUnitId)
        }
        val nextActiveIds = listOf(incomingUnitId)
        val nextSides = session.currentSnapshot.sides + (sideId to side.copy(activeUnitIds = nextActiveIds))
        val replacedSnapshot = session.currentSnapshot.copy(sides = nextSides)
        session.currentSnapshot =
            session.clearForceSwitchRequests(
                snapshot = replacedSnapshot,
                unitIds =
                    buildList {
                        outgoingUnitId?.let(::add)
                        add(incomingUnitId)
                    },
            )
        session.currentSnapshot =
            BattleSessionSwitchBoostStateSupport.applyIncomingBoostCarry(
                snapshot = session.currentSnapshot,
                outgoingUnitId = outgoingUnitId,
                incomingUnitId = incomingUnitId,
            )
        session.currentSnapshot = session.processSwitchIn(incomingUnitId, session.currentSnapshot)
        session.replacementRequests.removeIf { candidate -> candidate.sideId == sideId }
        session.recordLog("Submitted replacement choice for side $sideId: $incomingUnitId.")
        session.recordEvent(
            BattleSessionAutoReplacedPayload(
                sideId = sideId,
                after = nextActiveIds,
                manual = true,
            ),
        )
        return session.currentSnapshot
    }

    /**
     * 推进到回合结束。
     *
     * 流程顺序固定为:
     * 1. residual phase
     * 2. 濒死/替补处理
     * 3. turn + 1
     */
    fun endTurn(): BattleRuntimeSnapshot {
        session.ensureRunning()
        val residualSnapshot = session.battleFlowEngine.resolveResidualPhase(session.currentSnapshot)
        val advancedField = fieldEffectDurationManager.advance(residualSnapshot)
        val advancedSide = sideConditionDurationManager.advance(advancedField.snapshot)
        val advancedUnit = unitEffectDurationManager.advance(advancedSide.snapshot)
        val advancedDurationSnapshot =
            applyDurationExpirationMutations(
                snapshot = advancedUnit.snapshot,
                expirationMutations =
                    advancedField.expirationMutations +
                        advancedSide.expirationMutations +
                        advancedUnit.expirationMutations,
            )
        session.currentSnapshot = session.resolveFaintAndReplacement(advancedDurationSnapshot)
        session.currentSnapshot =
            session.currentSnapshot.copy(
                battle = session.currentSnapshot.battle.copy(turn = session.currentSnapshot.battle.turn + 1),
            )
        session.recordLog("Turn ended. Advanced to turn ${session.currentSnapshot.battle.turn}.")
        session.recordEvent(BattleSessionTurnEndedPayload)
        return session.currentSnapshot
    }

    /**
     * 把 duration 到期产生的移除 mutation 重新送回正式生命周期链。
     *
     * 这样“显式 remove”与“回合末自然到期 remove”都会走同一套 interceptor/applier。
     */
    private fun applyDurationExpirationMutations(
        snapshot: BattleRuntimeSnapshot,
        expirationMutations: List<BattleSessionScopedMutation>,
    ): BattleRuntimeSnapshot {
        if (expirationMutations.isEmpty()) {
            return snapshot
        }
        var currentSnapshot = snapshot
        expirationMutations.forEach { scopedMutation ->
            currentSnapshot =
                session.applyMutations(
                    snapshot = currentSnapshot,
                    selfId = scopedMutation.selfId,
                    targetId = scopedMutation.targetId,
                    sourceId = scopedMutation.sourceId,
                    mutations = listOf(scopedMutation.mutation),
                )
        }
        return currentSnapshot
    }
}