BattleSessionReplacementPlanResolver.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState

/**
 * 替补需求与自动替补计划解析器。
 *
 * 设计意图:
 * - 收敛“哪些 active 需要被替下”“当前有哪些候补”“自动替补策略选中了谁”
 *   这一组纯解析逻辑;
 * - 让 `BattleSessionReplacementResolver` 不再自己维护大段集合运算;
 * - 为后续更复杂的 replacement policy 保留稳定的计划模型。
 */
internal class BattleSessionReplacementPlanResolver(
    private val replacementStrategy: ReplacementStrategy,
) {
    /**
     * 为单个 side 解析当前替补计划。
     */
    fun resolve(
        sideId: String,
        side: SideState,
        units: Map<String, UnitState>,
    ): BattleSessionReplacementPlan {
        val faintedActiveIds = side.activeUnitIds.filter { unitId -> (units[unitId]?.currentHp ?: 0) <= 0 }
        val forcedActiveIds = side.activeUnitIds.filter { unitId -> units[unitId]?.forceSwitchRequested == true }
        val requiredOutgoingUnitIds = (faintedActiveIds + forcedActiveIds).distinct()
        val nextActiveUnitIds = replacementStrategy.selectActiveUnitIds(side, units)
        val candidateUnitIds =
            side.unitIds
                .filterNot { unitId -> unitId in side.activeUnitIds }
                .filter { unitId -> (units[unitId]?.currentHp ?: 0) > 0 }
        return BattleSessionReplacementPlan(
            sideId = sideId,
            beforeActiveUnitIds = side.activeUnitIds,
            nextActiveUnitIds = nextActiveUnitIds,
            requiredOutgoingUnitIds = requiredOutgoingUnitIds,
            candidateUnitIds = candidateUnitIds,
            slotTransitions = slotTransitions(side.activeUnitIds, nextActiveUnitIds),
        )
    }

    /**
     * 以槽位维度比较换人前后 active 列表,提取出真正发生变化的“出场 / 入场”对。
     */
    private fun slotTransitions(
        before: List<String>,
        after: List<String>,
    ): List<BattleSessionSlotTransition> =
        (0 until maxOf(before.size, after.size))
            .mapNotNull { index ->
                val outgoingUnitId = before.getOrNull(index)
                val incomingUnitId = after.getOrNull(index)
                if (outgoingUnitId == incomingUnitId) {
                    null
                } else {
                    BattleSessionSlotTransition(
                        outgoingUnitId = outgoingUnitId,
                        incomingUnitId = incomingUnitId,
                    )
                }
            }
}