DefaultCaptureRateCalculator.kt
package io.github.lishangbu.avalon.game.calculator.capture
import org.springframework.stereotype.Service
import kotlin.math.ceil
import kotlin.math.pow
import kotlin.math.sqrt
/**
* 默认捕捉率计算器。
*
* 公式说明:
*
* 1. 先解析球修正
* 2. 计算状态倍率
* 3. 计算有效捕获率 `max(1, captureRate + flatBonus)`
* 4. 计算 HP 因子 `((3 * maxHp) - (2 * currentHp)) / (3 * maxHp)`
* 5. 计算捕捉值 `a = effectiveCaptureRate * hpFactor * ballMultiplier * statusMultiplier`
* 6. 若 `a >= 255` 则必定成功
* 7. 否则计算四摇阈值 `b = 1048560 / sqrt(sqrt(16711680 / a))`
* 8. 将单摇概率提升为四摇全过的整体成功概率
*
* 注意:
*
* - 这里返回的是“理论成功概率”
* - battle 层若需要实际摇晃结果,可以在后续用 [CaptureRateResult.shakeCheckThreshold] 配合随机源再做判定
*/
@Service
class DefaultCaptureRateCalculator(
private val captureBallPolicy: CaptureBallPolicy = DefaultCaptureBallPolicy(),
) : CaptureRateCalculator {
override fun calculate(input: CaptureRateInput): CaptureRateResult {
validateInput(input)
val ballResolution = captureBallPolicy.resolve(input)
val statusMultiplier = resolveStatusMultiplier(input.statusEffectId)
val effectiveCaptureRate = (input.captureRate + ballResolution.flatCaptureRateBonus).coerceAtLeast(MIN_CAPTURE_RATE)
val hpFactor = ((3.0 * input.maxHp) - (2.0 * input.currentHp)) / (3.0 * input.maxHp)
if (ballResolution.directSuccess) {
return CaptureRateResult(
directSuccess = true,
guaranteedSuccess = true,
effectiveCaptureRate = effectiveCaptureRate,
ballMultiplier = ballResolution.multiplier,
flatCaptureRateBonus = ballResolution.flatCaptureRateBonus,
statusMultiplier = statusMultiplier,
hpFactor = hpFactor,
captureValue = MAX_CAPTURE_VALUE,
normalizedCaptureValueRate = 100.0,
shakeCheckThreshold = null,
singleShakeSuccessProbability = 1.0,
overallCaptureSuccessProbability = 1.0,
overallCaptureSuccessRate = 100.0,
note = ballResolution.note ?: "direct-success-ball",
)
}
val captureValue = effectiveCaptureRate * hpFactor * ballResolution.multiplier * statusMultiplier
val normalizedCaptureValueRate = (captureValue.coerceIn(0.0, MAX_CAPTURE_VALUE) / MAX_CAPTURE_VALUE) * 100.0
if (captureValue >= MAX_CAPTURE_VALUE) {
return CaptureRateResult(
directSuccess = false,
guaranteedSuccess = true,
effectiveCaptureRate = effectiveCaptureRate,
ballMultiplier = ballResolution.multiplier,
flatCaptureRateBonus = ballResolution.flatCaptureRateBonus,
statusMultiplier = statusMultiplier,
hpFactor = hpFactor,
captureValue = captureValue,
normalizedCaptureValueRate = 100.0,
shakeCheckThreshold = null,
singleShakeSuccessProbability = 1.0,
overallCaptureSuccessProbability = 1.0,
overallCaptureSuccessRate = 100.0,
note = "auto-success-threshold",
)
}
if (captureValue <= 0.0) {
return CaptureRateResult(
directSuccess = false,
guaranteedSuccess = false,
effectiveCaptureRate = effectiveCaptureRate,
ballMultiplier = ballResolution.multiplier,
flatCaptureRateBonus = ballResolution.flatCaptureRateBonus,
statusMultiplier = statusMultiplier,
hpFactor = hpFactor,
captureValue = 0.0,
normalizedCaptureValueRate = 0.0,
shakeCheckThreshold = 0.0,
singleShakeSuccessProbability = 0.0,
overallCaptureSuccessProbability = 0.0,
overallCaptureSuccessRate = 0.0,
note = "non-positive-capture-value",
)
}
val shakeCheckThreshold = SHAKE_NUMERATOR / sqrt(sqrt(SHAKE_DENOMINATOR / captureValue))
val singleShakeSuccessProbability = calculateSingleShakeSuccessProbability(shakeCheckThreshold)
val overallCaptureSuccessProbability = singleShakeSuccessProbability.pow(SHAKE_COUNT)
return CaptureRateResult(
directSuccess = false,
guaranteedSuccess = false,
effectiveCaptureRate = effectiveCaptureRate,
ballMultiplier = ballResolution.multiplier,
flatCaptureRateBonus = ballResolution.flatCaptureRateBonus,
statusMultiplier = statusMultiplier,
hpFactor = hpFactor,
captureValue = captureValue,
normalizedCaptureValueRate = normalizedCaptureValueRate,
shakeCheckThreshold = shakeCheckThreshold,
singleShakeSuccessProbability = singleShakeSuccessProbability,
overallCaptureSuccessProbability = overallCaptureSuccessProbability,
overallCaptureSuccessRate = overallCaptureSuccessProbability * 100.0,
note = "standard-four-shake",
)
}
private fun validateInput(input: CaptureRateInput) {
require(input.maxHp > 0) { "maxHp must be greater than 0." }
require(input.currentHp in 0..input.maxHp) { "currentHp must be between 0 and maxHp." }
require(input.captureRate >= 0) { "captureRate must be greater than or equal to 0." }
require(input.turn > 0) { "turn must be greater than 0." }
}
/**
* 状态倍率。
*
* 与常见主系列规则一致:
*
* - 睡眠、冰冻:2.0
* - 麻痹、灼伤、中毒:1.5
* - 其他或无状态:1.0
*/
private fun resolveStatusMultiplier(statusEffectId: String?): Double =
when (statusEffectId?.trim()?.lowercase()) {
"slp", "frz" -> 2.0
"par", "brn", "psn", "tox" -> 1.5
else -> 1.0
}
/**
* 根据离散整数随机空间精确计算“单次摇晃成功概率”。
*
* battle 中 shake roll 的取值空间是 `0..65535`,共 65536 个整数。
* 判定条件为:`roll < threshold`。
*
* 因此,成功样本数不是简单的 `threshold`,而是:
*
* - `threshold <= 0` 时为 0
* - `threshold >= 65536` 时为 65536
* - 否则为 `ceil(threshold)`
*/
private fun calculateSingleShakeSuccessProbability(threshold: Double): Double {
if (threshold <= 0.0) {
return 0.0
}
if (threshold >= SHAKE_ROLL_SPACE) {
return 1.0
}
val successfulRollCount = ceil(threshold).toInt().coerceIn(0, SHAKE_ROLL_SPACE)
return successfulRollCount.toDouble() / SHAKE_ROLL_SPACE
}
private companion object {
private const val MIN_CAPTURE_RATE = 1
private const val MAX_CAPTURE_VALUE = 255.0
private const val SHAKE_ROLL_SPACE = 65536
private const val SHAKE_COUNT = 4.0
private const val SHAKE_NUMERATOR = 1048560.0
private const val SHAKE_DENOMINATOR = 16711680.0
}
}