BattleMoveSpecialHitRuleResolver.kt

package io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleAttributeKeys
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleMoveDataReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.EventContextAttributeReader

/**
 * 出招前置“特殊命中规则”收口器。
 *
 * 设计意图:
 * - 把不适合拆散到 `on_modify_accuracy / on_modify_evasion` 的规则集中到一处;
 * - 让招式自身必中、天气必中、`lock-on / mind-reader`、`no-guard`、OHKO 公式
 *   在进入最终 accuracy roll 前统一裁决;
 * - 保持 battle flow 对 52Poké 命中判定页中的“特殊必中分支”有显式落点。
 *
 * 当前覆盖:
 * - move data `alwaysHit`
 * - move data `guaranteedHitWeatherEffectIds`
 * - move data `weatherAccuracyOverrides`
 * - `no-guard`
 * - 目标身上由当前攻击方施加的 `lock-on / mind-reader`
 * - move data `ohko` 命中公式,以及 OHKO 独有的阻断条件
 */
class BattleMoveSpecialHitRuleResolver {
    /**
     * 解析当前招式的特殊命中规则结果。
     */
    fun resolve(context: BattleMoveResolutionContext): BattleMoveSpecialHitResolution {
        val moveData = context.moveEffect.data
        val currentWeatherId =
            context.snapshot.field.weatherState
                ?.effectId
        val weatherAccuracyOverride =
            currentWeatherId
                ?.let { weatherId ->
                    BattleMoveDataReader.readWeatherAccuracyOverrides(moveData)[weatherId]
                }

        if (BattleMoveDataReader.readAlwaysHit(context.moveEffect.data) == true) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = true,
                resolvedAccuracy = weatherAccuracyOverride ?: context.accuracy,
            )
        }
        if (currentWeatherId != null && currentWeatherId in BattleMoveDataReader.readGuaranteedHitWeatherEffectIds(moveData)) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = true,
                resolvedAccuracy = weatherAccuracyOverride ?: context.accuracy,
            )
        }
        val attacker = context.snapshot.units[context.attackerId]
        val target = context.snapshot.units[context.targetId]
        if (attacker?.abilityId == NO_GUARD_ABILITY_ID || target?.abilityId == NO_GUARD_ABILITY_ID) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = true,
                resolvedAccuracy = weatherAccuracyOverride ?: context.accuracy,
            )
        }
        val trackedTargetState =
            target
                ?.volatileStates
                ?.values
                ?.firstOrNull { state ->
                    state.sourceId == context.attackerId && state.effectId in TRACKING_VOLATILE_EFFECT_IDS
                }
        if (trackedTargetState != null) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = true,
                resolvedAccuracy = weatherAccuracyOverride ?: context.accuracy,
            )
        }
        if (BattleMoveDataReader.readOneHitKnockOut(moveData) == true) {
            return resolveOneHitKnockOut(
                context = context,
                weatherAccuracyOverride = weatherAccuracyOverride,
                attacker = attacker,
                target = target,
            )
        }
        return BattleMoveSpecialHitResolution(
            resolvedAccuracy = weatherAccuracyOverride,
        )
    }

    private fun resolveOneHitKnockOut(
        context: BattleMoveResolutionContext,
        weatherAccuracyOverride: Int?,
        attacker: UnitState?,
        target: UnitState?,
    ): BattleMoveSpecialHitResolution {
        val attackerLevel = attacker?.metadata?.level ?: DEFAULT_LEVEL
        val targetLevel = target?.metadata?.level ?: DEFAULT_LEVEL
        if (attackerLevel < targetLevel) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = false,
                resolvedAccuracy = 0,
                skipAccuracyEvasionModifiers = true,
            )
        }
        if (target?.abilityId == STURDY_ABILITY_ID && target.currentHp >= target.maxHp) {
            return BattleMoveSpecialHitResolution(
                forcedHitSuccessful = false,
                resolvedAccuracy = 0,
                skipAccuracyEvasionModifiers = true,
            )
        }
        val moveType = BattleMoveDataReader.readType(context.moveEffect.data)
        val attackerIsOffTypeUser =
            moveType != null &&
                attacker != null &&
                moveType !in attacker.typeIds
        val baseAccuracy =
            weatherAccuracyOverride
                ?: if (attackerIsOffTypeUser) {
                    BattleMoveDataReader.readOffTypeOhkoAccuracy(context.moveEffect.data)
                } else {
                    null
                }
                ?: context.accuracy
                ?: DEFAULT_OHKO_BASE_ACCURACY
        val computedAccuracy =
            (baseAccuracy + (attackerLevel - targetLevel))
                .coerceIn(0, 100)
        val accuracyRoll = EventContextAttributeReader.readInt(BattleAttributeKeys.ACCURACY_ROLL, context.attributes)
        val forcedHitSuccessful =
            accuracyRoll
                ?.let { roll -> roll <= computedAccuracy }
        return BattleMoveSpecialHitResolution(
            forcedHitSuccessful = forcedHitSuccessful,
            resolvedAccuracy = computedAccuracy,
            skipAccuracyEvasionModifiers = true,
        )
    }

    private companion object {
        private const val DEFAULT_LEVEL: Int = 50
        private const val DEFAULT_OHKO_BASE_ACCURACY: Int = 30
        private const val NO_GUARD_ABILITY_ID: String = "no-guard"
        private const val STURDY_ABILITY_ID: String = "sturdy"

        private val TRACKING_VOLATILE_EFFECT_IDS: Set<String> =
            setOf(
                "lock-on",
                "mind-reader",
            )
    }
}