BattleSessionMutationProcessor.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.mutation.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleAttachedEffectProcessor
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleTriggeredHookDispatcher

/**
 * session 内部通用 mutation 提交器。
 *
 * 设计意图:
 * - 让 session 内部的“非 move phase mutation”也统一经过 interceptor、applier 和 triggered hook;
 * - 收口 end-turn duration 到期移除、直接伤害补扣血这类原本绕过完整生命周期的路径;
 * - 避免在多个 session 协调类里重复拼装 mutation filter/apply 上下文。
 */
internal class BattleSessionMutationProcessor(
    private val session: BattleSession,
) {
    private val triggeredHookDispatcher =
        BattleTriggeredHookDispatcher(
            attachedEffectProcessor =
                BattleAttachedEffectProcessor { snapshot, unitId, hookName, targetId, sourceId, relay, attributes ->
                    session.battleFlowPhaseProcessor.processAttachedEffects(
                        snapshot = snapshot,
                        unitId = unitId,
                        hookName = hookName,
                        targetId = targetId,
                        sourceId = sourceId,
                        relay = relay,
                        attributes = attributes,
                    )
                },
        )

    /**
     * 以 session 语义提交一批 mutation。
     *
     * @param snapshot 当前提交起点快照
     * @param selfId 当前 mutation 上下文的 self 单位
     * @param targetId 当前 mutation 上下文的目标单位
     * @param sourceId 当前 mutation 上下文的来源单位
     * @param mutations 待提交的 mutation 列表
     */
    fun apply(
        snapshot: BattleRuntimeSnapshot,
        selfId: String?,
        targetId: String?,
        sourceId: String?,
        mutations: List<BattleMutation>,
    ): BattleRuntimeSnapshot {
        if (mutations.isEmpty()) {
            return snapshot
        }
        val filteredResult =
            session.mutationInterceptorChain.filter(
                snapshot = snapshot,
                selfId = selfId,
                targetId = targetId,
                sourceId = sourceId,
                mutations = mutations,
                attachedEffectProcessor =
                    BattleAttachedEffectProcessor { attachedSnapshot, unitId, hookName, attachedTargetId, attachedSourceId, relay, attributes ->
                        session.battleFlowPhaseProcessor.processAttachedEffects(
                            snapshot = attachedSnapshot,
                            unitId = unitId,
                            hookName = hookName,
                            targetId = attachedTargetId,
                            sourceId = attachedSourceId,
                            relay = relay,
                            attributes = attributes,
                        )
                    },
            )
        if (filteredResult.mutations.isEmpty()) {
            return filteredResult.snapshot
        }
        val applyResult =
            session.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(session::sideIdOfUnit)?.let(filteredResult.snapshot.sides::get),
                        foeSide = targetId?.let(session::sideIdOfUnit)?.let(filteredResult.snapshot.sides::get),
                    ),
            )
        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 = listOfNotNull(selfId, targetId, sourceId).distinct(),
        )
    }
}