DamageBattleMutationInterceptor.kt

package io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow

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.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.DamageMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.MutationTargetSelectorResolver

/**
 * 伤害 mutation 的生命周期拦截器。
 *
 * 设计意图:
 * - 把 `DamageMutation` 真正接到 `on_damage` 生命周期上;
 * - 让目标身上的 ability / item / status / side condition / weather / terrain
 *   都能在伤害真正写回前拿到这次伤害事件;
 * - 在 hook 返回数值 relay 时,把新的实际伤害量回写到 mutation。
 *
 * 当前约束:
 * - 多目标 `DamageMutation` 仍然共用一个 mutation 负载;
 * - 因此只有当所有目标都给出一致的新伤害数值时,才会把 relay rewrite 回写成固定值;
 * - 如果不同目标想要不同伤害,后续应引入“按目标拆分 mutation”协议,而不是在这里隐式取最后一个 relay。
 */
class DamageBattleMutationInterceptor : BattleMutationInterceptor {
    /**
     * 排在状态类拦截器之后执行,保持“附加状态先判断,再处理数值类后果”的大致阅读顺序。
     */
    override val order: Int = 200

    override fun supports(mutation: BattleMutation): Boolean = mutation is DamageMutation

    override fun intercept(
        context: BattleMutationInterceptionContext,
        attachedEffectProcessor: BattleAttachedEffectProcessor,
    ): BattleMutationInterceptionResult {
        val mutation =
            context.mutation as? DamageMutation
                ?: return BattleMutationInterceptionResult(context.snapshot, true, context.mutation)
        val targetUnitIds =
            MutationTargetSelectorResolver.resolve(
                mutation.target,
                BattleMutationInterceptorSupport.mutationApplicationContext(context),
            )
        var currentSnapshot = context.snapshot
        var blocked = false
        var rewrittenDamage: Int? = null
        var rewriteConflicted = false

        targetUnitIds.forEach { affectedTargetId ->
            val affectedUnit = currentSnapshot.units[affectedTargetId] ?: return@forEach
            val amount = BattleMutationInterceptorSupport.calculateAmount(affectedUnit, mutation.mode, mutation.value)
            val result =
                attachedEffectProcessor.process(
                    snapshot = currentSnapshot,
                    unitId = affectedTargetId,
                    hookName = StandardHookNames.ON_DAMAGE.value,
                    targetId = affectedTargetId,
                    sourceId = context.sourceId,
                    relay = amount,
                    attributes =
                        buildMap {
                            put("mode", mutation.mode)
                            put("value", mutation.value)
                            put("amount", amount)
                            BattleMutationInterceptorSupport
                                .resolveTargetRelation(currentSnapshot, context.sourceId, affectedTargetId)
                                ?.let { relation -> put(BattleAttributeKeys.TARGET_RELATION, relation) }
                        },
                )
            currentSnapshot = result.snapshot
            if (result.cancelled || result.relay == false) {
                blocked = true
                return@forEach
            }

            val relayDamage = BattleMutationInterceptorSupport.normalizeIntegerRelay(result.relay) ?: return@forEach
            if (targetUnitIds.size == 1) {
                rewrittenDamage = relayDamage.coerceAtLeast(0)
                return@forEach
            }
            if (rewrittenDamage == null) {
                rewrittenDamage = relayDamage.coerceAtLeast(0)
            } else if (rewrittenDamage != relayDamage.coerceAtLeast(0)) {
                rewriteConflicted = true
            }
        }

        val nextMutation =
            if (!blocked && !rewriteConflicted && rewrittenDamage != null) {
                mutation.copy(
                    mode = null,
                    value = requireNotNull(rewrittenDamage).toDouble(),
                )
            } else {
                mutation
            }

        return BattleMutationInterceptionResult(
            snapshot = currentSnapshot,
            allowed = !blocked,
            mutation = nextMutation,
        )
    }
}