JsonEffectDefinitionBattleDataLoader.kt

package io.github.lishangbu.avalon.game.battle.engine.core.loader

import io.github.lishangbu.avalon.game.battle.engine.core.dsl.ActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.ConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.EffectDefinition
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.HookRule
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.AddRelayActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.AddStatusActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.AddVolatileActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ApplyConditionActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.BoostActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.BoostFromRelayActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ChangeTypeActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ClearBoostsActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ClearProbeActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ClearTerrainActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ClearWeatherActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ConsumeItemActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.CopyBoostsActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.DamageActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.FailMoveActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ForceSwitchActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.InvertBoostRelayActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.InvertStoredBoostsActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.ModifyMultiplierActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.PrepareBoostPassActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.RemoveConditionActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.RemoveStatusActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.RemoveVolatileActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.RestorePpActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.SetProbeActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.SetRelayActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.SetTerrainActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.SetWeatherActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.StealBoostsActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.SwapBoostsActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.TriggerEventActionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.AllConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.AnyConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.AttributeEqualsConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.BattleFormatIsConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.BoostCompareConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.ChanceConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HasAbilityConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HasItemConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HasStatusConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HasTypeConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HasVolatileConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.HpRatioConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.MoveHasTagConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.NotConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.StatCompareConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.TargetRelationConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.TerrainIsConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.TurnCompareConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.condition.WeatherIsConditionNode
import io.github.lishangbu.avalon.game.battle.engine.core.type.ActionTypeId
import io.github.lishangbu.avalon.game.battle.engine.core.type.ActorId
import io.github.lishangbu.avalon.game.battle.engine.core.type.ConditionTypeId
import io.github.lishangbu.avalon.game.battle.engine.core.type.EffectKindId
import io.github.lishangbu.avalon.game.battle.engine.core.type.HookName
import io.github.lishangbu.avalon.game.battle.engine.core.type.SpecialHandlerId
import io.github.lishangbu.avalon.game.battle.engine.core.type.TargetSelectorId
import tools.jackson.databind.JsonNode
import tools.jackson.databind.ObjectMapper

/**
 * 从 JSON 资源加载 [EffectDefinition] 的基础实现。
 *
 * 设计意图:
 * - 为第一版测试与样例数据提供真实文件加载入口。
 * - 先支持当前已落地测试需要的 DSL 子集,再逐步扩展。
 *
 * 当前支持的条件类型:
 * - `all`
 * - `any`
 * - `not`
 * - `chance`
 * - `hp_ratio`
 * - `has_status`
 * - `has_volatile`
 * - `has_type`
 * - `has_item`
 * - `has_ability`
 * - `weather_is`
 * - `terrain_is`
 * - `boost_compare`
 * - `stat_compare`
 * - `move_has_tag`
 * - `target_relation`
 * - `turn_compare`
 * - `battle_format_is`
 *
 * 当前支持的动作类型:
 * - `add_status`
 * - `remove_status`
 * - `add_volatile`
 * - `remove_volatile`
 * - `damage`
 * - `heal`
 * - `boost`
 * - `clear_boosts`
 * - `set_weather`
 * - `clear_weather`
 * - `set_terrain`
 * - `clear_terrain`
 * - `consume_item`
 * - `restore_pp`
 * - `change_type`
 * - `force_switch`
 * - `fail_move`
 * - `trigger_event`
 * - `apply_condition`
 * - `remove_condition`
 * - `modify_multiplier`
 * - `add_relay`
 * - `set_relay`
 * - `invert_boost_relay`
 * - `boost_from_relay`
 * - `copy_boosts`
 * - `swap_boosts`
 * - `invert_stored_boosts`
 * - `steal_boosts`
 * - `prepare_boost_pass`
 * - `set_probe`
 * - `clear_probe`
 *
 * @property resourcePaths 要加载的 classpath 资源路径列表。
 * @property objectMapper JSON 解析器。
 */
