BattleSessionActionInvalidationResolver.kt

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

/**
 * 回合中途动作失效判定器。
 *
 * 设计意图:
 * - 一些 action 在入队时是合法的,但在真正轮到执行前,战场已经被前置 action 改写。
 * - 典型场景是“单位先被打倒,再轮到自己出手”,这时应当跳过该动作,而不是在更深层抛异常。
 * - 把这层判定集中到 executor 前面,避免各个 handler 自己重复做脆弱判断。
 */
internal class BattleSessionActionInvalidationResolver(
    private val session: BattleSession,
) {
    /**
     * 返回当前 action 的失效原因;为空表示仍可继续执行。
     */
    fun resolve(action: BattleSessionAction): String? =
        when (action) {
            is BattleSessionSwitchAction -> resolveSwitchInvalidation(action)
            is BattleSessionRunAction -> resolveRunInvalidation(action)
            is BattleSessionTargetedAction -> resolveTargetedActionInvalidation(action)
            is BattleSessionSubmittingAction -> resolveSubmittingActionInvalidation(action)
            else -> null
        }

    /**
     * 校验由具体单位提交的动作是否仍有执行资格。
     *
     * 这里优先看“单位是否还活着”,再看“单位是否仍在 active 槽位”,
     * 这样返回给上层的原因更贴近实际战场变化。
     */
    private fun resolveSubmittingActionInvalidation(action: BattleSessionSubmittingAction): String? {
        val unit =
            session.currentSnapshot.units[action.submittingUnitId]
                ?: return "Submitting unit '${action.submittingUnitId}' was not found."
        if (unit.currentHp <= 0) {
            return "Submitting unit '${action.submittingUnitId}' has fainted."
        }
        val activeSide =
            session.currentSnapshot.sides.values.firstOrNull { side ->
                action.submittingUnitId in side.activeUnitIds
            }
        if (activeSide == null) {
            return "Submitting unit '${action.submittingUnitId}' is no longer active."
        }
        return null
    }

    /**
     * 校验带显式目标的动作是否仍能命中原目标。
     *
     * 当前策略比较保守:
     * - 目标不存在或已经倒下时直接跳过;
     * - 捕捉动作还要求目标继续留在可捕捉 side 的 active 槽位里。
     */
    private fun resolveTargetedActionInvalidation(action: BattleSessionTargetedAction): String? {
        resolveSubmittingActionInvalidation(action)?.let { reason ->
            return reason
        }
        val targetUnit =
            session.currentSnapshot.units[action.targetUnitId]
                ?: return "Target unit '${action.targetUnitId}' was not found."
        if (targetUnit.currentHp <= 0) {
            return "Target unit '${action.targetUnitId}' has fainted."
        }
        if (action is BattleSessionCaptureAction) {
            val capturableSideId = session.currentSnapshot.battle.capturableSideId
            val capturableSide = capturableSideId?.let(session.currentSnapshot.sides::get)
            if (capturableSide == null || action.targetUnitId !in capturableSide.activeUnitIds) {
                return "Capture target '${action.targetUnitId}' is no longer capturable."
            }
        }
        return null
    }

    /**
     * 校验换人动作是否仍有意义。
     */
    private fun resolveSwitchInvalidation(action: BattleSessionSwitchAction): String? {
        val side =
            session.currentSnapshot.sides[action.sideId]
                ?: return "Side '${action.sideId}' was not found."
        if (action.outgoingUnitId !in side.activeUnitIds) {
            return "Outgoing unit '${action.outgoingUnitId}' is no longer active on side '${action.sideId}'."
        }
        val incomingUnit =
            session.currentSnapshot.units[action.incomingUnitId]
                ?: return "Incoming unit '${action.incomingUnitId}' was not found."
        if (incomingUnit.currentHp <= 0) {
            return "Incoming unit '${action.incomingUnitId}' has fainted."
        }
        return null
    }

    /**
     * 校验逃跑动作是否仍有发起者。
     */
    private fun resolveRunInvalidation(action: BattleSessionRunAction): String? {
        val side =
            session.currentSnapshot.sides[action.sideId]
                ?: return "Side '${action.sideId}' was not found."
        val hasActiveUnit =
            side.activeUnitIds.any { unitId ->
                (session.currentSnapshot.units[unitId]?.currentHp ?: 0) > 0
            }
        return if (hasActiveUnit) {
            null
        } else {
            "Side '${action.sideId}' has no active unit that can run."
        }
    }
}