CaptureRateModels.kt

package io.github.lishangbu.avalon.game.calculator.capture

/**
 * 捕捉率计算所需的额外上下文。
 *
 * 这些字段全部是纯数值/纯标志输入,调用方可以在 battle 层或 service 层准备好后再传进来,
 * 计算器本身不关心这些值是来自数据库、快照还是前端请求。
 */
data class CaptureContext(
    /** 目标物种是否已经被玩家捕获过,用于 `repeat-ball`。 */
    val alreadyCaught: Boolean = false,
    /** 是否为钓鱼遭遇,用于 `dive-ball`。 */
    val isFishingEncounter: Boolean = false,
    /** 是否为冲浪遭遇,用于 `dive-ball`。 */
    val isSurfEncounter: Boolean = false,
    /** 是否为夜晚环境,用于 `dusk-ball`。 */
    val isNight: Boolean = false,
    /** 是否为洞窟环境,用于 `dusk-ball`。 */
    val isCave: Boolean = false,
    /** 目标是否被视为究极异兽,用于 `beast-ball`。 */
    val isUltraBeast: Boolean = false,
    /** 目标等级,用于 `nest-ball`。 */
    val targetLevel: Int? = null,
    /**
     * 目标体重。
     *
     * 当前按项目内部既有约定直接使用数值,不在计算器里做单位换算。
     * 后续 battle 模块只需要保证传入的单位与球策略一致即可。
     */
    val targetWeight: Int? = null,
    /** 目标属性集合,用于 `net-ball`。 */
    val targetTypes: Set<String> = emptySet(),
)

/**
 * 球策略解析结果。
 *
 * 某些球是倍率修正,某些球是平坦捕获率修正,例如 `heavy-ball`。
 * `master-ball` 这类直接成功的球则通过 [directSuccess] 表达。
 */
data class BallResolution(
    /** 球本身是否绕过后续公式,直接成功。 */
    val directSuccess: Boolean = false,
    /** 球的倍率修正。 */
    val multiplier: Double = 1.0,
    /** 球提供的平坦捕获率修正。 */
    val flatCaptureRateBonus: Int = 0,
    /** 解析备注,方便调用方记录来源。 */
    val note: String? = null,
)

/**
 * 捕捉率计算输入。
 *
 * 公式结构参考常见主系列“四摇”计算思路:
 *
 * `a = effectiveCaptureRate * hpFactor * ballMultiplier * statusMultiplier`
 *
 * 其中:
 *
 * - `effectiveCaptureRate = max(1, captureRate + flatBonus)`
 * - `hpFactor = ((3 * maxHp) - (2 * currentHp)) / (3 * maxHp)`
 */
data class CaptureRateInput(
    /** 当前 HP。允许为 0,便于上层在特殊规则下自行决定是否传入濒死目标。 */
    val currentHp: Int,
    /** 最大 HP。必须大于 0。 */
    val maxHp: Int,
    /** 基础捕获率。 */
    val captureRate: Int,
    /**
     * 状态 effect 标识。
     *
     * 当前只接受规范化 effect id:
     *
     * - 睡眠/冰冻:`slp`、`frz`
     * - 麻痹/灼伤/中毒:`par`、`brn`、`psn`、`tox`
     */
    val statusEffectId: String? = null,
    /** 球的内部名称,例如 `poke-ball`、`great-ball`。 */
    val ballItemInternalName: String,
    /** 当前回合数,从 1 开始。 */
    val turn: Int = 1,
    /** 球策略所需的额外环境上下文。 */
    val captureContext: CaptureContext = CaptureContext(),
)

/**
 * 捕捉率计算输出。
 *
 * 这里同时返回“中间值”和“最终概率”,目的是后续 battle 模块既可以:
 *
 * - 直接展示理论捕捉率
 * - 也可以复用 `shakeCheckThreshold` 做实际随机摇晃判定
 */
data class CaptureRateResult(
    /** 是否因为球本身规则而直接成功,例如 `master-ball`。 */
    val directSuccess: Boolean,
    /** 是否已经达到必定捕获条件。 */
    val guaranteedSuccess: Boolean,
    /** 平坦修正后的有效捕获率。 */
    val effectiveCaptureRate: Int,
    /** 球的倍率修正。 */
    val ballMultiplier: Double,
    /** 球的平坦捕获率修正。 */
    val flatCaptureRateBonus: Int,
    /** 状态修正倍率。 */
    val statusMultiplier: Double,
    /** HP 因子。HP 越低,该值越大。 */
    val hpFactor: Double,
    /** 公式中常见的捕捉值 `a`。 */
    val captureValue: Double,
    /**
     * 按 `a / 255` 归一化后的百分比。
     *
     * 这是对中间值 `a` 的线性归一化,不等于实际四摇成功率;
     * 之所以保留,是为了方便对齐旧公式文档和调试日志。
     */
    val normalizedCaptureValueRate: Double,
    /**
     * 单次摇晃判定阈值。
     *
     * 若结果为 `null`,表示当前输入已经绕过摇晃判定直接成功。
     */
    val shakeCheckThreshold: Double?,
    /**
     * 单次摇晃成功概率,取值范围为 `0.0 .. 1.0`。
     *
     * 当前按离散整数随机源精确计算,而不是简单做连续值除法:
     * battle 中的 shake roll 是 `0..65535` 的整数。
     */
    val singleShakeSuccessProbability: Double,
    /**
     * 四摇全部通过的整体成功概率,取值范围为 `0.0 .. 1.0`。
     */
    val overallCaptureSuccessProbability: Double,
    /**
     * 整体成功率百分比,等于 [overallCaptureSuccessProbability] * 100。
     */
    val overallCaptureSuccessRate: Double,
    /** 结果备注,便于调用方记录判定来源。 */
    val note: String,
)