BattleMoveDataReader.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleMoveDataKeys

/**
 * battle move `effect.data` 字段的轻量 typed reader。
 *
 * 设计意图:
 * - 收口 battle 主流程反复读取的
 *   `accuracy / type / damageClass / target / critRatio / alwaysCriticalHit / alwaysHit`
 *   以及少量特殊命中规则字段;
 * - 避免 `moveEffect.data[...]?.toString()`、`as? Number`、`== true` 在多个 phase step 中散落;
 * - 让 JSON/CSV 装配边界保留宽松输入,而执行链只通过统一 reader 取值。
 *
 * 说明:
 * - 这里只覆盖 battle move 主链当前高频字段,不扩展到所有 effect data;
 * - 对数值与布尔值保持与 `EventContextAttributeReader` 一致的宽松解析语义。
 */
object BattleMoveDataReader {
    fun readAccuracy(moveData: Map<String, Any?>): Int? = readInt(BattleMoveDataKeys.ACCURACY, moveData)

    fun readType(moveData: Map<String, Any?>): String? = readString(BattleMoveDataKeys.TYPE, moveData)

    fun readDamageClass(moveData: Map<String, Any?>): String? = readString(BattleMoveDataKeys.DAMAGE_CLASS, moveData)

    fun readTarget(moveData: Map<String, Any?>): String? = readString(BattleMoveDataKeys.TARGET, moveData)

    fun readCritRatio(moveData: Map<String, Any?>): Int? = readInt(BattleMoveDataKeys.CRIT_RATIO, moveData)

    fun readAlwaysCriticalHit(moveData: Map<String, Any?>): Boolean? = readBoolean(BattleMoveDataKeys.ALWAYS_CRITICAL_HIT, moveData)

    fun readAlwaysHit(moveData: Map<String, Any?>): Boolean? = readBoolean(BattleMoveDataKeys.ALWAYS_HIT, moveData)

    fun readGuaranteedHitWeatherEffectIds(moveData: Map<String, Any?>): Set<String> = readStringList(BattleMoveDataKeys.GUARANTEED_HIT_WEATHER_EFFECT_IDS, moveData).toSet()

    fun readWeatherAccuracyOverrides(moveData: Map<String, Any?>): Map<String, Int> = readIntMap(BattleMoveDataKeys.WEATHER_ACCURACY_OVERRIDES, moveData)

    fun readOneHitKnockOut(moveData: Map<String, Any?>): Boolean? = readBoolean(BattleMoveDataKeys.OHKO, moveData)

    fun readOffTypeOhkoAccuracy(moveData: Map<String, Any?>): Int? = readInt(BattleMoveDataKeys.OHKO_OFF_TYPE_ACCURACY, moveData)

    fun readPositiveStageBasePowerBase(moveData: Map<String, Any?>): Int? = readInt(BattleMoveDataKeys.BASE_POWER_POSITIVE_STAGE_BASE, moveData)

    fun readBasePowerPerPositiveStage(moveData: Map<String, Any?>): Int? = readInt(BattleMoveDataKeys.BASE_POWER_PER_POSITIVE_STAGE, moveData)

    fun readIgnoreTargetDefensiveBoosts(moveData: Map<String, Any?>): Boolean? = readBoolean(BattleMoveDataKeys.IGNORE_TARGET_DEFENSIVE_BOOSTS, moveData)

    fun readString(
        key: String,
        moveData: Map<String, Any?>,
    ): String? = moveData[key]?.toString()

    fun readInt(
        key: String,
        moveData: Map<String, Any?>,
    ): Int? =
        when (val value = moveData[key]) {
            null -> null
            is Int -> value
            is Number -> value.toInt()
            is String -> value.toIntOrNull()
            else -> null
        }

    fun readBoolean(
        key: String,
        moveData: Map<String, Any?>,
    ): Boolean? =
        when (val value = moveData[key]) {
            null -> {
                null
            }

            is Boolean -> {
                value
            }

            is String -> {
                when {
                    value.equals("true", ignoreCase = true) -> true
                    value.equals("false", ignoreCase = true) -> false
                    else -> null
                }
            }

            is Number -> {
                value.toInt() != 0
            }

            else -> {
                null
            }
        }

    fun readStringList(
        key: String,
        moveData: Map<String, Any?>,
    ): List<String> =
        when (val value = moveData[key]) {
            is Iterable<*> -> value.mapNotNull { entry -> entry?.toString() }
            is Array<*> -> value.mapNotNull { entry -> entry?.toString() }
            else -> emptyList()
        }

    fun readIntMap(
        key: String,
        moveData: Map<String, Any?>,
    ): Map<String, Int> =
        when (val value = moveData[key]) {
            is Map<*, *> -> {
                value.entries
                    .mapNotNull { (mapKey, mapValue) ->
                        val resolvedKey = mapKey?.toString() ?: return@mapNotNull null
                        val resolvedValue =
                            when (mapValue) {
                                is Int -> mapValue
                                is Number -> mapValue.toInt()
                                is String -> mapValue.toIntOrNull()
                                else -> null
                            } ?: return@mapNotNull null
                        resolvedKey to resolvedValue
                    }.toMap()
            }

            else -> {
                emptyMap()
            }
        }
}