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,
)
}
}