BattleSideConditionDurationManager.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.RemoveSideConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
import io.github.lishangbu.avalon.game.battle.engine.core.type.StandardTargetSelectorIds

/**
 * side condition 持续回合推进器。
 *
 * 设计意图:
 * - 在回合末统一衰减并清理带固定持续时间的 side condition。
 * - 把 duration 语义从 `BattleSessionLifecycleCoordinator` 中拆出来,避免回合流程代码继续膨胀。
 *
 * 当前约定:
 * - `duration = null` 视为无固定回合数,不会在这里被自动移除;
 * - 每次 `endTurn()` 完成 residual 后,所有 side condition 的剩余回合数减 1;
 * - 当剩余回合数降到 0 时,该 side condition 会被从 side 上移除。
 */
internal class BattleSideConditionDurationManager {
    /**
     * 推进当前快照中全部 side condition 的剩余持续时间。
     */
    fun advance(snapshot: BattleRuntimeSnapshot): BattleEffectDurationAdvanceResult {
        if (snapshot.sides.isEmpty()) {
            return BattleEffectDurationAdvanceResult(snapshot)
        }
        val expirationMutations = mutableListOf<BattleSessionScopedMutation>()
        val nextSides =
            snapshot.sides.mapValues { (_, side) ->
                advanceSide(side, expirationMutations)
            }
        val nextSnapshot =
            if (nextSides == snapshot.sides) {
                snapshot
            } else {
                snapshot.copy(sides = nextSides)
            }
        return BattleEffectDurationAdvanceResult(
            snapshot = nextSnapshot,
            expirationMutations = expirationMutations,
        )
    }

    /**
     * 推进单个 side 上全部 side condition 的剩余持续时间。
     */
    private fun advanceSide(
        side: SideState,
        expirationMutations: MutableList<BattleSessionScopedMutation>,
    ): SideState {
        if (side.conditionStates.isEmpty()) {
            return side
        }
        val contextUnitId = (side.activeUnitIds + side.unitIds).distinct().firstOrNull()

        val nextStates = linkedMapOf<String, AttachedEffectState>()
        side.conditionStates.values
            .sortedBy(AttachedEffectState::effectOrder)
            .forEach { state ->
                val nextState =
                    advanceState(state) { expiredState ->
                        if (contextUnitId != null) {
                            expirationMutations +=
                                BattleSessionScopedMutation(
                                    selfId = contextUnitId,
                                    targetId = contextUnitId,
                                    sourceId = expiredState.sourceId,
                                    mutation =
                                        RemoveSideConditionMutation(
                                            target = StandardTargetSelectorIds.SIDE,
                                            conditionEffectId = expiredState.effectId,
                                        ),
                                )
                        }
                    }
                if (nextState != null) {
                    nextStates[nextState.effectId] = nextState
                }
            }
        return if (nextStates == side.conditionStates) {
            side
        } else {
            side.copy(
                conditionStates = nextStates,
            )
        }
    }

    /**
     * 推进单个 side condition 的剩余回合。
     *
     * 返回空值表示该 condition 已在本次回合结束时自然到期。
     */
    private fun advanceState(
        state: AttachedEffectState,
        onExpired: (AttachedEffectState) -> Unit = {},
    ): AttachedEffectState? {
        val duration = state.duration ?: return state
        val nextDuration = duration - 1
        return if (nextDuration > 0) {
            state.copy(duration = nextDuration)
        } else {
            onExpired(state)
            state
        }
    }
}