BattleFieldEffectDurationManager.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.FieldState
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ClearTerrainMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ClearWeatherMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.RemoveFieldConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot

/**
 * 天气 / 地形的持续回合推进器。
 *
 * 设计意图:
 * - 在回合末统一推进 field-attached effect 的持续时间;
 * - 只以 `weatherState / terrainState / conditionStates` 作为场地挂载效果的唯一真实来源。
 */
internal class BattleFieldEffectDurationManager {
    /**
     * 推进当前快照上的天气与地形持续时间。
     */
    fun advance(snapshot: BattleRuntimeSnapshot): BattleEffectDurationAdvanceResult {
        val expirationMutations = mutableListOf<BattleSessionScopedMutation>()
        val nextField = advanceField(snapshot.field, expirationMutations)
        val nextSnapshot =
            if (nextField == snapshot.field) {
                snapshot
            } else {
                snapshot.copy(field = nextField)
            }
        return BattleEffectDurationAdvanceResult(
            snapshot = nextSnapshot,
            expirationMutations = expirationMutations,
        )
    }

    /**
     * 推进当前 field 上的天气、地形与 field condition 持续时间。
     */
    private fun advanceField(
        field: FieldState,
        expirationMutations: MutableList<BattleSessionScopedMutation>,
    ): FieldState {
        val nextWeatherState =
            advanceState(field.weatherState) { state ->
                expirationMutations +=
                    BattleSessionScopedMutation(
                        selfId = null,
                        targetId = null,
                        sourceId = state.sourceId,
                        mutation = ClearWeatherMutation(),
                    )
            }
        val nextTerrainState =
            advanceState(field.terrainState) { state ->
                expirationMutations +=
                    BattleSessionScopedMutation(
                        selfId = null,
                        targetId = null,
                        sourceId = state.sourceId,
                        mutation = ClearTerrainMutation(),
                    )
            }
        val nextConditionStates = linkedMapOf<String, AttachedEffectState>()
        field.conditionStates.values
            .sortedBy(AttachedEffectState::effectOrder)
            .forEach { state ->
                val nextState =
                    advanceState(state) { expiredState ->
                        expirationMutations +=
                            BattleSessionScopedMutation(
                                selfId = null,
                                targetId = null,
                                sourceId = expiredState.sourceId,
                                mutation = RemoveFieldConditionMutation(expiredState.effectId),
                            )
                    }
                if (nextState != null) {
                    nextConditionStates[nextState.effectId] = nextState
                }
            }
        return field.copy(
            weatherState = nextWeatherState,
            terrainState = nextTerrainState,
            conditionStates = nextConditionStates,
        )
    }

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