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