BattleChoiceResolver.kt

package io.github.lishangbu.avalon.game.battle.engine.application

import io.github.lishangbu.avalon.game.battle.engine.core.dsl.EffectDefinition
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.session.BattleSessionQuery
import io.github.lishangbu.avalon.game.battle.engine.core.session.ItemChoice
import io.github.lishangbu.avalon.game.battle.engine.core.session.MoveChoice
import io.github.lishangbu.avalon.game.battle.engine.core.session.target.BattleSessionTargetQuery
import io.github.lishangbu.avalon.game.battle.engine.core.session.target.BattleSessionTargetQueryService
import io.github.lishangbu.avalon.game.battle.engine.spi.effect.EffectDefinitionRepository
import kotlin.math.roundToInt

/**
 * battle-engine 应用层的智能 choice 解析器。
 *
 * 设计意图:
 * - 把“基于当前快照自动补齐目标、速度、优先级、命中参数”这组能力收回引擎;
 * - 让 game/controller 不再自己拼 `MoveChoice`、`ItemChoice` 的细节;
 * - 使 choice 的默认规则与目标解析直接复用引擎的 effect repository 和 target query 服务。
 */
class BattleChoiceResolver(
    private val effectDefinitionRepository: EffectDefinitionRepository,
    private val targetQueryService: BattleSessionTargetQueryService,
) {
    fun queryTargets(
        session: BattleSessionQuery,
        effectId: String,
        actorUnitId: String,
    ): BattleSessionTargetQuery =
        targetQueryService.resolve(
            snapshot = session.snapshot,
            effectId = effectId,
            actorUnitId = actorUnitId,
        )

    fun createMoveChoice(
        session: BattleSessionQuery,
        command: SubmitMoveChoiceCommand,
    ): MoveChoice {
        val effect = effectDefinitionRepository.get(command.moveId)
        val attacker = requireUnit(session, command.attackerId)
        val targetQuery = queryTargets(session, command.moveId, command.attackerId)
        val targetId = resolveTargetId(command.targetId, targetQuery, command.attackerId)
        val target = requireUnit(session, targetId)
        val basePower = command.basePower ?: effect.intData("basePower") ?: 0
        val moveAccuracy = command.accuracy ?: defaultAccuracy(effect, attacker)
        val targetEvasion = command.evasion ?: defaultEvasion(target)
        val priority = command.priority ?: effect.intData("priority") ?: 0
        val speed = command.speed ?: effectiveStat(attacker, "speed")
        val damage = command.damage ?: 0

        return MoveChoice(
            moveId = command.moveId,
            attackerId = command.attackerId,
            targetId = targetId,
            priority = priority,
            speed = speed,
            accuracy = moveAccuracy,
            evasion = targetEvasion,
            basePower = basePower,
            damage = damage,
            attributes =
                command.attributes +
                    mapOfNotNull(
                        "accuracyRoll" to command.accuracyRoll,
                        "chanceRoll" to command.chanceRoll,
                        "criticalRoll" to command.criticalRoll,
                        "damageRoll" to command.damageRoll,
                        "criticalHit" to command.criticalHit,
                        "computeDamage" to (command.damage == null),
                    ),
        )
    }

    fun createItemChoice(
        session: BattleSessionQuery,
        command: SubmitItemChoiceCommand,
    ): ItemChoice {
        val actor = requireUnit(session, command.actorUnitId)
        val targetQuery = queryTargets(session, command.itemId, command.actorUnitId)
        val targetId = resolveTargetId(command.targetId, targetQuery, command.actorUnitId)
        val effect = effectDefinitionRepository.get(command.itemId)
        return ItemChoice(
            itemId = command.itemId,
            actorUnitId = command.actorUnitId,
            targetId = targetId,
            priority = command.priority ?: effect.intData("priority") ?: 0,
            speed = command.speed ?: effectiveStat(actor, "speed"),
            attributes = command.attributes + mapOfNotNull("chanceRoll" to command.chanceRoll),
        )
    }

    private fun resolveTargetId(
        requestedTargetId: String?,
        targetQuery: BattleSessionTargetQuery,
        actorUnitId: String,
    ): String {
        if (requestedTargetId != null) {
            return requestedTargetId
        }
        if (!targetQuery.requiresExplicitTarget) {
            return targetQuery.availableTargetUnitIds.firstOrNull() ?: actorUnitId
        }
        if (targetQuery.availableTargetUnitIds.size == 1) {
            return targetQuery.availableTargetUnitIds.single()
        }
        error("Effect '${targetQuery.effectId}' requires an explicit target for actor '${targetQuery.actorUnitId}'.")
    }

    private fun defaultAccuracy(
        effect: EffectDefinition,
        attacker: UnitState,
    ): Int? {
        val baseAccuracy = effect.intData("accuracy") ?: return null
        return (baseAccuracy * stageMultiplier(attacker.boosts["accuracy"] ?: 0)).roundToInt()
    }

    private fun defaultEvasion(target: UnitState): Int = (100.0 * stageMultiplier(target.boosts["evasion"] ?: 0)).roundToInt()

    private fun effectiveStat(
        unit: UnitState,
        statName: String,
    ): Int {
        val baseStat = unit.stats[statName] ?: return 0
        val stage = unit.boosts[statName] ?: 0
        return (baseStat * stageMultiplier(stage)).roundToInt()
    }

    private fun stageMultiplier(stage: Int): Double {
        val normalized = stage.coerceIn(-6, 6)
        return if (normalized >= 0) {
            (2.0 + normalized) / 2.0
        } else {
            2.0 / (2.0 - normalized)
        }
    }

    private fun requireUnit(
        session: BattleSessionQuery,
        unitId: String,
    ): UnitState =
        requireNotNull(session.snapshot.units[unitId]) {
            "Unit '$unitId' was not found in battle snapshot."
        }

    private fun EffectDefinition.intData(key: String): Int? =
        when (val value = data[key]) {
            is Int -> value
            is Long -> value.toInt()
            is Double -> value.roundToInt()
            is Float -> value.roundToInt()
            is Number -> value.toInt()
            is String -> value.toIntOrNull()
            else -> null
        }

    private fun mapOfNotNull(vararg entries: Pair<String, Any?>): Map<String, Any> = entries.mapNotNull { (key, value) -> value?.let { key to it } }.toMap()
}