BattleSessionRunAttemptResolver.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleItemIds
import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleStatIds
import io.github.lishangbu.avalon.game.battle.engine.core.model.BattleType
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleFieldConditionSupport
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleHeldItemRuntimeSupport
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatAliasResolver
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatStageSupport
import kotlin.math.floor
import kotlin.math.roundToInt

/**
 * session 逃跑判定解析器。
 *
 * 设计意图:
 * - 把逃跑公式、阻断条件和速度推导从 `BattleSessionActionExecutionSupport` 中拆出;
 * - 让 run action 执行只保留“成功/失败后如何写回 session”这一层编排逻辑。
 */
internal object BattleSessionRunAttemptResolver {
    val directTrapConditions: Set<String> =
        setOf(
            "trapped",
            "partially-trapped",
            "cannot-escape",
        )

    fun resolve(
        session: BattleSession,
        action: BattleSessionRunAction,
    ): BattleSessionRunResolution {
        val battle = session.currentSnapshot.battle
        if (battle.battleKind != BattleType.WILD) {
            return BattleSessionRunResolution(success = false, reason = "run-not-allowed")
        }
        val side =
            session.currentSnapshot.sides[action.sideId]
                ?: return BattleSessionRunResolution(success = false, reason = "side-not-found")
        val opponents =
            session.currentSnapshot.sides.values
                .filter { other -> other.id != action.sideId }
        val runner = resolveRunnerUnit(session, side)
        if (runner == null) {
            return BattleSessionRunResolution(success = false, reason = "runner-not-found")
        }
        if (guaranteedRunSuccess(runner, session)) {
            return BattleSessionRunResolution(
                success = true,
                runnerUnitId = runner.id,
                reason = "guaranteed",
            )
        }
        blockedRunReason(runner, opponents, session)
            ?.let { reason ->
                return BattleSessionRunResolution(
                    success = false,
                    runnerUnitId = runner.id,
                    reason = reason,
                    failedAttempts = (battle.failedRunAttempts[action.sideId] ?: 0) + 1,
                )
            }

        val failedAttempts = (battle.failedRunAttempts[action.sideId] ?: 0) + 1
        val runnerSpeed = effectiveSpeed(runner).coerceAtLeast(1)
        val opponentSpeed =
            opponents
                .flatMap(SideState::activeUnitIds)
                .mapNotNull(session.currentSnapshot.units::get)
                .maxOfOrNull(::effectiveSpeed)
                ?.coerceAtLeast(1)
                ?: 1
        val scaledOpponentSpeed = ((opponentSpeed / 4) % 256).coerceAtLeast(1)
        val escapeValue = floor((runnerSpeed * 32.0) / scaledOpponentSpeed.toDouble()).toInt() + (30 * failedAttempts)
        if (escapeValue > 255) {
            return BattleSessionRunResolution(
                success = true,
                runnerUnitId = runner.id,
                reason = "formula",
                failedAttempts = failedAttempts,
                escapeValue = escapeValue,
            )
        }
        val roll = session.nextRandomInt(256)
        return if (roll < escapeValue) {
            BattleSessionRunResolution(
                success = true,
                runnerUnitId = runner.id,
                reason = "formula",
                failedAttempts = failedAttempts,
                escapeValue = escapeValue,
                roll = roll,
            )
        } else {
            BattleSessionRunResolution(
                success = false,
                runnerUnitId = runner.id,
                reason = "formula",
                failedAttempts = failedAttempts,
                escapeValue = escapeValue,
                roll = roll,
            )
        }
    }

    private fun resolveRunnerUnit(
        session: BattleSession,
        side: SideState,
    ): UnitState? =
        side.activeUnitIds
            .mapNotNull(session.currentSnapshot.units::get)
            .maxByOrNull(::effectiveSpeed)

    private fun guaranteedRunSuccess(
        unit: UnitState,
        session: BattleSession,
    ): Boolean =
        "ghost" in unit.typeIds ||
            unit.abilityId == "run-away" ||
            BattleHeldItemRuntimeSupport.hasActiveItem(unit, session.currentSnapshot.field, BattleItemIds.SMOKE_BALL)

    private fun blockedRunReason(
        runner: UnitState,
        opponents: List<SideState>,
        session: BattleSession,
    ): String? {
        if (directTrapConditions.any { it in runner.conditionStates || it in runner.volatileStates }) {
            return "direct-trap"
        }
        val opponentUnits =
            opponents
                .flatMap(SideState::activeUnitIds)
                .mapNotNull(session.currentSnapshot.units::get)
        val shadowTagged =
            opponentUnits.any { unit ->
                unit.abilityId == "shadow-tag" &&
                    runner.abilityId != "shadow-tag"
            }
        if (shadowTagged) {
            return "shadow-tag"
        }
        val arenaTrapped =
            opponentUnits.any { unit -> unit.abilityId == "arena-trap" } &&
                isGrounded(runner, session)
        return if (arenaTrapped) {
            "arena-trap"
        } else {
            null
        }
    }

    private fun isGrounded(
        unit: UnitState,
        session: BattleSession,
    ): Boolean {
        if (BattleFieldConditionSupport.hasGravity(session.currentSnapshot)) {
            return true
        }
        if ("flying" in unit.typeIds) {
            return false
        }
        if (unit.abilityId == "levitate") {
            return false
        }
        if (BattleHeldItemRuntimeSupport.hasActiveItem(unit, session.currentSnapshot.field, BattleItemIds.AIR_BALLOON)) {
            return false
        }
        if ("magnet-rise" in unit.volatileStates || "magnet-rise" in unit.conditionStates) {
            return false
        }
        return true
    }

    private fun effectiveSpeed(unit: UnitState): Int {
        val baseSpeed = BattleStatAliasResolver.readValue(unit.stats, BattleStatIds.SPEED) ?: 0
        val stage = BattleStatStageSupport.readStage(unit.boosts, BattleStatIds.SPEED)
        return (baseSpeed * BattleStatStageSupport.stageMultiplier(stage)).roundToInt()
    }
}

/**
 * 一次 session 逃跑尝试的解析结果。
 */
internal data class BattleSessionRunResolution(
    val success: Boolean,
    val reason: String,
    val runnerUnitId: String? = null,
    val failedAttempts: Int? = null,
    val escapeValue: Int? = null,
    val roll: Int? = null,
)