DefaultBattleFlowPhaseProcessor.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.dsl.EffectDefinition
import io.github.lishangbu.avalon.game.battle.engine.core.event.EventContext
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.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.HookRuleProcessor
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplier
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleHeldItemRuntimeSupport
import io.github.lishangbu.avalon.game.battle.engine.spi.effect.EffectDefinitionRepository

/**
 * 默认 battle hook phase 处理器。
 *
 * @property effectRepository effect 定义查询入口。
 * @property hookRuleProcessor 单条 hook rule 处理器。
 * @property mutationApplier mutation 写回组件。
 * @property mutationInterceptorChain mutation 拦截链。
 */
class DefaultBattleFlowPhaseProcessor(
    private val effectRepository: EffectDefinitionRepository,
    private val hookRuleProcessor: HookRuleProcessor,
    private val mutationApplier: MutationApplier,
    private val mutationInterceptorChain: BattleMutationInterceptorChain,
) : BattleFlowPhaseProcessor {
    private val triggeredHookDispatcher =
        BattleTriggeredHookDispatcher(
            attachedEffectProcessor =
                BattleAttachedEffectProcessor { attachedSnapshot, unitId, hookName, attachedTargetId, attachedSourceId, relay, attributes ->
                    processAttachedEffects(
                        snapshot = attachedSnapshot,
                        unitId = unitId,
                        hookName = hookName,
                        targetId = attachedTargetId,
                        sourceId = attachedSourceId,
                        relay = relay,
                        attributes = attributes,
                    )
                },
        )

    /**
     * 处理一次完整的 hook phase。
     */
    override fun processPhase(
        snapshot: BattleRuntimeSnapshot,
        hookName: String,
        moveEffect: EffectDefinition,
        selfId: String,
        targetId: String,
        sourceId: String,
        relay: Any?,
        attributes: Map<String, Any?>,
    ): HookPhaseResult {
        var currentResult =
            processEffectHook(
                snapshot = snapshot,
                hookName = hookName,
                effect = moveEffect,
                selfId = selfId,
                targetId = targetId,
                sourceId = sourceId,
                relay = relay,
                attributes = attributes,
            )
        if (currentResult.cancelled) {
            return currentResult
        }

        val attachmentOrder =
            attachmentOrderForPhase(
                hookName = hookName,
                selfId = selfId,
                targetId = targetId,
            )
        attachmentOrder.forEach { unitId ->
            currentResult =
                processAttachedEffects(
                    snapshot = currentResult.snapshot,
                    unitId = unitId,
                    hookName = hookName,
                    targetId = targetId,
                    sourceId = sourceId,
                    relay = currentResult.relay,
                    attributes = attributes,
                )
            if (currentResult.cancelled) {
                return currentResult
            }
        }
        return currentResult
    }

    /**
     * 处理某个单位上挂载 effect 的指定 hook。
     */
    override fun processAttachedEffects(
        snapshot: BattleRuntimeSnapshot,
        unitId: String,
        hookName: String,
        targetId: String?,
        sourceId: String?,
        relay: Any?,
        attributes: Map<String, Any?>,
    ): HookPhaseResult {
        var currentResult = HookPhaseResult(snapshot = snapshot, cancelled = false, relay = relay)
        val unit = requireUnit(currentResult.snapshot, unitId)
        val unitSide = sideOfUnit(currentResult.snapshot, unitId)
        val unitConditionEffectIds =
            unit.conditionStates
                .values
                .sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
                .map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
        val unitVolatileEffectIds =
            unit.volatileStates
                .values
                .sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
                .map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
        val sideConditionEffectIds =
            buildList {
                unitSide
                    ?.conditionStates
                    ?.values
                    ?.sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
                    ?.map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
                    ?.forEach(::add)
            }
        val fieldConditionEffectIds =
            currentResult.snapshot.field.conditionStates
                .values
                .sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
                .map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
        val effectIds =
            buildList {
                unit.abilityId?.let(::add)
                BattleHeldItemRuntimeSupport.activeItemId(unit, currentResult.snapshot.field)?.let(::add)
                unit.statusState?.effectId?.let(::add)
                addAll(unitConditionEffectIds)
                addAll(unitVolatileEffectIds)
                addAll(sideConditionEffectIds)
                addAll(fieldConditionEffectIds)
                currentResult.snapshot.field.weatherState
                    ?.effectId
                    ?.let(::add)
                currentResult.snapshot.field.terrainState
                    ?.effectId
                    ?.let(::add)
            }

        effectIds.forEach { effectId ->
            if (!effectRepository.contains(effectId)) {
                return@forEach
            }
            currentResult =
                processEffectHook(
                    snapshot = currentResult.snapshot,
                    hookName = hookName,
                    effect = effectRepository.get(effectId),
                    selfId = unitId,
                    targetId = targetId,
                    sourceId = sourceId,
                    relay = currentResult.relay,
                    attributes = attributes,
                )
            if (currentResult.cancelled) {
                return currentResult
            }
        }
        return currentResult
    }

    /**
     * 处理某个 effect 自身的指定 hook。
     */
    private fun processEffectHook(
        snapshot: BattleRuntimeSnapshot,
        hookName: String,
        effect: EffectDefinition,
        selfId: String?,
        targetId: String?,
        sourceId: String?,
        relay: Any?,
        attributes: Map<String, Any?>,
    ): HookPhaseResult {
        val rules =
            effect.hooks.entries
                .firstOrNull { entry -> entry.key.value == hookName }
                ?.value
                .orEmpty()
        if (rules.isEmpty()) {
            return HookPhaseResult(snapshot = snapshot, cancelled = false, relay = relay)
        }

        var currentSnapshot = snapshot
        var currentRelay: Any? = relay
        var cancelled = false

        val sortedRules =
            rules.sortedWith(
                compareByDescending<io.github.lishangbu.avalon.game.battle.engine.core.dsl.HookRule> { rule -> rule.priority }
                    .thenByDescending { rule -> rule.subOrder },
            )
        for (rule in sortedRules) {
            val currentSelfSide = selfId?.let { id -> sideOfUnit(currentSnapshot, id) }
            val currentTargetSide = targetId?.let { id -> sideOfUnit(currentSnapshot, id) }
            val context =
                EventContext(
                    hookName =
                        io.github.lishangbu.avalon.game.battle.engine.core.type
                            .HookName(hookName),
                    battle = currentSnapshot.battle,
                    self = selfId?.let { id -> currentSnapshot.units[id] },
                    target = targetId?.let { id -> currentSnapshot.units[id] },
                    source = sourceId?.let { id -> currentSnapshot.units[id] },
                    side = currentSelfSide,
                    foeSide = currentTargetSide,
                    field = currentSnapshot.field,
                    effect = effect,
                    effectLookup = ::findEffectOrNull,
                    relay = currentRelay,
                    attributes = attributes,
                )
            val ruleResult = hookRuleProcessor.process(rule, context)
            currentRelay = ruleResult.relay
            currentSnapshot = applyMutations(currentSnapshot, selfId, targetId, sourceId, ruleResult.mutations)
            if (ruleResult.cancelled) {
                cancelled = true
                break
            }
        }

        return HookPhaseResult(
            snapshot = currentSnapshot,
            cancelled = cancelled,
            relay = currentRelay,
        )
    }

    /**
     * 把一批 mutation 写回当前 battle 快照。
     */
    private fun applyMutations(
        snapshot: BattleRuntimeSnapshot,
        selfId: String?,
        targetId: String?,
        sourceId: String?,
        mutations: List<BattleMutation>,
    ): BattleRuntimeSnapshot {
        if (mutations.isEmpty()) {
            return snapshot
        }
        val filteredResult =
            mutationInterceptorChain.filter(
                snapshot = snapshot,
                selfId = selfId,
                targetId = targetId,
                sourceId = sourceId,
                mutations = mutations,
                attachedEffectProcessor =
                    BattleAttachedEffectProcessor { attachedSnapshot, unitId, hookName, attachedTargetId, attachedSourceId, relay, attributes ->
                        processAttachedEffects(
                            snapshot = attachedSnapshot,
                            unitId = unitId,
                            hookName = hookName,
                            targetId = attachedTargetId,
                            sourceId = attachedSourceId,
                            relay = relay,
                            attributes = attributes,
                        )
                    },
            )
        val applyResult =
            mutationApplier.apply(
                mutations = filteredResult.mutations,
                context =
                    MutationApplicationContext(
                        battle = filteredResult.snapshot.battle,
                        field = filteredResult.snapshot.field,
                        units = filteredResult.snapshot.units,
                        sides = filteredResult.snapshot.sides,
                        selfId = selfId,
                        targetId = targetId,
                        sourceId = sourceId,
                        side = selfId?.let { id -> sideOfUnit(filteredResult.snapshot, id) },
                        foeSide = targetId?.let { id -> sideOfUnit(filteredResult.snapshot, id) },
                    ),
            )
        val appliedSnapshot =
            filteredResult.snapshot.copy(
                battle = applyResult.battle,
                field = applyResult.field,
                units = applyResult.units,
                sides = applyResult.sides,
            )
        return triggeredHookDispatcher.dispatch(
            snapshot = appliedSnapshot,
            triggeredHooks = applyResult.triggeredHooks,
            targetId = targetId,
            sourceId = sourceId,
            participantUnitIds = triggeredHookParticipants(appliedSnapshot, selfId, targetId, sourceId),
        )
    }

    /**
     * 提取本次结算里最直接相关的单位集合,供 `trigger_event` 这种局部后续 hook 使用。
     */
    private fun triggeredHookParticipants(
        snapshot: BattleRuntimeSnapshot,
        selfId: String?,
        targetId: String?,
        sourceId: String?,
    ): List<String> = listOfNotNull(selfId, targetId, sourceId).distinct().filter { unitId -> unitId in snapshot.units }

    /**
     * 返回指定 phase 下挂载 effect 的处理顺序。
     */
    private fun attachmentOrderForPhase(
        hookName: String,
        selfId: String,
        targetId: String,
    ): List<String> =
        when (hookName) {
            StandardHookNames.ON_MODIFY_EVASION.value -> listOf(targetId)
            StandardHookNames.ON_MODIFY_ACCURACY.value -> listOf(selfId)
            StandardHookNames.ON_MODIFY_BASE_POWER.value -> listOf(selfId)
            StandardHookNames.ON_MODIFY_ATTACK.value -> listOf(selfId)
            StandardHookNames.ON_MODIFY_DEFENSE.value -> listOf(selfId)
            StandardHookNames.ON_MODIFY_CRIT_RATIO.value -> listOf(selfId, targetId)
            StandardHookNames.ON_MODIFY_STAB.value -> listOf(selfId)
            StandardHookNames.ON_MODIFY_DAMAGE.value -> listOf(selfId, targetId)
            StandardHookNames.ON_BEFORE_MOVE.value -> listOf(selfId)
            StandardHookNames.ON_TRY_MOVE.value -> listOf(selfId)
            StandardHookNames.ON_AFTER_MOVE.value -> listOf(selfId)
            else -> listOf(selfId, targetId)
        }

    /**
     * 从当前快照中读取指定单位状态。
     */
    private fun requireUnit(
        snapshot: BattleRuntimeSnapshot,
        unitId: String,
    ) = requireNotNull(snapshot.units[unitId]) { "Unit '$unitId' was not found in snapshot." }

    /**
     * 根据单位标识反查其所属 side。
     */
    private fun sideOfUnit(
        snapshot: BattleRuntimeSnapshot,
        unitId: String,
    ): SideState? = snapshot.sides.values.firstOrNull { side -> unitId in side.activeUnitIds || unitId in side.unitIds }

    /**
     * 以空值安全地查询一个 effect。
     */
    private fun findEffectOrNull(effectId: String): EffectDefinition? = if (effectRepository.contains(effectId)) effectRepository.get(effectId) else null
}