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