BattleSessionDirectDamageApplier.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleAttributeKeys
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.DamageMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
import io.github.lishangbu.avalon.game.battle.engine.core.type.StandardTargetSelectorIds

/**
 * session 直接伤害写回器。
 *
 * 设计意图:
 * - 承接“move pipeline 已经算出最终伤害数值,但还没自动落盘”的单目标扣血路径;
 * - 在真正写回前手动派发一次 `on_damage`,让 attached effect 有机会 veto
 *   或覆盖最终伤害;
 * - 与通用 mutation interceptor 链分开,避免重复触发 `on_damage` 生命周期。
 */
class BattleSessionDirectDamageApplier {
    /**
     * 将直接伤害应用到当前快照。
     */
    fun apply(
        session: BattleSession,
        sourceId: String,
        targetId: String,
        damage: Int,
    ): BattleRuntimeSnapshot {
        val hookResult =
            session.battleFlowPhaseProcessor.processAttachedEffects(
                snapshot = session.currentSnapshot,
                unitId = targetId,
                hookName = StandardHookNames.ON_DAMAGE.value,
                targetId = targetId,
                sourceId = sourceId,
                relay = damage,
                attributes =
                    buildMap {
                        put("amount", damage)
                        BattleSessionUnitRelationResolver
                            .resolveTargetRelation(session.currentSnapshot, sourceId, targetId)
                            ?.let { relation -> put(BattleAttributeKeys.TARGET_RELATION, relation) }
                    },
            )
        val resolvedDamage =
            when {
                hookResult.cancelled || hookResult.relay == false -> 0
                hookResult.relay is Number -> hookResult.relay.toInt().coerceAtLeast(0)
                else -> damage
            }
        if (resolvedDamage <= 0) {
            return hookResult.snapshot
        }
        val applyResult =
            session.mutationApplier.apply(
                mutations =
                    listOf(
                        DamageMutation(
                            target = StandardTargetSelectorIds.TARGET,
                            mode = null,
                            value = resolvedDamage.toDouble(),
                        ),
                    ),
                context =
                    MutationApplicationContext(
                        battle = hookResult.snapshot.battle,
                        field = hookResult.snapshot.field,
                        units = hookResult.snapshot.units,
                        sides = hookResult.snapshot.sides,
                        selfId = sourceId,
                        targetId = targetId,
                        sourceId = sourceId,
                        side = BattleSessionUnitRelationResolver.sideOfUnit(hookResult.snapshot, sourceId),
                        foeSide = BattleSessionUnitRelationResolver.sideOfUnit(hookResult.snapshot, targetId),
                    ),
            )
        return hookResult.snapshot.copy(
            battle = applyResult.battle,
            field = applyResult.field,
            units = applyResult.units,
            sides = applyResult.sides,
        )
    }
}