BattleMutationInterceptorSupport.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleTargetRelationValues
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleRelayReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatStageSupport

/**
 * battle mutation 拦截器的共用辅助方法集合。
 *
 * 设计意图:
 * - 把各个拦截器都会重复做的目标解析上下文组装、关系判定、数值换算集中收口;
 * - 保持“一个拦截器只关注一个生命周期 hook”的类职责,而不是每个文件都重复一套样板代码;
 * - 让 `set_status / add_volatile / damage / heal / boost` 这些 mutation hook 的上下文语义保持一致。
 */
internal object BattleMutationInterceptorSupport {
    /**
     * 为 mutation target selector 解析组装一份完整上下文。
     *
     * 这里沿用 `DefaultBattleFlowPhaseProcessor` 的快照语义:
     * - `selfId` 表示当前正在处理 hook 的 self;
     * - `targetId` 表示当前 move / effect 主目标;
     * - `sourceId` 表示这次 mutation 的来源单位。
     *
     * 拦截器本身不直接修改这些语义,只负责把它们原样透传给 selector 解析层。
     */
    fun mutationApplicationContext(context: BattleMutationInterceptionContext): MutationApplicationContext =
        MutationApplicationContext(
            battle = context.snapshot.battle,
            field = context.snapshot.field,
            units = context.snapshot.units,
            sides = context.snapshot.sides,
            selfId = context.selfId,
            targetId = context.targetId,
            sourceId = context.sourceId,
            side = sideOfUnit(context.snapshot, context.selfId),
            foeSide = sideOfUnit(context.snapshot, context.targetId),
        )

    /**
     * 计算当前 mutation 在单个目标身上的最终数值。
     *
     * 这里故意与 `DefaultMutationApplier` 中的换算规则保持一致,
     * 避免拦截器看到的 relay 数值和最终落盘值出现两套语义。
     */
    fun calculateAmount(
        unit: UnitState,
        mode: String?,
        value: Double,
    ): Int =
        when (mode) {
            "max_hp_ratio" -> (unit.maxHp * value).toInt()
            else -> value.toInt()
        }

    /**
     * 把 hook relay 规范化为整数语义。
     *
     * 默认数值动作会返回 `Double`,而 `damage / heal / restore_pp` 最终仍按整数落盘,
     * 因此这里统一把 relay 收口成 int。
     */
    fun normalizeIntegerRelay(relay: Any?): Int? = BattleRelayReader.readInt(relay)

    /**
     * 把 hook relay 规范化为 boost 映射。
     *
     * 当前 `on_boost` 虽然还没有默认 DSL 级的 object relay 修改动作,
     * 但运行时允许自定义执行器直接返回 map,这里提前定义可接受的结构:
     * - key 必须是字符串 stat id;
     * - value 必须是数值,并最终按整数 stage 处理。
     */
    fun normalizeBoostRelay(relay: Any?): Map<String, Int>? {
        val relayMap = relay as? Map<*, *> ?: return null
        val normalized = mutableMapOf<String, Int>()
        relayMap.forEach { (key, value) ->
            val statId = key as? String ?: return null
            val stageDelta = value as? Number ?: return null
            normalized[statId] = stageDelta.toInt()
        }
        return BattleStatStageSupport.normalizeBoostPayload(normalized)
    }

    /**
     * 把 hook relay 规范化为属性 id 列表。
     *
     * `on_change_type` 的新语义按“字符串数组”收口,不再只停留在 veto。
     */
    fun normalizeTypeRelay(relay: Any?): List<String>? {
        val relayValues =
            when (relay) {
                is Collection<*> -> relay.toList()
                is Array<*> -> relay.toList()
                else -> return null
            }
        return relayValues.map { value ->
            value as? String ?: return null
        }
    }

    /**
     * 推导“source 对当前 target 的关系”。
     *
     * 该值会作为 attributes 透传给 hook,便于 effect 规则区分:
     * - `self`
     * - `ally`
     * - `foe`
     *
     * 如果来源单位缺失,当前返回空值并由调用方决定是否写入 attributes。
     */
    fun resolveTargetRelation(
        snapshot: BattleRuntimeSnapshot,
        sourceId: String?,
        targetId: String,
    ): String? {
        if (sourceId == null) {
            return null
        }
        val sourceSideId = sideOfUnit(snapshot, sourceId)?.id ?: return null
        val targetSideId = sideOfUnit(snapshot, targetId)?.id ?: return null
        return when {
            sourceId == targetId -> BattleTargetRelationValues.SELF
            sourceSideId == targetSideId -> BattleTargetRelationValues.ALLY
            else -> BattleTargetRelationValues.FOE
        }
    }

    private fun sideOfUnit(
        snapshot: BattleRuntimeSnapshot,
        unitId: String?,
    ): SideState? =
        snapshot.sides.values.firstOrNull { side ->
            unitId in side.activeUnitIds || unitId in side.unitIds
        }
}