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()
}
}
}