class JsonEffectDefinitionBattleDataLoader(
    private val resourcePaths: List<String>,
    private val objectMapper: ObjectMapper = ObjectMapper(),
) : BattleDataLoader {
    override fun loadEffects(): List<EffectDefinition> = resourcePaths.map(::loadEffect)

    private fun loadEffect(resourcePath: String): EffectDefinition {
        val stream =
            requireNotNull(javaClass.classLoader.getResourceAsStream(resourcePath)) {
                "Resource '$resourcePath' was not found."
            }
        val rootNode = stream.use(objectMapper::readTree)
        return parseEffect(rootNode)
    }

    private fun parseEffect(rootNode: JsonNode): EffectDefinition =
        EffectDefinition(
            id = rootNode.requiredText("id"),
            kind = EffectKindId(rootNode.requiredText("kind")),
            name = rootNode.requiredText("name"),
            tags = rootNode.readStringSet("tags"),
            data = rootNode.readObjectMap("data"),
            hooks =
                rootNode.path("hooks").properties().asSequence().associate { entry ->
                    HookName(entry.key) to entry.value.mapArray(::parseHookRule)
                },
            specialHandler = rootNode.optionalText("specialHandler")?.let(::SpecialHandlerId),
        )

    private fun parseHookRule(node: JsonNode): HookRule =
        HookRule(
            priority = node.path("priority").asInt(0),
            subOrder = node.path("subOrder").asInt(0),
            condition = node.get("if")?.let(::parseCondition),
            thenActions = node.path("then").mapArray(::parseAction),
            elseActions = node.path("else").mapArray(::parseAction),
            tags = node.readStringSet("tags"),
        )

    private fun parseCondition(node: JsonNode): ConditionNode {
        val typeId = ConditionTypeId(node.requiredText("type"))
        return when (typeId.value) {
            "all" -> {
                AllConditionNode(
                    conditions = node.path("conditions").mapArray(::parseCondition),
                )
            }

            "any" -> {
                AnyConditionNode(
                    conditions = node.path("conditions").mapArray(::parseCondition),
                )
            }

            "chance" -> {
                ChanceConditionNode(
                    value = node.path("value").asInt(),
                )
            }

            "not" -> {
                NotConditionNode(
                    condition = parseCondition(node.required("condition")),
                )
            }

            "hp_ratio" -> {
                HpRatioConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    operator = node.requiredText("operator"),
                    value = node.path("value").asDouble(),
                )
            }

            "has_status" -> {
                HasStatusConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.optionalText("value"),
                )
            }

            "has_volatile" -> {
                HasVolatileConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.requiredText("value"),
                )
            }

            "has_type" -> {
                HasTypeConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.requiredText("value"),
                )
            }

            "has_item" -> {
                HasItemConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.requiredText("value"),
                )
            }

            "has_ability" -> {
                HasAbilityConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.requiredText("value"),
                )
            }

            "weather_is" -> {
                WeatherIsConditionNode(
                    value = node.requiredText("value"),
                )
            }

            "terrain_is" -> {
                TerrainIsConditionNode(
                    value = node.requiredText("value"),
                )
            }

            "boost_compare" -> {
                BoostCompareConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    stat = node.requiredText("stat"),
                    operator = node.requiredText("operator"),
                    value = node.path("value").asInt(),
                )
            }

            "stat_compare" -> {
                StatCompareConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    stat = node.requiredText("stat"),
                    operator = node.requiredText("operator"),
                    value = node.path("value").asInt(),
                )
            }

            "move_has_tag" -> {
                MoveHasTagConditionNode(
                    actor = ActorId(node.requiredText("actor")),
                    value = node.requiredText("value"),
                )
            }

            "target_relation" -> {
                TargetRelationConditionNode(
                    value = node.requiredText("value"),
                )
            }

            "attribute_equals" -> {
                AttributeEqualsConditionNode(
                    key = node.requiredText("key"),
                    value = node.requiredText("value"),
                )
            }

            "turn_compare" -> {
                TurnCompareConditionNode(
                    operator = node.requiredText("operator"),
                    value = node.path("value").asInt(),
                )
            }

            "battle_format_is" -> {
                BattleFormatIsConditionNode(
                    value = node.requiredText("value"),
                )
            }

            else -> {
                error("Unsupported condition type '${typeId.value}'.")
            }
        }
    }

    private fun parseAction(node: JsonNode): ActionNode {
        val typeId = ActionTypeId(node.requiredText("type"))
        return when (typeId.value) {
            "add_status" -> {
                AddStatusActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    value = node.requiredText("value"),
                    duration = node.optionalInt("duration"),
                )
            }

            "remove_status" -> {
                RemoveStatusActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                )
            }

            "add_volatile" -> {
                AddVolatileActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    value = node.requiredText("value"),
                    duration = node.optionalInt("duration"),
                )
            }

            "remove_volatile" -> {
                RemoveVolatileActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    value = node.requiredText("value"),
                )
            }

            "damage" -> {
                DamageActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    mode = node.optionalText("mode"),
                    value = node.path("value").asDouble(),
                )
            }

            "heal" -> {
                io.github.lishangbu.avalon.game.battle.engine.core.dsl.action.HealActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    mode = node.optionalText("mode"),
                    value = node.path("value").asDouble(),
                )
            }

            "boost" -> {
                BoostActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    stats =
                        node.required("stats").properties().asSequence().associate { entry ->
                            entry.key to entry.value.asInt()
                        },
                )
            }

            "clear_boosts" -> {
                ClearBoostsActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                )
            }

            "set_weather" -> {
                SetWeatherActionNode(
                    value = node.requiredText("value"),
                    duration = node.optionalInt("duration"),
                )
            }

            "clear_weather" -> {
                ClearWeatherActionNode()
            }

            "set_terrain" -> {
                SetTerrainActionNode(
                    value = node.requiredText("value"),
                    duration = node.optionalInt("duration"),
                )
            }

            "clear_terrain" -> {
                ClearTerrainActionNode()
            }

            "consume_item" -> {
                ConsumeItemActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                )
            }

            "restore_pp" -> {
                RestorePpActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    moveId = node.optionalText("moveId"),
                    value = node.path("value").asInt(),
                )
            }

            "change_type" -> {
                ChangeTypeActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    values = node.path("values").mapArray { child -> child.asString() },
                )
            }

            "force_switch" -> {
                ForceSwitchActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                )
            }

            "fail_move" -> {
                FailMoveActionNode()
            }

            "trigger_event" -> {
                TriggerEventActionNode(
                    hookName = HookName(node.requiredText("hookName")),
                )
            }

            "apply_condition" -> {
                ApplyConditionActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    value = node.requiredText("value"),
                    duration = node.optionalInt("duration"),
                )
            }

            "remove_condition" -> {
                RemoveConditionActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    value = node.requiredText("value"),
                )
            }

            "modify_multiplier" -> {
                ModifyMultiplierActionNode(
                    value = node.path("value").asDouble(),
                    rounding = node.optionalText("rounding"),
                )
            }

            "add_relay" -> {
                AddRelayActionNode(
                    value = node.path("value").asDouble(),
                )
            }

            "set_relay" -> {
                SetRelayActionNode(
                    value = readLiteral(node.required("value")),
                )
            }

            "invert_boost_relay" -> {
                InvertBoostRelayActionNode
            }

            "boost_from_relay" -> {
                BoostFromRelayActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    selection = node.optionalText("selection") ?: "all",
                )
            }

            "copy_boosts" -> {
                CopyBoostsActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    from = TargetSelectorId(node.requiredText("from")),
                    selection = node.optionalText("selection") ?: "all",
                    stats = node.readStringSet("stats"),
                )
            }

            "swap_boosts" -> {
                SwapBoostsActionNode(
                    left = TargetSelectorId(node.requiredText("left")),
                    right = TargetSelectorId(node.requiredText("right")),
                    stats = node.readStringSet("stats"),
                )
            }

            "invert_stored_boosts" -> {
                InvertStoredBoostsActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    stats = node.readStringSet("stats"),
                )
            }

            "steal_boosts" -> {
                StealBoostsActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    from = TargetSelectorId(node.requiredText("from")),
                    selection = node.optionalText("selection") ?: "positive",
                    stats = node.readStringSet("stats"),
                )
            }

            "prepare_boost_pass" -> {
                PrepareBoostPassActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                )
            }

            "set_probe" -> {
                SetProbeActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    key = node.requiredText("key"),
                    value = node.requiredText("value"),
                )
            }

            "clear_probe" -> {
                ClearProbeActionNode(
                    target = TargetSelectorId(node.requiredText("target")),
                    key = node.requiredText("key"),
                )
            }

            else -> {
                error("Unsupported action type '${typeId.value}'.")
            }
        }
    }

    private fun JsonNode.required(name: String): JsonNode =
        requireNotNull(get(name)) {
            "Field '$name' is required."
        }

    private fun JsonNode.requiredText(name: String): String = required(name).asString()

    private fun JsonNode.optionalText(name: String): String? = get(name)?.asString()

    private fun JsonNode.optionalInt(name: String): Int? = get(name)?.takeIf(JsonNode::isInt)?.asInt()

    private fun JsonNode.readStringSet(name: String): Set<String> =
        path(name)
            .mapArray { it.asString() }
            .toSet()

    private fun JsonNode.readObjectMap(name: String): Map<String, Any?> =
        path(name)
            .properties()
            .asSequence()
            .associate { entry -> entry.key to readLiteral(entry.value) }

    private fun readLiteral(node: JsonNode): Any? =
        when {
            node.isObject -> {
                node
                    .properties()
                    .asSequence()
                    .associate { entry -> entry.key to readLiteral(entry.value) }
            }

            node.isArray -> {
                node.mapArray(::readLiteral)
            }

            else -> {
                readScalar(node)
            }
        }

    private fun readScalar(node: JsonNode): Any? =
        when {
            node.isNull -> null
            node.isInt -> node.asInt()
            node.isLong -> node.asLong()
            node.isDouble || node.isFloat || node.isBigDecimal -> node.asDouble()
            node.isBoolean -> node.asBoolean()
            else -> node.asString()
        }

    private fun <T> JsonNode.mapArray(transform: (JsonNode) -> T): List<T> = if (isArray) asSequence().map(transform).toList() else emptyList()
}