DefaultCaptureFormulaService.kt
package io.github.lishangbu.avalon.game.battle.engine.core.capture
import io.github.lishangbu.avalon.game.calculator.capture.CaptureRateCalculator
import io.github.lishangbu.avalon.game.calculator.capture.CaptureRateInput
import io.github.lishangbu.avalon.game.calculator.capture.DefaultCaptureRateCalculator
import io.github.lishangbu.avalon.game.calculator.capture.CaptureContext as CalculatorCaptureContext
/**
* battle-engine 捕捉公式服务。
*
* 数值部分统一委托给 `avalon-game-calculator`,battle-engine 只保留实际摇晃判定,
* 这样可以保证:
*
* - 公式与概率口径只有一份实现
* - battle 层仍然可以继续使用自己的随机源推进真实捕捉流程
*/
class DefaultCaptureFormulaService(
private val captureRandomSource: CaptureRandomSource = DefaultCaptureRandomSource(),
private val captureRateCalculator: CaptureRateCalculator = DefaultCaptureRateCalculator(),
) : CaptureFormulaService {
override fun calculate(
input: CaptureFormulaInput,
nextShakeRoll: (() -> Int)?,
): CaptureFormulaResult {
val captureRateResult = captureRateCalculator.calculate(input.toCaptureRateInput())
val shakeRollSupplier = nextShakeRoll ?: { captureRandomSource.nextShakeRoll() }
if (captureRateResult.directSuccess || captureRateResult.guaranteedSuccess) {
return CaptureFormulaResult(
success = true,
shakes = 4,
captureValue = captureRateResult.captureValue,
finalRate = captureRateResult.overallCaptureSuccessRate,
ballRate = captureRateResult.ballMultiplier,
statusRate = captureRateResult.statusMultiplier,
reason = captureRateResult.note,
)
}
if (captureRateResult.captureValue <= 0.0) {
return CaptureFormulaResult(
success = false,
shakes = 0,
captureValue = 0.0,
finalRate = 0.0,
ballRate = captureRateResult.ballMultiplier,
statusRate = captureRateResult.statusMultiplier,
reason = "failed-first-shake",
)
}
val threshold =
requireNotNull(captureRateResult.shakeCheckThreshold) {
"shakeCheckThreshold must be present when capture is not guaranteed."
}
repeat(4) { shakeIndex ->
val roll = shakeRollSupplier().toDouble()
if (roll >= threshold) {
return CaptureFormulaResult(
success = false,
shakes = shakeIndex,
captureValue = captureRateResult.captureValue,
finalRate = captureRateResult.overallCaptureSuccessRate,
ballRate = captureRateResult.ballMultiplier,
statusRate = captureRateResult.statusMultiplier,
reason = failureReason(shakeIndex),
)
}
}
return CaptureFormulaResult(
success = true,
shakes = 4,
captureValue = captureRateResult.captureValue,
finalRate = captureRateResult.overallCaptureSuccessRate,
ballRate = captureRateResult.ballMultiplier,
statusRate = captureRateResult.statusMultiplier,
reason = "all-shakes-passed",
)
}
private fun CaptureFormulaInput.toCaptureRateInput(): CaptureRateInput =
CaptureRateInput(
currentHp = currentHp,
maxHp = maxHp,
captureRate = captureRate,
statusEffectId = statusState?.effectId,
ballItemInternalName = ballItemInternalName,
turn = turn,
captureContext =
CalculatorCaptureContext(
alreadyCaught = battleContext.alreadyCaught,
isFishingEncounter = battleContext.isFishingEncounter,
isSurfEncounter = battleContext.isSurfEncounter,
isNight = battleContext.isNight,
isCave = battleContext.isCave,
isUltraBeast = battleContext.isUltraBeast,
targetLevel = battleContext.targetLevel,
targetWeight = battleContext.targetWeight,
targetTypes = battleContext.targetTypes,
),
)
private fun failureReason(shakeIndex: Int): String =
when (shakeIndex) {
0 -> "failed-first-shake"
1 -> "failed-second-shake"
2 -> "failed-third-shake"
else -> "failed-fourth-shake"
}
}