BattleSessionEffectExecutionCoordinator.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.MoveResolutionResult
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleMoveDataReader

/**
 * session effect 执行协调器。
 *
 * 设计意图:
 * - 承载 move/item 这类“指定 effectId + actor + target”的统一结算编排;
 * - 把目标展开、attributes 补全、逐目标聚合、直接伤害写回和日志事件记录
 *   从 `BattleSessionActionExecutionSupport` 中剥离出来;
 * - 让 action support 退化成一层薄分发器。
 */
class BattleSessionEffectExecutionCoordinator(
    private val directDamageApplier: BattleSessionDirectDamageApplier = BattleSessionDirectDamageApplier(),
) {
    /**
     * 执行一个已经具备目标信息的 effect。
     */
    fun executeResolvedEffect(
        session: BattleSession,
        effectId: String,
        actorUnitId: String,
        targetUnitId: String,
        accuracy: Int?,
        evasion: Int?,
        basePower: Int,
        damage: Int,
        attributes: Map<String, Any?>,
    ): MoveResolutionResult {
        val targetQuery = session.queryTargets(effectId, actorUnitId)
        val effect = session.effectRepository.get(effectId)
        val resolvedAccuracy = accuracy ?: BattleMoveDataReader.readAccuracy(effect.data)
        val resolvedTargetIds =
            when {
                targetQuery.requiresExplicitTarget -> listOf(targetUnitId)
                targetQuery.availableTargetUnitIds.isEmpty() -> listOf(targetUnitId)
                else -> targetQuery.availableTargetUnitIds
            }
        val resolvedAttributes =
            BattleSessionEffectAttributeResolver
                .completeRandomAttributes(session, resolvedAccuracy, attributes)
                .let { candidate ->
                    BattleSessionEffectAttributeResolver.withResolvedTargetCount(candidate, resolvedTargetIds.size)
                }

        var aggregatedResult: MoveResolutionResult? = null
        resolvedTargetIds.forEach { resolvedTargetId ->
            val targetAwareAttributes =
                BattleSessionEffectAttributeResolver.withDerivedTargetRelation(
                    attributes = resolvedAttributes,
                    session = session,
                    actorUnitId = actorUnitId,
                    targetUnitId = resolvedTargetId,
                )
            val result =
                session.battleFlowEngine.resolveMoveAction(
                    snapshot = session.currentSnapshot,
                    moveId = effectId,
                    attackerId = actorUnitId,
                    targetId = resolvedTargetId,
                    accuracy = resolvedAccuracy,
                    evasion = evasion,
                    basePower = basePower,
                    damage = damage,
                    attributes = targetAwareAttributes,
                )
            session.currentSnapshot = result.snapshot
            if (result.hitSuccessful && result.damage > 0) {
                session.currentSnapshot = directDamageApplier.apply(session, actorUnitId, resolvedTargetId, result.damage)
            }
            session.currentSnapshot = session.resolveFaintAndReplacement()
            val finalResult = result.copy(snapshot = session.currentSnapshot)
            session.recordMoveExecution(effectId, actorUnitId, resolvedTargetId, finalResult)
            aggregatedResult = aggregateResult(aggregatedResult, finalResult, session.currentSnapshot)
            if (session.currentSnapshot.battle.lifecycle
                    .isEnded()
            ) {
                return requireNotNull(aggregatedResult)
            }
        }
        return requireNotNull(aggregatedResult) {
            "No targets were resolved for effect '$effectId'."
        }
    }

    /**
     * 聚合同一 effect 多目标结算结果。
     *
     * 当前策略保持与原实现一致:
     * - `hitSuccessful / criticalHit` 取任一目标命中即可;
     * - 其余数值字段保留最后一个目标的结果;
     * - `snapshot` 始终返回最新 session snapshot。
     */
    private fun aggregateResult(
        current: MoveResolutionResult?,
        next: MoveResolutionResult,
        latestSnapshot: io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot,
    ): MoveResolutionResult =
        if (current == null) {
            next
        } else {
            current.copy(
                snapshot = latestSnapshot,
                hitSuccessful = current.hitSuccessful || next.hitSuccessful,
                criticalHit = current.criticalHit || next.criticalHit,
                accuracy = next.accuracy,
                evasion = next.evasion,
                basePower = next.basePower,
                damageRoll = next.damageRoll,
                damage = next.damage,
            )
        }
}