BattleMoveCriticalHitPhaseStep.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.constant.BattleDamageClassIds
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleMoveDataReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleRelayReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.EventContextAttributeReader

/**
 * 要害判定与倍率应用 phase step。
 *
 * @property phaseProcessor battle hook phase 处理器。
 */
class BattleMoveCriticalHitPhaseStep(
    private val phaseProcessor: BattleFlowPhaseProcessor,
    private val fixedDamageRuleResolver: BattleMoveFixedDamageRuleResolver = BattleMoveFixedDamageRuleResolver(),
) : BattleMoveResolutionStep {
    override val order: Int = 250

    override fun execute(context: BattleMoveResolutionContext) {
        if (!context.hitSuccessful) {
            context.criticalHit = false
            return
        }
        if (fixedDamageRuleResolver.hasFixedDamageRule(context.moveEffect.data)) {
            context.criticalHit = false
            return
        }
        val damageClass = BattleMoveDataReader.readDamageClass(context.moveEffect.data)
        if (damageClass == null || damageClass == BattleDamageClassIds.STATUS || context.basePower <= 0) {
            context.criticalHit = false
            return
        }
        val explicitCritical = EventContextAttributeReader.readBoolean(BattleAttributeKeys.CRITICAL_HIT, context.attributes)
        if (explicitCritical != null) {
            context.criticalHit = explicitCritical
            return
        }

        val baseCritRatio = resolveBaseCritRatio(context)
        val critRatioResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_CRIT_RATIO.value,
                moveEffect = context.moveEffect,
                selfId = context.attackerId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = baseCritRatio,
                attributes =
                    context.attributes +
                        mapOf(
                            BattleAttributeKeys.BASE_CRIT_RATIO to baseCritRatio,
                            BattleAttributeKeys.DAMAGE_CLASS to damageClass,
                        ),
            )
        context.snapshot = critRatioResult.snapshot
        val critRatio = (BattleRelayReader.readInt(critRatioResult.relay) ?: baseCritRatio).coerceIn(0, MAX_CRIT_RATIO)
        context.criticalHit = determineCriticalHit(context, critRatio)
    }

    private fun resolveBaseCritRatio(context: BattleMoveResolutionContext): Int {
        if (EventContextAttributeReader.readBoolean(BattleAttributeKeys.ALWAYS_CRITICAL_HIT, context.attributes) == true) {
            return GUARANTEED_CRIT_RATIO
        }
        val attributeCritRatioBonus = EventContextAttributeReader.readInt(BattleAttributeKeys.CRIT_RATIO, context.attributes)
        val dataCritRatioBonus = BattleMoveDataReader.readCritRatio(context.moveEffect.data)
        val moveAlwaysCritical = BattleMoveDataReader.readAlwaysCriticalHit(context.moveEffect.data) == true
        return when {
            moveAlwaysCritical -> GUARANTEED_CRIT_RATIO
            else -> (DEFAULT_CRIT_RATIO + (attributeCritRatioBonus ?: dataCritRatioBonus ?: 0)).coerceAtLeast(0)
        }
    }

    private fun determineCriticalHit(
        context: BattleMoveResolutionContext,
        critRatio: Int,
    ): Boolean {
        if (critRatio >= GUARANTEED_CRIT_RATIO) {
            return true
        }
        if (critRatio <= 0) {
            return false
        }
        val denominator = CRIT_DENOMINATORS[critRatio] ?: return false
        val explicitRoll = EventContextAttributeReader.readInt(BattleAttributeKeys.CRITICAL_ROLL, context.attributes)
        val roll = explicitRoll ?: nextRandomInt(context, denominator)
        return roll == 0
    }

    private fun nextRandomInt(
        context: BattleMoveResolutionContext,
        bound: Int,
    ): Int {
        val randomResult =
            context.snapshot.battle.randomState
                .nextInt(bound)
        context.snapshot =
            context.snapshot.copy(
                battle =
                    context.snapshot.battle.copy(
                        randomState = randomResult.nextState,
                    ),
            )
        return randomResult.value
    }

    private companion object {
        private const val MAX_CRIT_RATIO: Int = 4
        private const val DEFAULT_CRIT_RATIO: Int = 1
        private const val GUARANTEED_CRIT_RATIO: Int = 4
        private val CRIT_DENOMINATORS: Map<Int, Int> =
            mapOf(
                1 to 24,
                2 to 8,
                3 to 2,
            )
    }
}