ApplyConditionBattleMutationInterceptor.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.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplyConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplyFieldConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplySideConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.MutationTargetSelectorResolver
import io.github.lishangbu.avalon.game.battle.engine.core.type.StandardTargetSelectorIds

/**
 * condition 挂载 mutation 的生命周期拦截器。
 *
 * 设计意图:
 * - 把单位 condition、side condition 与 field condition 的“挂载前”生命周期统一收口到 `on_apply_condition`;
 * - 让 ability / item / 状态 / 场地规则可以在 condition 真正写回前观察或拒绝本次挂载;
 * - 保持三条路径共享同一 hook 名称,差异通过 attributes 暴露。
 *
 * 当前约定:
 * - 单位 / side / field 都通过 `conditionScope` 区分作用域;
 * - side condition 会把目标 side 的 active 单位作为 attached-effect 派发载体;
 * - field condition 会把当前全部 active 单位作为 attached-effect 派发载体;
 * - 若没有 active 单位,则 field / side 当前不会派发该 hook。
 */
class ApplyConditionBattleMutationInterceptor : BattleMutationInterceptor {
    override val order: Int = 475

    override fun supports(mutation: BattleMutation): Boolean = mutation is ApplyConditionMutation || mutation is ApplySideConditionMutation || mutation is ApplyFieldConditionMutation

    override fun intercept(
        context: BattleMutationInterceptionContext,
        attachedEffectProcessor: BattleAttachedEffectProcessor,
    ): BattleMutationInterceptionResult =
        when (val mutation = context.mutation) {
            is ApplyConditionMutation -> interceptUnitCondition(context, mutation, attachedEffectProcessor)
            is ApplySideConditionMutation -> interceptSideCondition(context, mutation, attachedEffectProcessor)
            is ApplyFieldConditionMutation -> interceptFieldCondition(context, mutation, attachedEffectProcessor)
            else -> BattleMutationInterceptionResult(context.snapshot, true, context.mutation)
        }

    private fun interceptUnitCondition(
        context: BattleMutationInterceptionContext,
        mutation: ApplyConditionMutation,
        attachedEffectProcessor: BattleAttachedEffectProcessor,
    ): BattleMutationInterceptionResult {
        val targetUnitIds =
            MutationTargetSelectorResolver.resolve(
                mutation.target,
                BattleMutationInterceptorSupport.mutationApplicationContext(context),
            )
        var currentSnapshot = context.snapshot
        var blocked = false

        targetUnitIds.forEach { affectedTargetId ->
            val result =
                attachedEffectProcessor.process(
                    snapshot = currentSnapshot,
                    unitId = affectedTargetId,
                    hookName = StandardHookNames.ON_APPLY_CONDITION.value,
                    targetId = affectedTargetId,
                    sourceId = context.sourceId,
                    relay = true,
                    attributes =
                        buildMap {
                            put("conditionEffectId", mutation.conditionEffectId)
                            put("conditionScope", UNIT_SCOPE)
                            mutation.duration?.let { duration -> put("duration", duration) }
                            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 BattleMutationInterceptionResult(
            snapshot = currentSnapshot,
            allowed = !blocked,
            mutation = mutation,
        )
    }

    private fun interceptSideCondition(
        context: BattleMutationInterceptionContext,
        mutation: ApplySideConditionMutation,
        attachedEffectProcessor: BattleAttachedEffectProcessor,
    ): BattleMutationInterceptionResult {
        val targetSides = resolveTargetSides(context, mutation)
        var currentSnapshot = context.snapshot
        var blocked = false

        targetSides.forEach { targetSide ->
            targetSide.activeUnitIds.distinct().forEach { affectedTargetId ->
                val result =
                    attachedEffectProcessor.process(
                        snapshot = currentSnapshot,
                        unitId = affectedTargetId,
                        hookName = StandardHookNames.ON_APPLY_CONDITION.value,
                        targetId = affectedTargetId,
                        sourceId = context.sourceId,
                        relay = true,
                        attributes =
                            buildMap {
                                put("conditionEffectId", mutation.conditionEffectId)
                                put("conditionScope", SIDE_SCOPE)
                                put("sideId", targetSide.id)
                                mutation.duration?.let { duration -> put("duration", duration) }
                                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 BattleMutationInterceptionResult(
            snapshot = currentSnapshot,
            allowed = !blocked,
            mutation = mutation,
        )
    }

    private fun interceptFieldCondition(
        context: BattleMutationInterceptionContext,
        mutation: ApplyFieldConditionMutation,
        attachedEffectProcessor: BattleAttachedEffectProcessor,
    ): BattleMutationInterceptionResult {
        val targetUnitIds =
            context.snapshot.sides.values
                .flatMap(SideState::activeUnitIds)
                .distinct()
        var currentSnapshot = context.snapshot
        var blocked = false

        targetUnitIds.forEach { affectedTargetId ->
            val result =
                attachedEffectProcessor.process(
                    snapshot = currentSnapshot,
                    unitId = affectedTargetId,
                    hookName = StandardHookNames.ON_APPLY_CONDITION.value,
                    targetId = affectedTargetId,
                    sourceId = context.sourceId,
                    relay = true,
                    attributes =
                        buildMap {
                            put("conditionEffectId", mutation.conditionEffectId)
                            put("conditionScope", FIELD_SCOPE)
                            mutation.duration?.let { duration -> put("duration", duration) }
                            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 BattleMutationInterceptionResult(
            snapshot = currentSnapshot,
            allowed = !blocked,
            mutation = mutation,
        )
    }

    private fun resolveTargetSides(
        context: BattleMutationInterceptionContext,
        mutation: ApplySideConditionMutation,
    ): List<SideState> {
        val applicationContext = BattleMutationInterceptorSupport.mutationApplicationContext(context)
        return when (mutation.target) {
            StandardTargetSelectorIds.SIDE -> listOfNotNull(applicationContext.side)
            StandardTargetSelectorIds.FOE_SIDE -> listOfNotNull(applicationContext.foeSide)
            else -> emptyList()
        }
    }

    private companion object {
        private const val UNIT_SCOPE: String = "unit"
        private const val SIDE_SCOPE: String = "side"
        private const val FIELD_SCOPE: String = "field"
    }
}