BattleSessionRunActionExecutor.kt

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

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

/**
 * session run action 执行器。
 *
 * 设计意图:
 * - 收敛逃跑 action 的成功/失败分支编排;
 * - 让逃跑失败计数、battle ended 写回和结构化事件记录
 *   不再混在通用 action support 中;
 * - 保持 run 尝试细节仍由 `BattleSessionRunAttemptResolver` 负责。
 */
class BattleSessionRunActionExecutor {
    /**
     * 执行逃跑动作。
     */
    fun apply(
        session: BattleSession,
        action: BattleSessionRunAction,
    ): BattleRuntimeSnapshot {
        val resolution = BattleSessionRunAttemptResolver.resolve(session, action)
        if (!resolution.success) {
            val failedAttempts = resolution.failedAttempts ?: session.currentSnapshot.battle.failedRunAttempts[action.sideId] ?: 0
            session.currentSnapshot =
                session.currentSnapshot.copy(
                    battle =
                        session.currentSnapshot.battle.copy(
                            failedRunAttempts = session.currentSnapshot.battle.failedRunAttempts + (action.sideId to failedAttempts),
                        ),
                )
            session.recordLog(
                "Run failed for side ${action.sideId}. " +
                    "reason=${resolution.reason} runner=${resolution.runnerUnitId} " +
                    "failedAttempts=$failedAttempts" +
                    (resolution.escapeValue?.let { " escapeValue=$it" } ?: "") +
                    (resolution.roll?.let { " roll=$it" } ?: "") +
                    ".",
            )
            session.recordEvent(
                BattleSessionRunFailedPayload(
                    sideId = action.sideId,
                    runnerUnitId = resolution.runnerUnitId,
                    reason = resolution.reason,
                    failedAttempts = failedAttempts,
                    escapeValue = resolution.escapeValue,
                    roll = resolution.roll,
                ),
            )
            return session.currentSnapshot
        }

        val survivingOpponent =
            session.currentSnapshot.sides.keys
                .firstOrNull { sideId -> sideId != action.sideId }
        // 逃跑成功时,胜负已经确定,但战后副作用仍然交给统一 settlement 处理。
        session.currentSnapshot =
            session.currentSnapshot.copy(
                battle =
                    session.currentSnapshot.battle.copy(
                        lifecycle = BattleLifecycle.ENDED_UNSETTLED,
                        winner = survivingOpponent,
                        endedReason = "run",
                    ),
            )
        session.recordLog(
            "Executed run action for side ${action.sideId}. " +
                "runner=${resolution.runnerUnitId} reason=${resolution.reason}. Winner: $survivingOpponent.",
        )
        session.recordEvent(
            BattleSessionBattleEndedPayload(
                winner = survivingOpponent,
                actionType = BattleSessionActionEventKind.RUN,
                runner = action.sideId,
            ),
        )
        return session.currentSnapshot
    }
}