BattleMoveFixedDamageRuleResolver.kt

package io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow

import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import kotlin.math.floor

/**
 * 固定伤害类招式原生规则解析器。
 *
 * 设计意图:
 * - 把“固定数值 / 按攻击方等级 / 按目标当前生命比例”这类不走 A/D 公式的规则,
 *   从主伤害 phase step 中拆成独立组件,避免 `BattleMovePowerDamagePhaseStep` 继续膨胀。
 * - 让 move fixture 可以直接声明固定伤害规则,而不是要求调用方始终预先把 `damage` 算好。
 *
 * 当前支持的静态元数据:
 * - `fixedDamage`: 直接固定伤害值,例如 20、40。
 * - `fixedDamageMode=attacker_level`: 以攻击方等级作为固定伤害。
 * - `fixedDamageMode=target_current_hp_ratio` + `fixedDamageValue`: 以目标当前 HP 比例结算伤害。
 *
 * 约定:
 * - 这里返回的是“固定伤害本体”;属性免疫仍由主伤害 phase 统一裁决。
 * - 当前生命比例模式会对仍存活的目标至少造成 1 点伤害,避免出现命中后 0 伤害的空结算。
 */
class BattleMoveFixedDamageRuleResolver {
    /**
     * 判断当前招式是否声明了固定伤害规则。
     *
     * 该判定会同时被要害 phase 与伤害 phase 使用,
     * 目的是让固定伤害在更早的阶段就跳过要害判定等不应参与的分支。
     */
    fun hasFixedDamageRule(moveData: Map<String, Any?>): Boolean = moveData.containsKey(FIXED_DAMAGE_KEY) || moveData.containsKey(FIXED_DAMAGE_MODE_KEY)

    /**
     * 解析当前招式的固定伤害结果。
     *
     * @return 若当前招式不是固定伤害类,返回 `null`;否则返回已经原生计算好的固定伤害值。
     */
    fun resolve(
        moveData: Map<String, Any?>,
        attacker: UnitState?,
        target: UnitState?,
    ): Int? {
        parseInt(moveData[FIXED_DAMAGE_KEY])
            ?.let { fixedDamage ->
                return fixedDamage.coerceAtLeast(0)
            }

        return when (moveData[FIXED_DAMAGE_MODE_KEY]?.toString()) {
            ATTACKER_LEVEL_MODE -> resolveAttackerLevelDamage(attacker)
            TARGET_CURRENT_HP_RATIO_MODE -> resolveTargetCurrentHpRatioDamage(moveData, target)
            else -> null
        }
    }

    private fun resolveAttackerLevelDamage(attacker: UnitState?): Int? {
        val level = attacker?.metadata?.level ?: DEFAULT_LEVEL
        return level.coerceAtLeast(1)
    }

    private fun resolveTargetCurrentHpRatioDamage(
        moveData: Map<String, Any?>,
        target: UnitState?,
    ): Int? {
        val ratio = parseDouble(moveData[FIXED_DAMAGE_VALUE_KEY]) ?: return null
        val currentHp = target?.currentHp ?: return null
        if (currentHp <= 0) {
            return 0
        }
        return floor((currentHp * ratio).coerceAtLeast(1.0)).toInt()
    }

    private fun parseInt(value: Any?): Int? =
        when (value) {
            is Int -> value
            is Number -> value.toInt()
            else -> value?.toString()?.toIntOrNull()
        }

    private fun parseDouble(value: Any?): Double? =
        when (value) {
            is Double -> value
            is Number -> value.toDouble()
            else -> value?.toString()?.toDoubleOrNull()
        }

    private companion object {
        private const val DEFAULT_LEVEL: Int = 50
        private const val FIXED_DAMAGE_KEY: String = "fixedDamage"
        private const val FIXED_DAMAGE_MODE_KEY: String = "fixedDamageMode"
        private const val FIXED_DAMAGE_VALUE_KEY: String = "fixedDamageValue"
        private const val ATTACKER_LEVEL_MODE: String = "attacker_level"
        private const val TARGET_CURRENT_HP_RATIO_MODE: String = "target_current_hp_ratio"
    }
}